papi-projects 0.1.8__tar.gz → 0.2.0__tar.gz
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_projects-0.1.8 → papi_projects-0.2.0}/PKG-INFO +1 -1
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/project.py +68 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/user.py +1 -1
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/wrappers.py +47 -26
- {papi_projects-0.1.8 → papi_projects-0.2.0}/pyproject.toml +1 -1
- {papi_projects-0.1.8 → papi_projects-0.2.0}/scripts/collate_toggl_hours.py +5 -4
- {papi_projects-0.1.8 → papi_projects-0.2.0}/scripts/create_project.py +1 -1
- {papi_projects-0.1.8 → papi_projects-0.2.0}/scripts/create_toggl_project.py +0 -1
- {papi_projects-0.1.8 → papi_projects-0.2.0}/README.md +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/__init__.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/mocks.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/tests/__init__.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/tests/test_project.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/tests/test_user.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/tests/test_userdb.json +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/papi/tests/test_wrappers.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/scripts/__init__.py +0 -0
- {papi_projects-0.1.8 → papi_projects-0.2.0}/scripts/create_notion_project.py +0 -0
|
@@ -12,6 +12,70 @@ logger = logging.getLogger(__name__)
|
|
|
12
12
|
|
|
13
13
|
THIS_YEAR = pendulum.now().year
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
def get_project_ids(project_names) -> list:
|
|
17
|
+
"""This function takes a list of project names and finds and
|
|
18
|
+
returns a list of project IDs.
|
|
19
|
+
|
|
20
|
+
:param project_names: A list of project names.
|
|
21
|
+
:type project_names: list
|
|
22
|
+
:return: A list of project IDs.
|
|
23
|
+
:rtype: list
|
|
24
|
+
"""
|
|
25
|
+
logger.debug("Calling get_project_ids function")
|
|
26
|
+
project_ids = []
|
|
27
|
+
project_id_pattern = r"P[0-9]{4}-[A-Z]{2}[A-Z0-9]{1}-[A-Z]{4}"
|
|
28
|
+
for project_name in project_names:
|
|
29
|
+
match = re.search(project_id_pattern, project_name)
|
|
30
|
+
if match:
|
|
31
|
+
project_id = match.group()
|
|
32
|
+
if project_id not in project_ids:
|
|
33
|
+
project_ids.append(project_id)
|
|
34
|
+
project_ids = sorted(project_ids)
|
|
35
|
+
if len(project_ids):
|
|
36
|
+
logger.info(f"{len(project_ids)} project IDs found")
|
|
37
|
+
else:
|
|
38
|
+
logger.info(f"No project IDs found")
|
|
39
|
+
return project_ids
|
|
40
|
+
|
|
41
|
+
def decompose_project_name(project_name) -> dict:
|
|
42
|
+
"""This function takes a project name string and attempts
|
|
43
|
+
to split out a project ID, name, and grant code.
|
|
44
|
+
|
|
45
|
+
:param project_name: A project name string.
|
|
46
|
+
:type project_name: str
|
|
47
|
+
:return: A dictionary with the split-out parts.
|
|
48
|
+
:rtype: dict
|
|
49
|
+
"""
|
|
50
|
+
logger.debug("Calling decompose_project_name function")
|
|
51
|
+
pattern = r"""
|
|
52
|
+
^
|
|
53
|
+
(?P<project_id>P\d{4}-(?:[A-Z]{2}\d|[A-Z]{3})-[A-Z]{4})?
|
|
54
|
+
(?:\s*[-–—]\s*|\s+)?
|
|
55
|
+
(?P<project_name>[^()\[\]]+?)?
|
|
56
|
+
(?:\s*[\(\[]\s*
|
|
57
|
+
(?P<grant_code>[^)\]]+)
|
|
58
|
+
\s*[\)\]])?
|
|
59
|
+
$
|
|
60
|
+
"""
|
|
61
|
+
regex = re.compile(pattern, re.VERBOSE)
|
|
62
|
+
match = regex.match(project_name)
|
|
63
|
+
if match:
|
|
64
|
+
project_id = match.group('project_id')
|
|
65
|
+
project_name = match.group('project_name')
|
|
66
|
+
grant_code = match.group('grant_code')
|
|
67
|
+
return {
|
|
68
|
+
"project_id": project_id,
|
|
69
|
+
"project_name": project_name,
|
|
70
|
+
"grant_code": grant_code
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
return {
|
|
74
|
+
"project_id": None,
|
|
75
|
+
"project_name": None,
|
|
76
|
+
"grant_code": None
|
|
77
|
+
}
|
|
78
|
+
|
|
15
79
|
def check_project_id(id: str) -> bool:
|
|
16
80
|
"""Checks whether a project ID is correctly formed.
|
|
17
81
|
|
|
@@ -115,6 +179,8 @@ class Project(Protocol):
|
|
|
115
179
|
p_uuid: str = None,
|
|
116
180
|
name: str = "",
|
|
117
181
|
grant_code: str = None,
|
|
182
|
+
created_at = None,
|
|
183
|
+
modified_at = None,
|
|
118
184
|
) -> None:
|
|
119
185
|
"""Constructor method"""
|
|
120
186
|
logger.debug("Creating Project instance")
|
|
@@ -122,6 +188,8 @@ class Project(Protocol):
|
|
|
122
188
|
self.user_id = user_id
|
|
123
189
|
self.grant_code = grant_code
|
|
124
190
|
self.name = name
|
|
191
|
+
self.created_at = created_at
|
|
192
|
+
self.modified_at = modified_at
|
|
125
193
|
if suffix is not None:
|
|
126
194
|
self.suffix = suffix
|
|
127
195
|
else:
|
|
@@ -184,7 +184,7 @@ class UserDB(Protocol):
|
|
|
184
184
|
else:
|
|
185
185
|
user.user_id = f"{first_last_initial}1"
|
|
186
186
|
self.db.insert(user.to_json())
|
|
187
|
-
logger.info(f"User ID '{
|
|
187
|
+
logger.info(f"User ID '{user.user_id}' inserted into user database")
|
|
188
188
|
return user.user_id
|
|
189
189
|
|
|
190
190
|
def search_by_user_name(self, user_name: str) -> list:
|
|
@@ -6,36 +6,11 @@ import pendulum
|
|
|
6
6
|
import warnings
|
|
7
7
|
import logging
|
|
8
8
|
from typing import Protocol, runtime_checkable
|
|
9
|
-
from papi.project import Project
|
|
9
|
+
from papi.project import Project, get_project_ids, decompose_project_name
|
|
10
10
|
from papi.user import User
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
-
def get_project_ids(project_names):
|
|
15
|
-
"""This function takes a list of project names and finds and
|
|
16
|
-
returns a list of project IDs.
|
|
17
|
-
|
|
18
|
-
:param project_names: A list of project names.
|
|
19
|
-
:type project_names: list
|
|
20
|
-
:return: A list of project IDs.
|
|
21
|
-
:rtype: list
|
|
22
|
-
"""
|
|
23
|
-
logger.debug("Calling get_project_ids function")
|
|
24
|
-
project_ids = []
|
|
25
|
-
project_id_pattern = r"P[0-9]{4}-[A-Z]{2}[A-Z0-9]{1}-[A-Z]{4}"
|
|
26
|
-
for project_name in project_names:
|
|
27
|
-
match = re.search(project_id_pattern, project_name)
|
|
28
|
-
if match:
|
|
29
|
-
project_id = match.group()
|
|
30
|
-
if project_id not in project_ids:
|
|
31
|
-
project_ids.append(project_id)
|
|
32
|
-
project_ids = sorted(project_ids)
|
|
33
|
-
if len(project_ids):
|
|
34
|
-
logger.info(f"{len(project_ids)} project IDs found")
|
|
35
|
-
else:
|
|
36
|
-
logger.info(f"No project IDs found")
|
|
37
|
-
return project_ids
|
|
38
|
-
|
|
39
14
|
|
|
40
15
|
@runtime_checkable
|
|
41
16
|
class AsanaWrapper(Protocol):
|
|
@@ -433,6 +408,29 @@ class TogglTrackWrapper(Protocol):
|
|
|
433
408
|
r = client.get("https://api.track.toggl.com/api/v9/me/projects")
|
|
434
409
|
r_json = r.json()
|
|
435
410
|
return r_json
|
|
411
|
+
|
|
412
|
+
def get_user_project_objects(self) -> list:
|
|
413
|
+
"""Gets all of the Toggl Track user's projects as Project instances.
|
|
414
|
+
|
|
415
|
+
:return: A list of Project instances.
|
|
416
|
+
:rtype: dict
|
|
417
|
+
"""
|
|
418
|
+
logger.debug("Calling TogglTrackWrapper.get_user_project_objects method")
|
|
419
|
+
user_projects_json = self.get_user_projects()
|
|
420
|
+
user_project_names = [p["name"] for p in user_projects_json]
|
|
421
|
+
user_project_created = [pendulum.parse(p["created_at"]) for p in user_projects_json]
|
|
422
|
+
user_project_modified = [pendulum.parse(p["at"]) for p in user_projects_json]
|
|
423
|
+
user_projects = []
|
|
424
|
+
for i, n in enumerate(user_project_names):
|
|
425
|
+
decomposed = decompose_project_name(n)
|
|
426
|
+
if decomposed["project_id"] is not None:
|
|
427
|
+
project_id = decomposed["project_id"]
|
|
428
|
+
project_name = decomposed["project_name"]
|
|
429
|
+
created_at = user_project_created[i]
|
|
430
|
+
modified_at = user_project_modified[i]
|
|
431
|
+
project = Project(id=project_id, name=project_name, created_at=created_at, modified_at=modified_at)
|
|
432
|
+
user_projects.append(project)
|
|
433
|
+
return user_projects
|
|
436
434
|
|
|
437
435
|
def get_user_hours(
|
|
438
436
|
self, start_time=None, end_time=pendulum.now().to_rfc3339_string()
|
|
@@ -509,6 +507,29 @@ class TogglTrackWrapper(Protocol):
|
|
|
509
507
|
user_ids = sorted(list(set([pid.split("-")[1] for pid in project_ids])))
|
|
510
508
|
return user_ids
|
|
511
509
|
|
|
510
|
+
def get_workspace_project_objects(self) -> list:
|
|
511
|
+
"""Gets all of the Toggl Track workspace's projects as Project instances.
|
|
512
|
+
|
|
513
|
+
:return: A list of Project instances.
|
|
514
|
+
:rtype: dict
|
|
515
|
+
"""
|
|
516
|
+
logger.debug("Calling TogglTrackWrapper.get_workspace_project_objects method")
|
|
517
|
+
workspace_projects_json = self.get_workspace_projects()
|
|
518
|
+
workspace_project_names = [p["name"] for p in workspace_projects_json]
|
|
519
|
+
workspace_projects = []
|
|
520
|
+
workspace_project_created = [pendulum.parse(p["created_at"]) for p in workspace_projects_json]
|
|
521
|
+
workspace_project_modified = [pendulum.parse(p["at"]) for p in workspace_projects_json]
|
|
522
|
+
for i, n in enumerate(workspace_project_names):
|
|
523
|
+
decomposed = decompose_project_name(n)
|
|
524
|
+
if decomposed["project_id"] is not None:
|
|
525
|
+
project_id = decomposed["project_id"]
|
|
526
|
+
project_name = decomposed["project_name"]
|
|
527
|
+
created_at = workspace_project_created[i]
|
|
528
|
+
modified_at = workspace_project_modified[i]
|
|
529
|
+
project = Project(id=project_id, name=project_name, created_at=created_at, modified_at=modified_at)
|
|
530
|
+
workspace_projects.append(project)
|
|
531
|
+
return workspace_projects
|
|
532
|
+
|
|
512
533
|
def check_project_exists(self, id: str) -> str:
|
|
513
534
|
"""Checks whether a Toggl Track project containing the specified
|
|
514
535
|
project ID already exists. If a name containing that ID is found,
|
|
@@ -48,18 +48,19 @@ def main():
|
|
|
48
48
|
# Get tracked hours and tracked project IDs/names
|
|
49
49
|
tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
|
|
50
50
|
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
51
|
+
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
51
52
|
|
|
52
53
|
output = args.output
|
|
53
54
|
|
|
54
55
|
if output:
|
|
55
56
|
# If output filename provided, write to file
|
|
56
57
|
with open(output, "w") as out:
|
|
57
|
-
for
|
|
58
|
-
out.write(f"{
|
|
58
|
+
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
59
|
+
out.write(f"{h[0]}\t{h[1]}\n")
|
|
59
60
|
else:
|
|
60
61
|
# Otherwise, print out project names and tracked hours to stdout
|
|
61
|
-
for
|
|
62
|
-
print(f"{
|
|
62
|
+
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
63
|
+
print(f"{h[0]}\t{h[1]}")
|
|
63
64
|
|
|
64
65
|
if __name__ == "__main__":
|
|
65
66
|
main()
|
|
@@ -29,7 +29,7 @@ def prompt_for_args():
|
|
|
29
29
|
if enable_logging:
|
|
30
30
|
log_level_choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
31
31
|
log_level = input(f"Set log level {log_level_choices} [INFO]: ").strip().upper()
|
|
32
|
-
log_level = log_level if log_level else
|
|
32
|
+
log_level = log_level if log_level else "INFO"
|
|
33
33
|
if log_level is not None and log_level not in log_level_choices:
|
|
34
34
|
print("Invalid log level. Defaulting to 'INFO'.")
|
|
35
35
|
log_level = 'INFO'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|