1
- from base64 import b64encode
2
1
from datetime import datetime
3
2
import io
4
3
import os
5
4
import sys
6
5
from typing import TextIO
7
6
8
7
import pandas as pd
9
- import requests
10
8
11
- from compiler_admin import __version__
9
+ from compiler_admin . api . toggl import Toggl
12
10
from compiler_admin .services .google import user_info as google_user_info
13
11
import compiler_admin .services .files as files
14
12
15
- # Toggl API config
16
- API_BASE_URL = "https://api.track.toggl.com"
17
- API_REPORTS_BASE_URL = "reports/api/v3"
18
- API_WORKSPACE = "workspace/{}"
19
-
20
- # cache of previously seen project information, keyed on Toggl project name
21
- PROJECT_INFO = {}
22
-
23
13
# cache of previously seen user information, keyed on email
24
- USER_INFO = {}
14
+ USER_INFO = files . JsonFileCache ( "TOGGL_USER_INFO" )
25
15
NOT_FOUND = "NOT FOUND"
26
16
27
17
# input CSV columns needed for conversion
31
21
OUTPUT_COLUMNS = ["Date" , "Client" , "Project" , "Task" , "Notes" , "Hours" , "First name" , "Last name" ]
32
22
33
23
34
- def _harvest_client_name ():
35
- """Gets the value of the HARVEST_CLIENT_NAME env var."""
36
- return os .environ .get ("HARVEST_CLIENT_NAME" )
37
-
38
-
39
- def _get_info (obj : dict , key : str , env_key : str ):
40
- """Read key from obj, populating obj once from a file path at env_key."""
41
- if obj == {}:
42
- file_path = os .environ .get (env_key )
43
- if file_path :
44
- file_info = files .read_json (file_path )
45
- obj .update (file_info )
46
- return obj .get (key )
47
-
48
-
49
- def _toggl_api_authorization_header ():
50
- """Gets an `Authorization: Basic xyz` header using the Toggl API token.
51
-
52
- See https://engineering.toggl.com/docs/authentication.
53
- """
54
- token = _toggl_api_token ()
55
- creds = f"{ token } :api_token"
56
- creds64 = b64encode (bytes (creds , "utf-8" )).decode ("utf-8" )
57
- return {"Authorization" : "Basic {}" .format (creds64 )}
58
-
59
-
60
- def _toggl_api_headers ():
61
- """Gets a dict of headers for Toggl API requests.
62
-
63
- See https://engineering.toggl.com/docs/.
64
- """
65
- headers = {"Content-Type" : "application/json" }
66
- headers .update ({"User-Agent" : "compilerla/compiler-admin:{}" .format (__version__ )})
67
- headers .update (_toggl_api_authorization_header ())
68
- return headers
69
-
70
-
71
- def _toggl_api_report_url (endpoint : str ):
72
- """Get a fully formed URL for the Toggl Reports API v3 endpoint.
73
-
74
- See https://engineering.toggl.com/docs/reports_start.
75
- """
76
- workspace_id = _toggl_workspace ()
77
- return "/" .join ((API_BASE_URL , API_REPORTS_BASE_URL , API_WORKSPACE .format (workspace_id ), endpoint ))
78
-
79
-
80
- def _toggl_api_token ():
81
- """Gets the value of the TOGGL_API_TOKEN env var."""
82
- return os .environ .get ("TOGGL_API_TOKEN" )
83
-
84
-
85
- def _toggl_client_id ():
86
- """Gets the value of the TOGGL_CLIENT_ID env var."""
87
- client_id = os .environ .get ("TOGGL_CLIENT_ID" )
88
- if client_id :
89
- return int (client_id )
90
- return None
91
-
92
-
93
- def _toggl_project_info (project : str ):
94
- """Return the cached project for the given project key."""
95
- return _get_info (PROJECT_INFO , project , "TOGGL_PROJECT_INFO" )
96
-
97
-
98
- def _toggl_user_info (email : str ):
99
- """Return the cached user for the given email."""
100
- return _get_info (USER_INFO , email , "TOGGL_USER_INFO" )
101
-
102
-
103
- def _toggl_workspace ():
104
- """Gets the value of the TOGGL_WORKSPACE_ID env var."""
105
- return os .environ .get ("TOGGL_WORKSPACE_ID" )
106
-
107
-
108
24
def _get_first_name (email : str ) -> str :
109
25
"""Get cached first name or derive from email."""
110
- user = _toggl_user_info (email )
26
+ user = USER_INFO . get (email )
111
27
first_name = user .get ("First Name" ) if user else None
112
28
if first_name is None :
113
29
parts = email .split ("@" )
@@ -122,7 +38,7 @@ def _get_first_name(email: str) -> str:
122
38
123
39
def _get_last_name (email : str ):
124
40
"""Get cached last name or query from Google."""
125
- user = _toggl_user_info (email )
41
+ user = USER_INFO . get (email )
126
42
last_name = user .get ("Last Name" ) if user else None
127
43
if last_name is None :
128
44
user = google_user_info (email )
@@ -134,7 +50,7 @@ def _get_last_name(email: str):
134
50
return last_name
135
51
136
52
137
- def _str_timedelta (td ):
53
+ def _str_timedelta (td : str ):
138
54
"""Convert a string formatted duration (e.g. 01:30) to a timedelta."""
139
55
return pd .to_timedelta (pd .to_datetime (td , format = "%H:%M:%S" ).strftime ("%H:%M:%S" ))
140
56
@@ -160,7 +76,7 @@ def convert_to_harvest(
160
76
None. Either prints the resulting CSV data or writes to output_path.
161
77
"""
162
78
if client_name is None :
163
- client_name = _harvest_client_name ( )
79
+ client_name = os . environ . get ( "HARVEST_CLIENT_NAME" )
164
80
165
81
# read CSV file, parsing dates and times
166
82
source = files .read_csv (source_path , usecols = INPUT_COLUMNS , parse_dates = ["Start date" ], cache_dates = True )
@@ -175,8 +91,9 @@ def convert_to_harvest(
175
91
source ["Client" ] = client_name
176
92
source ["Task" ] = "Project Consulting"
177
93
178
- # get cached project name if any
179
- source ["Project" ] = source ["Project" ].apply (lambda x : _toggl_project_info (x ) or x )
94
+ # get cached project name if any, keyed on Toggl project name
95
+ project_info = files .JsonFileCache ("TOGGL_PROJECT_INFO" )
96
+ source ["Project" ] = source ["Project" ].apply (lambda x : project_info .get (key = x , default = x ))
180
97
181
98
# assign First and Last name
182
99
source ["First name" ] = source ["Email" ].apply (_get_first_name )
@@ -208,42 +125,20 @@ def download_time_entries(
208
125
209
126
Extra kwargs are passed along in the POST request body.
210
127
211
- By default, requests a report with the following configuration:
212
- * `billable=True`
213
- * `client_ids=[$TOGGL_CLIENT_ID]`
214
- * `rounding=1` (True, but this is an int param)
215
- * `rounding_minutes=15`
216
-
217
- See https://engineering.toggl.com/docs/reports/detailed_reports#post-export-detailed-report.
218
-
219
128
Returns:
220
129
None. Either prints the resulting CSV data or writes to output_path.
221
130
"""
222
- start = start_date .strftime ("%Y-%m-%d" )
223
- end = end_date .strftime ("%Y-%m-%d" )
224
- # calculate a timeout based on the size of the reporting period in days
225
- # approximately 5 seconds per month of query size, with a minimum of 5 seconds
226
- range_days = (end_date - start_date ).days
227
- timeout = int ((max (30 , range_days ) / 30.0 ) * 5 )
228
-
229
- if ("client_ids" not in kwargs or not kwargs ["client_ids" ]) and isinstance (_toggl_client_id (), int ):
230
- kwargs ["client_ids" ] = [_toggl_client_id ()]
231
-
232
- params = dict (
233
- billable = True ,
234
- start_date = start ,
235
- end_date = end ,
236
- rounding = 1 ,
237
- rounding_minutes = 15 ,
238
- )
239
- params .update (kwargs )
240
-
241
- headers = _toggl_api_headers ()
242
- url = _toggl_api_report_url ("search/time_entries.csv" )
243
-
244
- response = requests .post (url , json = params , headers = headers , timeout = timeout )
245
- response .raise_for_status ()
131
+ env_client_id = os .environ .get ("TOGGL_CLIENT_ID" )
132
+ if env_client_id :
133
+ env_client_id = int (env_client_id )
134
+ if ("client_ids" not in kwargs or not kwargs ["client_ids" ]) and isinstance (env_client_id , int ):
135
+ kwargs ["client_ids" ] = [env_client_id ]
136
+
137
+ token = os .environ .get ("TOGGL_API_TOKEN" )
138
+ workspace = os .environ .get ("TOGGL_WORKSPACE_ID" )
139
+ toggl = Toggl (token , workspace )
246
140
141
+ response = toggl .detailed_time_entries (start_date , end_date , ** kwargs )
247
142
# the raw response has these initial 3 bytes:
248
143
#
249
144
# b"\xef\xbb\xbfUser,Email,Client..."
0 commit comments