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.
Files changed (28) hide show
  1. {papi_projects-0.4.3 → papi_projects-0.4.4}/PKG-INFO +1 -1
  2. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/__init__.py +13 -0
  3. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/wrappers.py +6 -3
  4. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/PKG-INFO +1 -1
  5. {papi_projects-0.4.3 → papi_projects-0.4.4}/pyproject.toml +1 -1
  6. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/collate_toggl_hours.py +9 -1
  7. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/generate_timesheet.py +57 -23
  8. {papi_projects-0.4.3 → papi_projects-0.4.4}/README.md +0 -0
  9. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/project.py +0 -0
  10. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/task.py +0 -0
  11. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/user.py +0 -0
  12. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi/workorder.py +0 -0
  13. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/SOURCES.txt +0 -0
  14. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/dependency_links.txt +0 -0
  15. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/entry_points.txt +0 -0
  16. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/requires.txt +0 -0
  17. {papi_projects-0.4.3 → papi_projects-0.4.4}/papi_projects.egg-info/top_level.txt +0 -0
  18. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/__init__.py +0 -0
  19. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_notion_project.py +0 -0
  20. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_notion_task.py +0 -0
  21. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_project.py +0 -0
  22. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/create_toggl_project.py +0 -0
  23. {papi_projects-0.4.3 → papi_projects-0.4.4}/scripts/test_notion.py +0 -0
  24. {papi_projects-0.4.3 → papi_projects-0.4.4}/setup.cfg +0 -0
  25. {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_project.py +0 -0
  26. {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_task.py +0 -0
  27. {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_user.py +0 -0
  28. {papi_projects-0.4.3 → papi_projects-0.4.4}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papi-projects
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: PAPI is an API for managing projects
5
5
  Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
6
6
  License-Expression: MIT
@@ -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["name"] for u in props["Owner"]["people"]]
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(f"Malformed project properties on page {nid!r}: {e!r}")
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papi-projects
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: PAPI is an API for managing projects
5
5
  Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "papi-projects"
3
- version = "0.4.3"
3
+ version = "0.4.4"
4
4
  description = "PAPI is an API for managing projects"
5
5
  authors = [{ name = "sandyjmacdonald", email = "sandyjmacdonald@gmail.com" }]
6
6
  license = "MIT"
@@ -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
- hours_dict = get_toggl_hours(start_time, end_time, toggl)
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
- project = notion.get_project(
119
- config["NOTION_PROJECTS_DB"],
120
- project_id,
121
- workorders_db_id=config["NOTION_WORKORDERS_DB"]
122
- )
123
- if project is not None:
124
- project_name = project.name
125
- workorder_id = project.workorder
126
- if workorder_id is not None:
127
- workorder = notion.get_workorder(
128
- workorders_db_id=config["NOTION_WORKORDERS_DB"],
129
- workorder_id=workorder_id
130
- )
131
- if workorder.is_complete():
132
- payment_type = workorder.payment_type
133
- costing_rate = workorder.costing_rate
134
- hourly_rate = workorder.hourly_rate
135
- cost = calculate_cost(hours, workorder.hourly_rate)
136
- else:
137
- project_name = ""
138
- workorder_id = ""
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