papi-projects 0.4.3__tar.gz → 0.4.4__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.4.3 → papi_projects-0.4.4}/PKG-INFO +1 -1
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/__init__.py +13 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/wrappers.py +6 -3
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/PKG-INFO +1 -1
- {papi_projects-0.4.3 → papi_projects-0.4.4}/pyproject.toml +1 -1
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/collate_toggl_hours.py +9 -1
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/generate_timesheet.py +57 -23
- {papi_projects-0.4.3 → papi_projects-0.4.4}/README.md +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/project.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/task.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/user.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/workorder.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/SOURCES.txt +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/dependency_links.txt +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/entry_points.txt +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/requires.txt +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/top_level.txt +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/__init__.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_notion_project.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_notion_task.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_project.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_toggl_project.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/test_notion.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/setup.cfg +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_project.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_task.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_user.py +0 -0
- {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_wrappers.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
|
+
from decimal import Decimal, ROUND_UP
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
try:
|
|
@@ -81,6 +82,18 @@ if missing:
|
|
|
81
82
|
raise RuntimeError(f"Missing required config values: {', '.join(missing)}")
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def round_up_to_multiple(hours: float, multiple: float = 7.5) -> float:
|
|
86
|
+
"""Round hours up to the nearest multiple of `multiple` (default 7.5).
|
|
87
|
+
|
|
88
|
+
Values that are already an exact multiple are left unchanged, e.g. 7.5 -> 7.5,
|
|
89
|
+
1 -> 7.5, 10 -> 15. Uses Decimal to avoid floating-point rounding surprises.
|
|
90
|
+
"""
|
|
91
|
+
h = Decimal(str(hours))
|
|
92
|
+
m = Decimal(str(multiple))
|
|
93
|
+
multiples = (h / m).quantize(Decimal("1"), rounding=ROUND_UP)
|
|
94
|
+
return float(multiples * m)
|
|
95
|
+
|
|
96
|
+
|
|
84
97
|
def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
|
|
85
98
|
logger = logging.getLogger('papi')
|
|
86
99
|
|
|
@@ -1077,20 +1077,23 @@ class NotionWrapper(Protocol):
|
|
|
1077
1077
|
|
|
1078
1078
|
props = page.get("properties", {})
|
|
1079
1079
|
|
|
1080
|
+
nid = page.get("id")
|
|
1080
1081
|
try:
|
|
1081
1082
|
pid = project_id or props["Project ID"]["title"][0]["plain_text"]
|
|
1082
1083
|
name = props["Description"]["rich_text"][0]["plain_text"]
|
|
1083
|
-
owner = [u
|
|
1084
|
+
owner = [u.get("name") for u in props["Owner"]["people"] if u.get("name")]
|
|
1084
1085
|
status = props["Status"]["status"]["name"]
|
|
1085
1086
|
priority = props["Priority"]["select"]["name"]
|
|
1086
1087
|
user_id = props["PI Code"]["rollup"]["array"][0]["rich_text"][0]["plain_text"]
|
|
1087
|
-
nid = page["id"]
|
|
1088
1088
|
if len(props["Workorder"]["relation"]):
|
|
1089
1089
|
workorder_page_id = props["Workorder"]["relation"][0]["id"]
|
|
1090
1090
|
else:
|
|
1091
1091
|
workorder_page_id = None
|
|
1092
1092
|
except (KeyError, IndexError) as e:
|
|
1093
|
-
logger.error(
|
|
1093
|
+
logger.error(
|
|
1094
|
+
f"Malformed project properties for Project ID={project_id!r} "
|
|
1095
|
+
f"on Notion page {nid!r}: {e!r}"
|
|
1096
|
+
)
|
|
1094
1097
|
return None
|
|
1095
1098
|
|
|
1096
1099
|
if workorders_db_id is not None and workorder_page_id is not None:
|
|
@@ -2,7 +2,7 @@ import argparse
|
|
|
2
2
|
import pendulum
|
|
3
3
|
import warnings
|
|
4
4
|
from papi.wrappers import TogglTrackWrapper
|
|
5
|
-
from papi import config
|
|
5
|
+
from papi import config, round_up_to_multiple
|
|
6
6
|
|
|
7
7
|
def main():
|
|
8
8
|
"""Main function of collate-toggl-hours script"""
|
|
@@ -22,6 +22,11 @@ def main():
|
|
|
22
22
|
help="output TSV filename, omit to write to stdout",
|
|
23
23
|
default=False,
|
|
24
24
|
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--round",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="round each project's total hours up to the nearest 7.5",
|
|
29
|
+
)
|
|
25
30
|
args = parser.parse_args()
|
|
26
31
|
|
|
27
32
|
# Set up start/end date
|
|
@@ -55,6 +60,9 @@ def main():
|
|
|
55
60
|
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
56
61
|
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
57
62
|
|
|
63
|
+
if args.round:
|
|
64
|
+
hours_per_project = [(name, round_up_to_multiple(hours)) for name, hours in hours_per_project]
|
|
65
|
+
|
|
58
66
|
output = args.output
|
|
59
67
|
|
|
60
68
|
if output:
|
|
@@ -4,7 +4,7 @@ import warnings
|
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from papi.wrappers import TogglTrackWrapper, NotionWrapper
|
|
7
|
-
from papi import config, setup_logger
|
|
7
|
+
from papi import config, setup_logger, round_up_to_multiple
|
|
8
8
|
from papi.project import get_project_ids, check_project_id
|
|
9
9
|
from decimal import Decimal, ROUND_UP
|
|
10
10
|
import pandas as pd
|
|
@@ -51,6 +51,8 @@ def calculate_cost(hours: float, hourly_rate: float) -> float:
|
|
|
51
51
|
|
|
52
52
|
def get_toggl_hours(start_time, end_time, toggl):
|
|
53
53
|
tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
|
|
54
|
+
if not tracked_hours:
|
|
55
|
+
return {}
|
|
54
56
|
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
55
57
|
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
56
58
|
hours_dict = {}
|
|
@@ -67,6 +69,11 @@ def main():
|
|
|
67
69
|
parser.add_argument("-s", "--start", type=str, required=True)
|
|
68
70
|
parser.add_argument("-e", "--end", type=str, default=False)
|
|
69
71
|
parser.add_argument("--disable-logging", action="store_true")
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--round",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="round each project's total hours (across all users) up to the nearest 7.5",
|
|
76
|
+
)
|
|
70
77
|
parser.add_argument("-o", "--output", type=str, required=True)
|
|
71
78
|
args = parser.parse_args()
|
|
72
79
|
|
|
@@ -104,7 +111,17 @@ def main():
|
|
|
104
111
|
for user in toggl_users:
|
|
105
112
|
print(user)
|
|
106
113
|
toggl = initialise_toggl(user["api_key"], toggl_workspace)
|
|
107
|
-
|
|
114
|
+
try:
|
|
115
|
+
hours_dict = get_toggl_hours(start_time, end_time, toggl)
|
|
116
|
+
except Exception:
|
|
117
|
+
logger.error(f"[Toggl] Failed fetching hours for user {user['name']!r}")
|
|
118
|
+
raise
|
|
119
|
+
if not hours_dict:
|
|
120
|
+
logger.warning(
|
|
121
|
+
f"[Toggl] No hours tracked for user {user['name']!r} "
|
|
122
|
+
f"in the requested period; skipping."
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
108
125
|
print(hours_dict)
|
|
109
126
|
|
|
110
127
|
for project_id, hours in hours_dict.items():
|
|
@@ -115,27 +132,34 @@ def main():
|
|
|
115
132
|
cost = 0.0
|
|
116
133
|
workorder_id = ""
|
|
117
134
|
if check_project_id(project_id):
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
try:
|
|
136
|
+
project = notion.get_project(
|
|
137
|
+
config["NOTION_PROJECTS_DB"],
|
|
138
|
+
project_id,
|
|
139
|
+
workorders_db_id=config["NOTION_WORKORDERS_DB"]
|
|
140
|
+
)
|
|
141
|
+
if project is not None:
|
|
142
|
+
project_name = project.name
|
|
143
|
+
workorder_id = project.workorder
|
|
144
|
+
if workorder_id is not None:
|
|
145
|
+
workorder = notion.get_workorder(
|
|
146
|
+
workorders_db_id=config["NOTION_WORKORDERS_DB"],
|
|
147
|
+
workorder_id=workorder_id
|
|
148
|
+
)
|
|
149
|
+
if workorder.is_complete():
|
|
150
|
+
payment_type = workorder.payment_type
|
|
151
|
+
costing_rate = workorder.costing_rate
|
|
152
|
+
hourly_rate = workorder.hourly_rate
|
|
153
|
+
cost = calculate_cost(hours, workorder.hourly_rate)
|
|
154
|
+
else:
|
|
155
|
+
project_name = ""
|
|
156
|
+
workorder_id = ""
|
|
157
|
+
except Exception:
|
|
158
|
+
logger.error(
|
|
159
|
+
f"[Notion] Failed processing project {project_id!r} "
|
|
160
|
+
f"for user {user['name']!r}"
|
|
161
|
+
)
|
|
162
|
+
raise
|
|
139
163
|
else:
|
|
140
164
|
project_name = project_id
|
|
141
165
|
|
|
@@ -185,6 +209,16 @@ def main():
|
|
|
185
209
|
"agresso_description",
|
|
186
210
|
]
|
|
187
211
|
]
|
|
212
|
+
|
|
213
|
+
if args.round:
|
|
214
|
+
totals_df["hours"] = totals_df["hours"].apply(round_up_to_multiple)
|
|
215
|
+
totals_df["cost"] = totals_df.apply(
|
|
216
|
+
lambda row: calculate_cost(row["hours"], row["hourly_rate"])
|
|
217
|
+
if row["hourly_rate"] not in ("", None)
|
|
218
|
+
else row["cost"],
|
|
219
|
+
axis=1,
|
|
220
|
+
)
|
|
221
|
+
|
|
188
222
|
totals_df.to_csv(totals_path, sep="\t", index=False)
|
|
189
223
|
|
|
190
224
|
da_df = totals_df[totals_df["payment_type"] == "DA"]
|
|
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
|
|
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
|
|
File without changes
|