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/__init__.py +9 -0
- papi/mocks.py +76 -0
- papi/project.py +161 -0
- papi/tests/__init__.py +0 -0
- papi/tests/test_project.py +123 -0
- papi/tests/test_user.py +121 -0
- papi/tests/test_userdb.json +44 -0
- papi/tests/test_wrappers.py +108 -0
- papi/user.py +197 -0
- papi/wrappers.py +522 -0
- papi_projects-0.1.0.dist-info/METADATA +19 -0
- papi_projects-0.1.0.dist-info/RECORD +17 -0
- papi_projects-0.1.0.dist-info/WHEEL +4 -0
- papi_projects-0.1.0.dist-info/entry_points.txt +4 -0
- scripts/__init__.py +0 -0
- scripts/collatetogglhours.py +64 -0
- scripts/createtogglproject.py +78 -0
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,,
|
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()
|