papi-projects 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
papi/wrappers.py ADDED
@@ -0,0 +1,522 @@
1
+ import re
2
+ import time
3
+ import httpx
4
+ import json
5
+ import pendulum
6
+ import warnings
7
+ from typing import Protocol, runtime_checkable
8
+ from papi.project import Project
9
+
10
+
11
+ def get_project_ids(project_names):
12
+ """This function takes a list of project names and finds and
13
+ returns a list of project IDs.
14
+
15
+ :param project_names: A list of project names.
16
+ :type project_names: list
17
+ :return: A list of project IDs.
18
+ :rtype: list
19
+ """
20
+ project_ids = []
21
+ project_id_pattern = r"P[0-9]{4}-[A-Z]{2}[A-Z0-9]{1}-[A-Z]{4}"
22
+ for project_name in project_names:
23
+ match = re.search(project_id_pattern, project_name)
24
+ if match:
25
+ project_id = match.group()
26
+ if project_id not in project_ids:
27
+ project_ids.append(project_id)
28
+ project_ids = sorted(project_ids)
29
+ return project_ids
30
+
31
+
32
+ @runtime_checkable
33
+ class AsanaWrapper(Protocol):
34
+ """This class is a wrapper around the Asana REST API, that adds specific
35
+ helper functions for getting certain bits of data back from the API, and
36
+ adds convenience functions for creating skeleton projects, for example.
37
+
38
+ :param api_token: Asana API token.
39
+ :type api_token: str
40
+ :param password: Asana password.
41
+ :type password: str
42
+ """
43
+
44
+ def __init__(self, api_token: str, password: str) -> None:
45
+ """Constructor method"""
46
+ self.api_token = api_token
47
+ self.password = password
48
+ self.client = None
49
+ self.me = None
50
+ self.my_id = None
51
+ self.workspaces = None
52
+ self.default_workspace_id = None
53
+ self.teams = None
54
+ self.default_team_id = None
55
+
56
+ def connect(self) -> httpx.Client:
57
+ """Creates a connection to the Asana REST API.
58
+
59
+ :return: The httpx Client instance with appropriate authentication.
60
+ :rtype: httpx.Client
61
+ """
62
+ auth = httpx.BasicAuth(username=self.api_token, password=self.password)
63
+ self.client = httpx.Client(auth=auth)
64
+ return self.client
65
+
66
+ def get_me(self) -> dict:
67
+ """Gets the Asana user's data back from the REST API and returns it as
68
+ a dictionary.
69
+
70
+ :return: A dictionary containing the user's Asana data.
71
+ :rtype: dict
72
+ """
73
+ client = self.connect()
74
+ r = client.get("https://app.asana.com/api/1.0/users/me")
75
+ r_json = r.json()
76
+ return r_json["data"]
77
+
78
+ def set_me(self) -> None:
79
+ """Sets this class' attributes from the data returned by the call to the
80
+ ``get_me`` method, i.e. the user's Asana ID, and their workspaces.
81
+ """
82
+ self.me = self.get_me()
83
+ self.my_id = self.me["gid"]
84
+ workspaces = self.me["workspaces"]
85
+ self.workspaces = {}
86
+ for workspace in workspaces:
87
+ workspace_id = workspace["gid"]
88
+ workspace_name = workspace["name"]
89
+ self.workspaces[workspace_id] = workspace_name
90
+
91
+ def set_workspaces(self) -> None:
92
+ """Calls the ``set_me`` method and hence sets the user's Asnana workspaces."""
93
+ self.set_me()
94
+
95
+ def get_workspace_id_by_name(self, name: str) -> str:
96
+ """Gets an Asana workspace ID from the associated Asana workspace name.
97
+
98
+ :param name: Asnana workspace name.
99
+ :type name: str
100
+ :return: Asnana workspace ID.
101
+ :rtype: str
102
+ """
103
+ if self.workspaces is None:
104
+ self.set_workspaces()
105
+ for id in self.workspaces:
106
+ if self.workspaces[id] == name:
107
+ return id
108
+
109
+ def set_default_workspace(self, name: str) -> str:
110
+ """Sets the user's default Asana workspace. When other class methods are called,
111
+ this is the workspace that will be used.
112
+
113
+ :param name: Name of the Asana workspace to set as default.
114
+ :type name: str
115
+ :return: ID of the default Asana workspace that has been set.
116
+ :rtype: str
117
+ """
118
+ workspace_id = self.get_workspace_id_by_name(name)
119
+ self.default_workspace_id = workspace_id
120
+ return self.default_workspace_id
121
+
122
+ def get_teams(self, workspace_id: str) -> dict:
123
+ """Gets the user's Asana teams using their Asana workspace ID.
124
+
125
+ :param workspace_id: Asana workspace ID from which to get Asana teams.
126
+ :type workspace_id: str
127
+ :return: A dictionary containing the Asana teams data.
128
+ :rtype: dict
129
+ """
130
+ params = {"workspace": workspace_id}
131
+ client = self.connect()
132
+ r = client.get(
133
+ f"https://app.asana.com/api/1.0/users/{self.my_id}/teams", params=params
134
+ )
135
+ r_json = r.json()
136
+ return r_json["data"]
137
+
138
+ def set_teams(self, workspace_id: str) -> None:
139
+ """Gets and sets the user's Asana teams using an Asana workspace ID.
140
+
141
+ :param workspace_id: Asana workspace ID from which to set Asana teams.
142
+ :type workspace_id: str
143
+ """
144
+ if self.workspaces is None:
145
+ self.set_workspaces()
146
+ teams = self.get_teams(workspace_id)
147
+ self.teams = {}
148
+ for team in teams:
149
+ team_id = team["gid"]
150
+ team_name = team["name"]
151
+ self.teams[team_id] = team_name
152
+
153
+ def get_team_id_by_name(self, name: str) -> str:
154
+ """Gets the ID of an Asana team once a user's Asana teams have been set.
155
+
156
+ :param name: Name of the Asana team for which the ID should be found.
157
+ :type name: str
158
+ :raises AttributeError: If the teams attribute is not set on the class,
159
+ then raise an AttributeError.
160
+ :return: The ID of the named Asana team.
161
+ :rtype: str
162
+ """
163
+ if self.teams is not None:
164
+ for id in self.teams:
165
+ if self.teams[id] == name:
166
+ return id
167
+ else:
168
+ raise AttributeError(
169
+ "teams attribute has not been set, set with set_teams() method"
170
+ )
171
+
172
+ def set_default_team(self, name: str) -> str:
173
+ """Sets a user's default Asana team using the name of the team.
174
+
175
+ :param name: Name of the Asana team to set as default.
176
+ :type name: str
177
+ :return: The ID of the Asana team that has been set as default.
178
+ :rtype: str
179
+ """
180
+ team_id = self.get_team_id_by_name(name)
181
+ self.default_team_id = team_id
182
+ return self.default_team_id
183
+
184
+ def get_team_projects(self) -> dict:
185
+ """Gets all of an Asana team's projects.
186
+
187
+ :return: A dictionary containing the team's Asana projects.
188
+ :rtype: dict
189
+ """
190
+ if self.default_team_id is not None:
191
+ team_gid = self.default_team_id
192
+ client = self.connect()
193
+ r = client.get(f"https://app.asana.com/api/1.0/teams/{team_gid}/projects")
194
+ r_json = r.json()
195
+ return r_json["data"]
196
+
197
+ def get_team_project_ids(self) -> list:
198
+ """Gets project IDS from an Asana team's projects.
199
+
200
+ :return: A list of unique project IDs.
201
+ :rtype: list
202
+ """
203
+ projects = self.get_team_projects()
204
+ project_names = [project["name"] for project in projects]
205
+ project_ids = get_project_ids(project_names)
206
+ return project_ids
207
+
208
+ def get_team_project_user_ids(self) -> list:
209
+ """Gets user IDS from an Asana team's projects.
210
+
211
+ :return: A list of unique team project user IDs.
212
+ :rtype: list
213
+ """
214
+ project_ids = self.get_team_project_ids()
215
+ user_ids = sorted(list(set([pid.split("-")[1] for pid in project_ids])))
216
+ return user_ids
217
+
218
+ def create_project(
219
+ self, project: Project, workspace_id: str, team_id: str, template_id: str = None
220
+ ) -> str:
221
+ """Creates a skeleton Asana project from a ``Project`` instance.
222
+
223
+ :param project: ``Project`` instance, for which the Asana project
224
+ should be created.
225
+ :type project: Project
226
+ :param workspace_id: ID of the Asana workspace in which the project
227
+ should be created.
228
+ :type workspace_id: str
229
+ :param team_id: ID of the Asana team for which the project should
230
+ be created.
231
+ :type team_id: str
232
+ :param template_id: Template ID to use when creating the project, defaults to None
233
+ :type template_id: str, optional
234
+ :raises TypeError: If the ``project`` parameter passed is not a valid
235
+ ``Project`` instance, raise a TypeError.
236
+ :return: The ID of the created Asana project.
237
+ :rtype: str
238
+ """
239
+ if not isinstance(project, Project):
240
+ raise TypeError("Provided project is not a valid Project instance")
241
+
242
+ data = {
243
+ "name": project.id,
244
+ "workspace": workspace_id,
245
+ "team": team_id,
246
+ "public": True,
247
+ }
248
+
249
+ if template_id is not None:
250
+ client = self.connect()
251
+ r = client.post(
252
+ f"https://app.asana.com/api/1.0/project_templates/{template_id}/instantiateProject",
253
+ data=data,
254
+ )
255
+ project = r.json()
256
+ project_id = project["data"]["new_project"]["gid"]
257
+ time.sleep(2.5)
258
+ return project_id
259
+ else:
260
+ pass
261
+
262
+ def get_templates(self, team_id: str) -> dict:
263
+ """Gets the templates from the provided Asana team ID.
264
+
265
+ :param team_id: ID of the Asana team from which to retrieve templates.
266
+ :type team_id: str
267
+ :return: A dictionary containing the retrieved Asana templates.
268
+ :rtype: dict
269
+ """
270
+ client = self.connect()
271
+ templates_r = client.get(
272
+ f"https://app.asana.com/api/1.0/teams/{team_id}/project_templates"
273
+ )
274
+ templates = templates_r.json()
275
+ return templates["data"]
276
+
277
+ def get_user_projects(self):
278
+ """Gets all of the Asana user's projects."""
279
+ pass
280
+
281
+ def check_project_exists(self, id):
282
+ """Checks whether an Asana project containing the specified project ID
283
+ already exists.
284
+ """
285
+ pass
286
+
287
+
288
+ class TogglTrackWrapper(Protocol):
289
+ """This class is a wrapper around the Toggl Track REST API, that adds specific
290
+ helper functions for getting certain bits of data back from the API, and
291
+ adds convenience functions for creating projects, for example.
292
+
293
+ :param api_token: Toggl Track API token
294
+ :type api_token: str
295
+ :param password: Toggl Track password
296
+ :type password: str
297
+ """
298
+
299
+ def __init__(self, api_token: str, password: str) -> None:
300
+ """Constructor method"""
301
+ self.api_token = api_token
302
+ self.password = password
303
+ self.client = None
304
+ self.me = None
305
+ self.my_id = None
306
+ self.workspaces = None
307
+ self.default_workspace_id = None
308
+
309
+ def connect(self) -> httpx.Client:
310
+ """Creates a connection to the Toggl Track REST API.
311
+
312
+ :return: The httpx Client instance with appropriate authentication.
313
+ :rtype: httpx.Client
314
+ """
315
+ auth = httpx.BasicAuth(username=self.api_token, password=self.password)
316
+ self.client = httpx.Client(auth=auth)
317
+ return self.client
318
+
319
+ def get_me(self) -> dict:
320
+ """Gets the Toggl Track user's data back from the REST API and
321
+ returns it as a dictionary.
322
+
323
+ :return: A dictionary containing the user's Toggl Track data.
324
+ :rtype: dict
325
+ """
326
+ client = self.connect()
327
+ r = client.get("https://api.track.toggl.com/api/v9/me")
328
+ r_json = r.json()
329
+ return r_json
330
+
331
+ def set_me(self) -> None:
332
+ """Sets this class' attributes from the data returned by the call to the
333
+ ``get_me`` method, i.e. the user's Toggl Track ID, etc.
334
+ """
335
+ self.me = self.get_me()
336
+ self.my_id = self.me["id"]
337
+
338
+ def get_workspaces(self) -> list:
339
+ """Gets all of the Toggl Track user's workspaces.
340
+
341
+ :return: A list containing the user's Toggl Track workspaces.
342
+ :rtype: list
343
+ """
344
+ client = self.connect()
345
+ r = client.get("https://api.track.toggl.com/api/v9/me/workspaces")
346
+ r_json = r.json()
347
+ return r_json
348
+
349
+ def set_workspaces(self) -> None:
350
+ """Calls the ``set_me`` method and hence sets the user's Asnana workspaces."""
351
+ self.workspaces = self.get_workspaces()
352
+
353
+ def get_workspace_id_by_name(self, name: str) -> str:
354
+ """Gets an Toggl Track workspace ID from the associated workspace name.
355
+
356
+ :param name: Toggl Track workspace name.
357
+ :type name: str
358
+ :return: Toggl Track workspace ID.
359
+ :rtype: str
360
+ """
361
+ if self.workspaces is None:
362
+ self.set_workspaces()
363
+ for workspace in self.workspaces:
364
+ if workspace["name"] == name:
365
+ return workspace["id"]
366
+
367
+ def set_default_workspace(self, name: str) -> str:
368
+ """Sets the user's default Toggl Track workspace. When other class
369
+ methods are called, this is the workspace that will be used.
370
+
371
+ :param name: Name of the Toggl Track workspace to set as default.
372
+ :type name: str
373
+ :return: ID of the default Toggl Track workspace that has been set.
374
+ :rtype: str
375
+ """
376
+ workspace_id = self.get_workspace_id_by_name(name)
377
+ self.default_workspace_id = workspace_id
378
+ return self.default_workspace_id
379
+
380
+ def get_user_projects(self) -> dict:
381
+ """Gets all of the Toggl Track user's projects.
382
+
383
+ :return: A dictionary containing the user's Toggl Track projects.
384
+ :rtype: dict
385
+ """
386
+ client = self.connect()
387
+ r = client.get("https://api.track.toggl.com/api/v9/me/projects")
388
+ r_json = r.json()
389
+ return r_json
390
+
391
+ def get_user_hours(self, start_time=None, end_time=pendulum.now().to_rfc3339_string()) -> dict:
392
+ """Gets all of the Toggl Track user's tracked hours for a given
393
+ time period. If no end_time is given, then the current time is used.
394
+
395
+ :return: A dictionary containing the Toggl Track projects and hours tracked.
396
+ :rtype: dict
397
+ """
398
+ if start_time is not None:
399
+ client = self.connect()
400
+ r = client.get("https://api.track.toggl.com/api/v9/me/time_entries", params={"start_date": start_time, "end_date": end_time})
401
+ times_json = r.json()
402
+ times = {}
403
+ for t in times_json:
404
+ pid = t["pid"]
405
+ seconds = t["duration"]
406
+ hours = seconds / 60 / 60
407
+ if pid not in times:
408
+ times[pid] = hours
409
+ else:
410
+ times[pid] += hours
411
+ return times
412
+ else:
413
+ warnings.warn("Please provide a valid start date/time in RFC3339 format!")
414
+ return None
415
+
416
+ def get_workspace_projects(self) -> dict:
417
+ """Gets all of a Toggl Track workspace's projects.
418
+
419
+ :return: A dictionary containing the workspace's Toggl Track projects.
420
+ :rtype: dict
421
+ """
422
+ if self.default_workspace_id is None:
423
+ if self.workspaces is None:
424
+ self.set_workspaces()
425
+ self.default_workspace_id = self.workspaces[0]["id"]
426
+ else:
427
+ self.default_workspace_id = self.workspaces[0]["id"]
428
+ workspace_id = self.default_workspace_id
429
+ client = self.connect()
430
+ r = client.get(
431
+ f"https://api.track.toggl.com/api/v9/workspaces/{workspace_id}/projects"
432
+ )
433
+ r_json = r.json()
434
+ return r_json
435
+
436
+ def get_workspace_project_ids(self) -> list:
437
+ """Gets project IDS from a Toggl Track workspace's projects.
438
+
439
+ :return: A list of unique project IDs.
440
+ :rtype: list
441
+ """
442
+ projects = self.get_workspace_projects()
443
+ project_names = [project["name"] for project in projects]
444
+ project_ids = get_project_ids(project_names)
445
+ return project_ids
446
+
447
+ def get_workspace_project_user_ids(self) -> list:
448
+ """Gets user IDS from a Toggl Track workspace's projects.
449
+
450
+ :return: A list of unique workspace project user IDs.
451
+ :rtype: list
452
+ """
453
+ project_ids = self.get_workspace_project_ids()
454
+ user_ids = sorted(list(set([pid.split("-")[1] for pid in project_ids])))
455
+ return user_ids
456
+
457
+ def check_project_exists(self, id):
458
+ """Checks whether a Toggl Track project containing the specified
459
+ project ID already exists. If a name containing that ID is found,
460
+ return the project details from Toggl Track.
461
+
462
+ :return: A dictionary containing the matching project, if it exists.
463
+ :rtype: dict
464
+ """
465
+ projects = self.get_workspace_projects()
466
+ matching_project = {}
467
+ for project in projects:
468
+ if id in project["name"]:
469
+ matching_project = project
470
+ return matching_project
471
+
472
+ def create_project(
473
+ self,
474
+ project: Project,
475
+ workspace_id: str,
476
+ ) -> str:
477
+ """Creates a skeleton Toggl Track project from a ``Project`` instance.
478
+
479
+ :param project: ``Project`` instance, for which the Toggl Track project
480
+ should be created.
481
+ :type project: Project
482
+ :param workspace_id: ID of the Toggl Track workspace in which the project
483
+ should be created.
484
+ :type workspace_id: str
485
+ :raises TypeError: If the ``project`` parameter passed is not a valid
486
+ ``Project`` instance, raise a TypeError.
487
+ :return: The ID of the created Toggl Track project or None.
488
+ :rtype: str
489
+ """
490
+ if not isinstance(project, Project):
491
+ raise TypeError("Provided project is not a valid Project instance")
492
+ if self.default_workspace_id is None:
493
+ if self.workspaces is None:
494
+ self.set_workspaces()
495
+ self.default_workspace_id = self.workspaces[0]["id"]
496
+ else:
497
+ self.default_workspace_id = self.workspaces[0]["id"]
498
+ workspace_id = self.default_workspace_id
499
+ if not self.check_project_exists(project.id):
500
+ name = project.id
501
+ if project.name != "":
502
+ name += f" - {project.name}"
503
+ if project.grant_code is not None:
504
+ name += f" ({project.grant_code})"
505
+ data = {
506
+ "name": name,
507
+ "active": True,
508
+ "auto_estimates": False,
509
+ "is_private": False,
510
+ "color": "#2da608",
511
+ }
512
+ client = self.connect()
513
+ r = client.post(
514
+ f"https://api.track.toggl.com/api/v9/workspaces/{workspace_id}/projects",
515
+ data=json.dumps(data),
516
+ )
517
+ project = r.json()
518
+ project_id = project["id"]
519
+ time.sleep(2.5)
520
+ return project_id
521
+ else:
522
+ return None
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: papi-projects
3
+ Version: 0.1.0
4
+ Summary: PAPI is an API for managing projects
5
+ License: MIT
6
+ Author: sandyjmacdonald
7
+ Author-email: sandyjmacdonald@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: httpx (>=0.27.2,<0.28.0)
14
+ Requires-Dist: pendulum (>=3.0.0,<4.0.0)
15
+ Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
16
+ Requires-Dist: tinydb (>=4.8.0,<5.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+
@@ -0,0 +1,17 @@
1
+ papi/__init__.py,sha256=4THFjyZzHha87B0ZcMFxvkNJpAvVWGQBA4nyRxzoG34,255
2
+ papi/mocks.py,sha256=J54Q3Ff8lAPdcUyr_YYVEAW7Gnb1PbKxfp865Ca8JGo,1977
3
+ papi/project.py,sha256=wjZh8eChIgSkYCRWuP0va_RPK2iL9M5WBlXTCvbup5M,5306
4
+ papi/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ papi/tests/test_project.py,sha256=HAEtGy0gWiwDQ96xDd2V_MV_akHgkwnl2jBzGuEVK-4,3086
6
+ papi/tests/test_user.py,sha256=L9rS5XlrC6knQQciM4AtHXeg7rRpMXHKSDV84zUBwSk,3230
7
+ papi/tests/test_userdb.json,sha256=Sj2hHxdQYz-h3LlKaG6EjzMEb2MO2sQxmQPoTDRWfiQ,1177
8
+ papi/tests/test_wrappers.py,sha256=MpXpjdwWNeDJ49Vdo3fiYPdM6DxXENsUAHUSFi_xd3I,3021
9
+ papi/user.py,sha256=ThXwasb1zmNTn8XCzYnK-bn0c1ZamcipCW3iIXx7-b4,7068
10
+ papi/wrappers.py,sha256=7oByRzq_5xfXp2BIBOxKJfYMtaNdSv3Froj4y3A8R1Y,18834
11
+ scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ scripts/collatetogglhours.py,sha256=sjIlZycPbRqZHUMH3kjfQunZwuDDPFZxoJB-9WXroWI,2146
13
+ scripts/createtogglproject.py,sha256=u59GXzJkxkJbIaWkR4O6h1eaI8c0PRjCBUcL2LEyA44,3120
14
+ papi_projects-0.1.0.dist-info/METADATA,sha256=ovCp41DWhak8Z-KHOs-CMrfF_b4WFShfLZ4oPOwytDQ,618
15
+ papi_projects-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
16
+ papi_projects-0.1.0.dist-info/entry_points.txt,sha256=okckMdk-hSC5Q3NmtII27kJJh6evm9CuBMYPgIDAqLw,123
17
+ papi_projects-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ collate-toggl-hours=scripts.collatetogglhours:main
3
+ create-toggl-project=scripts.createtogglproject:main
4
+
scripts/__init__.py ADDED
File without changes
@@ -0,0 +1,64 @@
1
+ import argparse
2
+ import pendulum
3
+ from papi.wrappers import TogglTrackWrapper
4
+ from papi import config
5
+
6
+ def main():
7
+ """Main function of collate-toggl-hours script"""
8
+
9
+ # Set up argparse
10
+ parser = argparse.ArgumentParser()
11
+ parser.add_argument(
12
+ "-s", "--start", type=str, help="start date in YYYY-MM-DD format", required=True
13
+ )
14
+ parser.add_argument(
15
+ "-e", "--end", type=str, help="end date in YYYY-MM-DD format, if none supplied then end date is now", default=False
16
+ )
17
+ parser.add_argument(
18
+ "-o",
19
+ "--output",
20
+ type=str,
21
+ help="output TSV filename, omit to write to stdout",
22
+ default=False,
23
+ )
24
+ args = parser.parse_args()
25
+
26
+ # Set up start/end date
27
+ start_date = args.start
28
+ start_time = pendulum.parse(start_date).to_rfc3339_string()
29
+ end_date = args.end
30
+ if not end_date:
31
+ end_time = pendulum.now().to_rfc3339_string()
32
+ else:
33
+ end_time = pendulum.parse(end_date).to_rfc3339_string()
34
+
35
+ # Set up Toggl Track API wrapper
36
+ #
37
+ # NOTE: you must have added Toggl Track API key and password, with
38
+ # variable names below to .env file in this directory
39
+ toggl_api_key = config["TOGGL_TRACK_API_KEY"]
40
+ toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
41
+ toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
42
+
43
+ # Tell wrapper which workspace to set as default
44
+ toggl.set_default_workspace("TF Data Science")
45
+ toggl.set_me()
46
+
47
+ # Get tracked hours and tracked project IDs/names
48
+ tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
49
+ projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
50
+
51
+ output = args.output
52
+
53
+ if output:
54
+ # If output filename provided, write to file
55
+ with open(output, "w") as out:
56
+ for t in tracked_hours:
57
+ out.write(f"{projects[t]}\t{tracked_hours[t]}\n")
58
+ else:
59
+ # Otherwise, print out project names and tracked hours to stdout
60
+ for t in tracked_hours:
61
+ print(f"{projects[t]}\t{tracked_hours[t]}")
62
+
63
+ if __name__ == "__main__":
64
+ main()