12
12
from compiler_admin .services .google import user_info as google_user_info
13
13
import compiler_admin .services .files as files
14
14
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
15
# cache of previously seen project information, keyed on Toggl project name
21
16
PROJECT_INFO = {}
22
17
31
26
OUTPUT_COLUMNS = ["Date" , "Client" , "Project" , "Task" , "Notes" , "Hours" , "First name" , "Last name" ]
32
27
33
28
29
+ class Toggl :
30
+ """Toggl API Client.
31
+
32
+ See https://engineering.toggl.com/docs/.
33
+ """
34
+
35
+ API_BASE_URL = "https://api.track.toggl.com"
36
+ API_REPORTS_BASE_URL = "reports/api/v3"
37
+ API_WORKSPACE = "workspace/{}"
38
+ API_HEADERS = {"Content-Type" : "application/json" , "User-Agent" : "compilerla/compiler-admin:{}" .format (__version__ )}
39
+
40
+ def __init__ (self , api_token : str , workspace_id : int , ** kwargs ):
41
+ self ._token = api_token
42
+ self .workspace_id = workspace_id
43
+
44
+ self .headers = dict (self .API_HEADERS )
45
+ self .headers .update (self ._authorization_header ())
46
+
47
+ self .timeout = int (kwargs .get ("timeout" , 5 ))
48
+
49
+ def _authorization_header (self ):
50
+ """Gets an `Authorization: Basic xyz` header using the Toggl API token.
51
+
52
+ See https://engineering.toggl.com/docs/authentication.
53
+ """
54
+ creds = f"{ self ._token } :api_token"
55
+ creds64 = b64encode (bytes (creds , "utf-8" )).decode ("utf-8" )
56
+ return {"Authorization" : "Basic {}" .format (creds64 )}
57
+
58
+ def _make_report_url (self , endpoint : str ):
59
+ """Get a fully formed URL for the Toggl Reports API v3 endpoint.
60
+
61
+ See https://engineering.toggl.com/docs/reports_start.
62
+ """
63
+ return "/" .join ((self .API_BASE_URL , self .API_REPORTS_BASE_URL , self .workspace_url_fragment , endpoint ))
64
+
65
+ @property
66
+ def workspace_url_fragment (self ):
67
+ """The workspace portion of an API URL."""
68
+ return self .API_WORKSPACE .format (self .workspace_id )
69
+
70
+ def post_reports (self , endpoint : str , ** kwargs ) -> requests .Response :
71
+ """Send a POST request to the Reports v3 `endpoint`.
72
+
73
+ Extra `kwargs` are passed through as a POST json body.
74
+
75
+ Will raise for non-200 status codes.
76
+
77
+ See https://engineering.toggl.com/docs/reports_start.
78
+ """
79
+ url = self ._make_report_url (endpoint )
80
+
81
+ response = requests .post (url , json = kwargs , headers = self .headers , timeout = self .timeout )
82
+ response .raise_for_status ()
83
+
84
+ return response
85
+
86
+ def detailed_time_entries (self , start_date : datetime , end_date : datetime , ** kwargs ):
87
+ """Request a CSV report from Toggl of detailed time entries for the given date range.
88
+
89
+ Args:
90
+ start_date (datetime): The beginning of the reporting period.
91
+
92
+ end_date (str): The end of the reporting period.
93
+
94
+ Extra `kwargs` are passed through as a POST json body.
95
+
96
+ By default, requests a report with the following configuration:
97
+ * `billable=True`
98
+ * `rounding=1` (True, but this is an int param)
99
+ * `rounding_minutes=15`
100
+
101
+ See https://engineering.toggl.com/docs/reports/detailed_reports#post-export-detailed-report.
102
+
103
+ Returns:
104
+ response (requests.Response): The HTTP response.
105
+ """
106
+ start = start_date .strftime ("%Y-%m-%d" )
107
+ end = end_date .strftime ("%Y-%m-%d" )
108
+
109
+ # calculate a timeout based on the size of the reporting period in days
110
+ # approximately 5 seconds per month of query size, with a minimum of 5 seconds
111
+ range_days = (end_date - start_date ).days
112
+ current_timeout = self .timeout
113
+ dynamic_timeout = int ((max (30 , range_days ) / 30.0 ) * 5 )
114
+ self .timeout = max (current_timeout , dynamic_timeout )
115
+
116
+ params = dict (
117
+ billable = True ,
118
+ start_date = start ,
119
+ end_date = end ,
120
+ rounding = 1 ,
121
+ rounding_minutes = 15 ,
122
+ )
123
+ params .update (kwargs )
124
+
125
+ response = self .post_reports ("search/time_entries.csv" , ** params )
126
+ self .timeout = current_timeout
127
+
128
+ return response
129
+
130
+
34
131
def _harvest_client_name ():
35
132
"""Gets the value of the HARVEST_CLIENT_NAME env var."""
36
133
return os .environ .get ("HARVEST_CLIENT_NAME" )
@@ -46,37 +143,6 @@ def _get_info(obj: dict, key: str, env_key: str):
46
143
return obj .get (key )
47
144
48
145
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
146
def _toggl_api_token ():
81
147
"""Gets the value of the TOGGL_API_TOKEN env var."""
82
148
return os .environ .get ("TOGGL_API_TOKEN" )
@@ -208,42 +274,17 @@ def download_time_entries(
208
274
209
275
Extra kwargs are passed along in the POST request body.
210
276
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
277
Returns:
220
278
None. Either prints the resulting CSV data or writes to output_path.
221
279
"""
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
280
if ("client_ids" not in kwargs or not kwargs ["client_ids" ]) and isinstance (_toggl_client_id (), int ):
230
281
kwargs ["client_ids" ] = [_toggl_client_id ()]
231
282
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 ()
283
+ token = _toggl_api_token ()
284
+ workspace = _toggl_workspace ()
285
+ toggl = Toggl (token , workspace )
246
286
287
+ response = toggl .detailed_time_entries (start_date , end_date , ** kwargs )
247
288
# the raw response has these initial 3 bytes:
248
289
#
249
290
# b"\xef\xbb\xbfUser,Email,Client..."
0 commit comments