papi-projects 0.1.9__tar.gz → 0.2.1__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.9 → papi_projects-0.2.1}/PKG-INFO +1 -1
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/project.py +68 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/user.py +1 -1
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/wrappers.py +47 -26
- {papi_projects-0.1.9 → papi_projects-0.2.1}/pyproject.toml +1 -1
- {papi_projects-0.1.9 → papi_projects-0.2.1}/scripts/collate_toggl_hours.py +5 -4
- {papi_projects-0.1.9 → papi_projects-0.2.1}/scripts/create_notion_project.py +11 -11
- {papi_projects-0.1.9 → papi_projects-0.2.1}/scripts/create_project.py +11 -11
- {papi_projects-0.1.9 → papi_projects-0.2.1}/scripts/create_toggl_project.py +10 -21
- {papi_projects-0.1.9 → papi_projects-0.2.1}/README.md +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/__init__.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/mocks.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/tests/__init__.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/tests/test_project.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/tests/test_user.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/tests/test_userdb.json +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/papi/tests/test_wrappers.py +0 -0
- {papi_projects-0.1.9 → papi_projects-0.2.1}/scripts/__init__.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()
|
|
@@ -51,24 +51,24 @@ def main():
|
|
|
51
51
|
project_name = args.name
|
|
52
52
|
project_id = args.project_id
|
|
53
53
|
|
|
54
|
-
if user_id and not user_name:
|
|
55
|
-
user = User(user_id=user_id)
|
|
56
|
-
elif user_name and not user_id:
|
|
57
|
-
user = User(user_name)
|
|
58
|
-
elif user_id and user_name:
|
|
59
|
-
user = User(user_name, user_id=user_id)
|
|
60
|
-
else:
|
|
61
|
-
logger.warning("Please provide either a three-letter user ID and/or user name")
|
|
62
|
-
return
|
|
63
|
-
|
|
64
54
|
if project_id and not project_name:
|
|
65
55
|
project = Project(id=project_id)
|
|
56
|
+
user_id = project.user_id
|
|
57
|
+
user = User(user_id=user_id)
|
|
66
58
|
elif project_name and not project_id:
|
|
59
|
+
if user_id and not user_name:
|
|
60
|
+
user = User(user_id=user_id)
|
|
61
|
+
elif user_name and not user_id:
|
|
62
|
+
user = User(user_name)
|
|
63
|
+
elif user_id and user_name:
|
|
64
|
+
user = User(user_name, user_id=user_id)
|
|
67
65
|
project = Project(name=project_name, user_id=user.user_id)
|
|
68
66
|
elif project_id and project_name:
|
|
69
67
|
project = Project(name=project_name, id=project_id)
|
|
68
|
+
user_id = project.user_id
|
|
69
|
+
user = User(user_id=user_id)
|
|
70
70
|
else:
|
|
71
|
-
logger.warning("Please provide either a valid project ID
|
|
71
|
+
logger.warning("Please provide either a valid project ID or valid three-letter user ID")
|
|
72
72
|
return
|
|
73
73
|
|
|
74
74
|
# Create project on Notion
|
|
@@ -108,24 +108,24 @@ def main():
|
|
|
108
108
|
enable_toggl = args.enable_toggl
|
|
109
109
|
enable_notion = args.enable_notion
|
|
110
110
|
|
|
111
|
-
if user_id and not user_name:
|
|
112
|
-
user = User(user_id=user_id)
|
|
113
|
-
elif user_name and not user_id:
|
|
114
|
-
user = User(user_name)
|
|
115
|
-
elif user_id and user_name:
|
|
116
|
-
user = User(user_name, user_id=user_id)
|
|
117
|
-
else:
|
|
118
|
-
logger.warning("Please provide either a three-letter user ID and/or user name")
|
|
119
|
-
return
|
|
120
|
-
|
|
121
111
|
if project_id and not project_name:
|
|
122
112
|
project = Project(id=project_id)
|
|
113
|
+
user_id = project.user_id
|
|
114
|
+
user = User(user_id=user_id)
|
|
123
115
|
elif project_name and not project_id:
|
|
116
|
+
if user_id and not user_name:
|
|
117
|
+
user = User(user_id=user_id)
|
|
118
|
+
elif user_name and not user_id:
|
|
119
|
+
user = User(user_name)
|
|
120
|
+
elif user_id and user_name:
|
|
121
|
+
user = User(user_name, user_id=user_id)
|
|
124
122
|
project = Project(name=project_name, user_id=user.user_id)
|
|
125
123
|
elif project_id and project_name:
|
|
126
124
|
project = Project(name=project_name, id=project_id)
|
|
125
|
+
user_id = project.user_id
|
|
126
|
+
user = User(user_id=user_id)
|
|
127
127
|
else:
|
|
128
|
-
logger.warning("Please provide either a valid project ID
|
|
128
|
+
logger.warning("Please provide either a valid project ID or valid three-letter user ID")
|
|
129
129
|
return
|
|
130
130
|
|
|
131
131
|
if enable_toggl:
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import warnings
|
|
3
2
|
from papi.wrappers import TogglTrackWrapper
|
|
4
3
|
from papi import config, setup_logger
|
|
5
4
|
from papi.project import Project
|
|
@@ -54,28 +53,18 @@ def main():
|
|
|
54
53
|
project_name = args.name
|
|
55
54
|
project_id = args.project_id
|
|
56
55
|
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
project = Project(user_id=user_id, grant_code=grant_code)
|
|
63
|
-
elif not grant_code and project_name:
|
|
64
|
-
project = Project(user_id=user_id, name=project_name)
|
|
56
|
+
if project_id and not project_name:
|
|
57
|
+
project = Project(id=project_id)
|
|
58
|
+
elif project_name and not project_id:
|
|
59
|
+
if user_id:
|
|
60
|
+
project = Project(name=project_name, user_id=user_id)
|
|
65
61
|
else:
|
|
66
|
-
project
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
project = Project(id=project_id, grant_code=grant_code, name=project_name)
|
|
71
|
-
elif grant_code and not project_name:
|
|
72
|
-
project = Project(id=project_id, grant_code=grant_code)
|
|
73
|
-
elif not grant_code and project_name:
|
|
74
|
-
project = Project(id=project_id, name=project_name)
|
|
75
|
-
else:
|
|
76
|
-
project = Project(id=project_id)
|
|
62
|
+
logger.warning("Please provide either a valid project ID or valid three-letter user ID")
|
|
63
|
+
return
|
|
64
|
+
elif project_id and project_name:
|
|
65
|
+
project = Project(name=project_name, id=project_id)
|
|
77
66
|
else:
|
|
78
|
-
logger.warning("Please provide either a
|
|
67
|
+
logger.warning("Please provide either a valid project ID or valid three-letter user ID")
|
|
79
68
|
return
|
|
80
69
|
|
|
81
70
|
# Create project on Toggl Track
|
|
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
|