papi-projects 0.4.0__tar.gz → 0.4.2__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.0 → papi_projects-0.4.2}/PKG-INFO +2 -1
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/__init__.py +44 -29
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/wrappers.py +1 -3
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/PKG-INFO +2 -1
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/requires.txt +1 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/pyproject.toml +2 -1
- papi_projects-0.4.2/scripts/generate_timesheet.py +154 -0
- papi_projects-0.4.0/scripts/generate_timesheet.py +0 -133
- {papi_projects-0.4.0 → papi_projects-0.4.2}/README.md +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/project.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/task.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/user.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi/workorder.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/SOURCES.txt +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/dependency_links.txt +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/entry_points.txt +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/papi_projects.egg-info/top_level.txt +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/__init__.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/collate_toggl_hours.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/create_notion_project.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/create_notion_task.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/create_project.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/create_toggl_project.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/scripts/test_notion.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/setup.cfg +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/tests/test_project.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/tests/test_task.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/tests/test_user.py +0 -0
- {papi_projects-0.4.0 → papi_projects-0.4.2}/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
|
+
Version: 0.4.2
|
|
4
4
|
Summary: PAPI is an API for managing projects
|
|
5
5
|
Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: httpx>=0.27.2
|
|
|
11
11
|
Requires-Dist: tinydb>=4.8.0
|
|
12
12
|
Requires-Dist: python-dotenv>=1.0.0
|
|
13
13
|
Requires-Dist: pyperclip>=1.9.0
|
|
14
|
+
Requires-Dist: pandas>=2.3.3
|
|
14
15
|
|
|
15
16
|
PAPI is an API for managing projects.
|
|
16
17
|
|
|
@@ -1,41 +1,60 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
|
-
from dotenv import dotenv_values
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
5
|
+
try:
|
|
6
|
+
import tomllib
|
|
7
|
+
except ModuleNotFoundError:
|
|
8
|
+
import tomli as tomllib
|
|
9
|
+
|
|
10
|
+
|
|
6
11
|
def load_config():
|
|
7
|
-
|
|
12
|
+
config_path = os.environ.get("PAPI_CONFIG_FILE")
|
|
13
|
+
if config_path:
|
|
14
|
+
path = Path(config_path).expanduser()
|
|
15
|
+
else:
|
|
16
|
+
path = Path.home() / ".config" / "papi" / "config.toml"
|
|
17
|
+
|
|
18
|
+
if not path.exists():
|
|
19
|
+
raise RuntimeError(f"Config file not found: {path}")
|
|
20
|
+
|
|
21
|
+
with path.open("rb") as f:
|
|
22
|
+
raw = tomllib.load(f)
|
|
23
|
+
|
|
8
24
|
config = {}
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"TOGGL_TRACK_API_KEY",
|
|
15
|
-
"TOGGL_TRACK_PASSWORD",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
26
|
+
toggl = raw.get("toggl", {})
|
|
27
|
+
notion = raw.get("notion", {})
|
|
28
|
+
|
|
29
|
+
mapping = {
|
|
30
|
+
"TOGGL_TRACK_API_KEY": toggl.get("api_key"),
|
|
31
|
+
"TOGGL_TRACK_PASSWORD": toggl.get("password"),
|
|
32
|
+
"TOGGL_TRACK_WORKSPACE": toggl.get("workspace"),
|
|
33
|
+
"NOTION_API_SECRET": notion.get("api_secret"),
|
|
34
|
+
"NOTION_CLIENTS_DB": notion.get("clients_db"),
|
|
35
|
+
"NOTION_PROJECTS_DB": notion.get("projects_db"),
|
|
36
|
+
"NOTION_TASKS_DB": notion.get("tasks_db"),
|
|
37
|
+
"NOTION_TEMPLATE_PAGE_ID": notion.get("template_page_id"),
|
|
38
|
+
"NOTION_WORKORDERS_DB": notion.get("workorders_db"),
|
|
39
|
+
"NOTION_TRAC_COSTS_DB": notion.get("trac_costs_db"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for k, v in mapping.items():
|
|
43
|
+
if v is not None:
|
|
44
|
+
config[k] = v
|
|
45
|
+
|
|
46
|
+
for key in mapping.keys():
|
|
24
47
|
if key in os.environ:
|
|
25
48
|
config[key] = os.environ[key]
|
|
26
49
|
|
|
27
|
-
local_env_path = Path(__file__).parent / ".env"
|
|
28
|
-
if local_env_path.exists():
|
|
29
|
-
local_vals = dotenv_values(local_env_path)
|
|
30
|
-
for k, v in local_vals.items():
|
|
31
|
-
config.setdefault(k, v)
|
|
32
|
-
|
|
33
50
|
return config
|
|
34
51
|
|
|
52
|
+
|
|
35
53
|
config = load_config()
|
|
36
54
|
|
|
37
55
|
TOGGL_TRACK_API_KEY = config["TOGGL_TRACK_API_KEY"]
|
|
38
56
|
TOGGL_TRACK_PASSWORD = config["TOGGL_TRACK_PASSWORD"]
|
|
57
|
+
TOGGL_TRACK_WORKSPACE = config["TOGGL_TRACK_WORKSPACE"]
|
|
39
58
|
NOTION_API_SECRET = config["NOTION_API_SECRET"]
|
|
40
59
|
NOTION_CLIENTS_DB = config["NOTION_CLIENTS_DB"]
|
|
41
60
|
NOTION_PROJECTS_DB = config["NOTION_PROJECTS_DB"]
|
|
@@ -47,6 +66,7 @@ NOTION_TRAC_COSTS_DB = config["NOTION_TRAC_COSTS_DB"]
|
|
|
47
66
|
required = [
|
|
48
67
|
"TOGGL_TRACK_API_KEY",
|
|
49
68
|
"TOGGL_TRACK_PASSWORD",
|
|
69
|
+
"TOGGL_TRACK_WORKSPACE",
|
|
50
70
|
"NOTION_API_SECRET",
|
|
51
71
|
"NOTION_CLIENTS_DB",
|
|
52
72
|
"NOTION_PROJECTS_DB",
|
|
@@ -60,15 +80,14 @@ missing = [k for k in required if k not in config]
|
|
|
60
80
|
if missing:
|
|
61
81
|
raise RuntimeError(f"Missing required config values: {', '.join(missing)}")
|
|
62
82
|
|
|
83
|
+
|
|
63
84
|
def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
|
|
64
85
|
logger = logging.getLogger('papi')
|
|
65
86
|
|
|
66
87
|
if enable_logging:
|
|
67
|
-
# Convert log_level string to logging level
|
|
68
88
|
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
|
69
89
|
logger.setLevel(numeric_level)
|
|
70
|
-
|
|
71
|
-
# Create handler
|
|
90
|
+
|
|
72
91
|
if log_file:
|
|
73
92
|
handler = logging.FileHandler(log_file)
|
|
74
93
|
else:
|
|
@@ -76,18 +95,14 @@ def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str =
|
|
|
76
95
|
|
|
77
96
|
handler.setLevel(numeric_level)
|
|
78
97
|
|
|
79
|
-
# Create formatter
|
|
80
98
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
81
99
|
handler.setFormatter(formatter)
|
|
82
100
|
|
|
83
|
-
# Add handler to logger if not already added
|
|
84
101
|
if not logger.handlers:
|
|
85
102
|
logger.addHandler(handler)
|
|
86
103
|
|
|
87
|
-
# Prevent propagation to root logger
|
|
88
104
|
logger.propagate = False
|
|
89
105
|
else:
|
|
90
|
-
# Set a higher log level to suppress lower-level logs
|
|
91
106
|
logger.setLevel(logging.WARNING)
|
|
92
107
|
|
|
93
108
|
return logger
|
|
@@ -799,7 +799,6 @@ class NotionWrapper(Protocol):
|
|
|
799
799
|
payment_type=payment_type,
|
|
800
800
|
notion_page_id=notion_page_id,
|
|
801
801
|
)
|
|
802
|
-
print(workorder)
|
|
803
802
|
if len(workorders):
|
|
804
803
|
logger.info(f"{len(workorders)} Notion workorders found")
|
|
805
804
|
else:
|
|
@@ -911,7 +910,7 @@ class NotionWrapper(Protocol):
|
|
|
911
910
|
"people": []
|
|
912
911
|
},
|
|
913
912
|
"Status": {
|
|
914
|
-
"status": {"name": "
|
|
913
|
+
"status": {"name": "In Development"}
|
|
915
914
|
},
|
|
916
915
|
"Priority": {
|
|
917
916
|
"select": {"name": "Medium"}
|
|
@@ -974,7 +973,6 @@ class NotionWrapper(Protocol):
|
|
|
974
973
|
project_owner =[owner["name"] for owner in p["properties"]["Owner"]["people"]]
|
|
975
974
|
project_status = p["properties"]["Status"]["status"]["name"]
|
|
976
975
|
project_priority = p["properties"]["Priority"]["select"]["name"]
|
|
977
|
-
print(p["properties"])
|
|
978
976
|
notion_page_id = p["id"]
|
|
979
977
|
if "TEMPLATE" not in project_name:
|
|
980
978
|
user_id = p["properties"]["PI Code"]["rollup"]["array"][0]["rich_text"][0][
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: papi-projects
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: PAPI is an API for managing projects
|
|
5
5
|
Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: httpx>=0.27.2
|
|
|
11
11
|
Requires-Dist: tinydb>=4.8.0
|
|
12
12
|
Requires-Dist: python-dotenv>=1.0.0
|
|
13
13
|
Requires-Dist: pyperclip>=1.9.0
|
|
14
|
+
Requires-Dist: pandas>=2.3.3
|
|
14
15
|
|
|
15
16
|
PAPI is an API for managing projects.
|
|
16
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "papi-projects"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "PAPI is an API for managing projects"
|
|
5
5
|
authors = [{ name = "sandyjmacdonald", email = "sandyjmacdonald@gmail.com" }]
|
|
6
6
|
license = "MIT"
|
|
@@ -13,6 +13,7 @@ dependencies = [
|
|
|
13
13
|
"tinydb>=4.8.0",
|
|
14
14
|
"python-dotenv>=1.0.0",
|
|
15
15
|
"pyperclip>=1.9.0",
|
|
16
|
+
"pandas>=2.3.3",
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
[project.scripts]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pendulum
|
|
3
|
+
import warnings
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from papi.wrappers import TogglTrackWrapper, NotionWrapper
|
|
7
|
+
from papi import config, setup_logger
|
|
8
|
+
from papi.project import get_project_ids, check_project_id
|
|
9
|
+
from decimal import Decimal, ROUND_UP
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tomllib
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
import tomli as tomllib
|
|
16
|
+
|
|
17
|
+
def load_toggl_users_from_toml():
|
|
18
|
+
config_path = os.environ.get("PAPI_CONFIG_FILE")
|
|
19
|
+
if config_path:
|
|
20
|
+
path = Path(config_path).expanduser()
|
|
21
|
+
else:
|
|
22
|
+
path = Path.home() / ".config" / "papi" / "config.toml"
|
|
23
|
+
if not path.exists():
|
|
24
|
+
return []
|
|
25
|
+
with path.open("rb") as f:
|
|
26
|
+
raw = tomllib.load(f)
|
|
27
|
+
toggl = raw.get("toggl", {})
|
|
28
|
+
users = toggl.get("users", {})
|
|
29
|
+
out = []
|
|
30
|
+
if isinstance(users, dict):
|
|
31
|
+
for _, u in users.items():
|
|
32
|
+
if not isinstance(u, dict):
|
|
33
|
+
continue
|
|
34
|
+
name = u.get("name")
|
|
35
|
+
api_key = u.get("api_key")
|
|
36
|
+
if name and api_key:
|
|
37
|
+
out.append({"name": name, "api_key": api_key})
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
def initialise_toggl(toggl_api_key, toggl_workspace):
|
|
41
|
+
toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
|
|
42
|
+
toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
|
|
43
|
+
toggl.set_default_workspace(toggl_workspace)
|
|
44
|
+
toggl.set_me()
|
|
45
|
+
return toggl
|
|
46
|
+
|
|
47
|
+
def calculate_cost(hours: float, hourly_rate: float) -> float:
|
|
48
|
+
cost = Decimal(hours * hourly_rate)
|
|
49
|
+
cost_rounded = cost.quantize(Decimal("0.01"), rounding=ROUND_UP)
|
|
50
|
+
return cost_rounded
|
|
51
|
+
|
|
52
|
+
def get_toggl_hours(start_time, end_time, toggl):
|
|
53
|
+
tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
|
|
54
|
+
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
55
|
+
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
56
|
+
hours_dict = {}
|
|
57
|
+
for i, p in enumerate(hours_per_project):
|
|
58
|
+
project_id = get_project_ids([p[0]])
|
|
59
|
+
if len(project_id):
|
|
60
|
+
hours_dict[project_id[0]] = hours_per_project[i][1]
|
|
61
|
+
else:
|
|
62
|
+
hours_dict[p[0]] = hours_per_project[i][1]
|
|
63
|
+
return hours_dict
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
parser = argparse.ArgumentParser()
|
|
67
|
+
parser.add_argument("-s", "--start", type=str, required=True)
|
|
68
|
+
parser.add_argument("-e", "--end", type=str, default=False)
|
|
69
|
+
parser.add_argument("--disable-logging", action="store_true")
|
|
70
|
+
parser.add_argument("-o", "--output", type=str, required=True)
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
if not args.disable_logging:
|
|
74
|
+
logger = setup_logger(True, "WARNING", None)
|
|
75
|
+
|
|
76
|
+
start_time = pendulum.parse(args.start).to_rfc3339_string()
|
|
77
|
+
end_time = pendulum.now().to_rfc3339_string() if not args.end else pendulum.parse(args.end).to_rfc3339_string()
|
|
78
|
+
|
|
79
|
+
if pendulum.now() < pendulum.parse(args.start).add(months=3):
|
|
80
|
+
toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
|
|
81
|
+
toggl_users = load_toggl_users_from_toml()
|
|
82
|
+
if not toggl_users:
|
|
83
|
+
toggl_users = [{"name": "Self", "api_key": config["TOGGL_TRACK_API_KEY"]}]
|
|
84
|
+
|
|
85
|
+
notion = NotionWrapper(config["NOTION_API_SECRET"])
|
|
86
|
+
output = args.output
|
|
87
|
+
|
|
88
|
+
df = pd.DataFrame(
|
|
89
|
+
columns=[
|
|
90
|
+
"workorder",
|
|
91
|
+
"project_id",
|
|
92
|
+
"payment_type",
|
|
93
|
+
"costing_rate",
|
|
94
|
+
"hourly_rate",
|
|
95
|
+
"hours",
|
|
96
|
+
"cost",
|
|
97
|
+
"person",
|
|
98
|
+
"description",
|
|
99
|
+
"agresso_description",
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for user in toggl_users:
|
|
104
|
+
toggl = initialise_toggl(user["api_key"], toggl_workspace)
|
|
105
|
+
hours_dict = get_toggl_hours(start_time, end_time, toggl)
|
|
106
|
+
|
|
107
|
+
for project_id, hours in hours_dict.items():
|
|
108
|
+
hours = float(hours)
|
|
109
|
+
payment_type = ""
|
|
110
|
+
costing_rate = ""
|
|
111
|
+
hourly_rate = ""
|
|
112
|
+
cost = 0.0
|
|
113
|
+
workorder_id = ""
|
|
114
|
+
if check_project_id(project_id):
|
|
115
|
+
project = notion.get_project(
|
|
116
|
+
config["NOTION_PROJECTS_DB"],
|
|
117
|
+
project_id,
|
|
118
|
+
workorders_db_id=config["NOTION_WORKORDERS_DB"]
|
|
119
|
+
)
|
|
120
|
+
project_name = project.name
|
|
121
|
+
workorder_id = project.workorder
|
|
122
|
+
if workorder_id is not None:
|
|
123
|
+
workorder = notion.get_workorder(
|
|
124
|
+
workorders_db_id=config["NOTION_WORKORDERS_DB"],
|
|
125
|
+
workorder_id=workorder_id
|
|
126
|
+
)
|
|
127
|
+
if workorder.is_complete():
|
|
128
|
+
payment_type = workorder.payment_type
|
|
129
|
+
costing_rate = workorder.costing_rate
|
|
130
|
+
hourly_rate = workorder.hourly_rate
|
|
131
|
+
cost = calculate_cost(hours, workorder.hourly_rate)
|
|
132
|
+
else:
|
|
133
|
+
project_name = project_id
|
|
134
|
+
|
|
135
|
+
df.loc[len(df)] = [
|
|
136
|
+
workorder_id,
|
|
137
|
+
project_id,
|
|
138
|
+
payment_type,
|
|
139
|
+
costing_rate,
|
|
140
|
+
hourly_rate,
|
|
141
|
+
hours,
|
|
142
|
+
cost,
|
|
143
|
+
user["name"],
|
|
144
|
+
project_name,
|
|
145
|
+
f"{project_id}: {project_name}",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
df.to_csv(output, sep="\t", index=False)
|
|
149
|
+
logger.warning(f"{len(df)} journal entries successfully written to {output}")
|
|
150
|
+
else:
|
|
151
|
+
logger.warning("Start time must not be more than 3 months ago!")
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import pendulum
|
|
3
|
-
import warnings
|
|
4
|
-
from papi.wrappers import TogglTrackWrapper, NotionWrapper
|
|
5
|
-
from papi import config
|
|
6
|
-
from papi.project import get_project_ids, check_project_id
|
|
7
|
-
|
|
8
|
-
from decimal import Decimal, ROUND_UP
|
|
9
|
-
|
|
10
|
-
def initialise_toggl(toggl_api_key, toggl_workspace):
|
|
11
|
-
"""Initialises the TogglTrackWrapper.
|
|
12
|
-
|
|
13
|
-
:param toggl_api_key: A user's Toggl Track API key.
|
|
14
|
-
:type toggl_api_key: str
|
|
15
|
-
:param toggl_workspace: The name of the Toggl Track workspace.
|
|
16
|
-
:type toggl_workspace: str
|
|
17
|
-
:return: An initialised TogglTrackWrapper.
|
|
18
|
-
:rtype: TogglTrackWrapper
|
|
19
|
-
"""
|
|
20
|
-
toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
|
|
21
|
-
toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
|
|
22
|
-
toggl.set_default_workspace(toggl_workspace)
|
|
23
|
-
toggl.set_me()
|
|
24
|
-
return toggl
|
|
25
|
-
|
|
26
|
-
def calculate_cost(hours: float, hourly_rate: float) -> float:
|
|
27
|
-
"""Calculates a properly-rounded cost given hours and an hourly rate.
|
|
28
|
-
|
|
29
|
-
:param hours: A number of hours, e.g. 1.5 for one and a half hours.
|
|
30
|
-
:type hours: float
|
|
31
|
-
:param hourly_rate: An hourly rate to charge.
|
|
32
|
-
:type hourly_rate: float
|
|
33
|
-
:return: A properly-rounded (note: rounded up) cost to charge.
|
|
34
|
-
:rtype: float
|
|
35
|
-
"""
|
|
36
|
-
cost = Decimal(hours * hourly_rate)
|
|
37
|
-
cost_rounded = cost.quantize(Decimal("0.01"), rounding=ROUND_UP)
|
|
38
|
-
return cost_rounded
|
|
39
|
-
|
|
40
|
-
def get_toggl_hours(start_time, end_time, toggl):
|
|
41
|
-
"""Get the tracked hours between a start time and end time from an
|
|
42
|
-
initialised TogglTrackWrapper instance.
|
|
43
|
-
|
|
44
|
-
:param start_time: A start time, e.g. 2025-11-01
|
|
45
|
-
:type start_time: str
|
|
46
|
-
:param end_time: An end time, e.g. 2025-11-31
|
|
47
|
-
:type end_time: str
|
|
48
|
-
:param toggl: An initialised TogglTrackWrapper instance.
|
|
49
|
-
:type toggl: TogglTrackWrapper
|
|
50
|
-
:return: A dicitonary of projects and tracked hours.
|
|
51
|
-
:rtype: dict
|
|
52
|
-
"""
|
|
53
|
-
tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
|
|
54
|
-
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
55
|
-
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
56
|
-
hours_dict = {}
|
|
57
|
-
for i, p in enumerate(hours_per_project):
|
|
58
|
-
project_id = get_project_ids([p[0]])
|
|
59
|
-
if len(project_id):
|
|
60
|
-
project_id = project_id[0]
|
|
61
|
-
hours_dict[project_id] = hours_per_project[i][1]
|
|
62
|
-
else:
|
|
63
|
-
hours_dict[p[0]] = hours_per_project[i][1]
|
|
64
|
-
return hours_dict
|
|
65
|
-
|
|
66
|
-
def main():
|
|
67
|
-
"""Main function of generate-timesheet script"""
|
|
68
|
-
|
|
69
|
-
# Set up argparse
|
|
70
|
-
parser = argparse.ArgumentParser()
|
|
71
|
-
parser.add_argument(
|
|
72
|
-
"-s", "--start", type=str, help="start date in YYYY-MM-DD format", required=True
|
|
73
|
-
)
|
|
74
|
-
parser.add_argument(
|
|
75
|
-
"-e", "--end", type=str, help="end date in YYYY-MM-DD format, if none supplied then end date is now", default=False
|
|
76
|
-
)
|
|
77
|
-
parser.add_argument(
|
|
78
|
-
"-o",
|
|
79
|
-
"--output",
|
|
80
|
-
type=str,
|
|
81
|
-
help="output TSV filename, omit to write to stdout",
|
|
82
|
-
default=False,
|
|
83
|
-
)
|
|
84
|
-
args = parser.parse_args()
|
|
85
|
-
|
|
86
|
-
# Set up start/end date
|
|
87
|
-
start_date = args.start
|
|
88
|
-
start_time = pendulum.parse(start_date).to_rfc3339_string()
|
|
89
|
-
end_date = args.end
|
|
90
|
-
if not end_date:
|
|
91
|
-
end_time = pendulum.now().to_rfc3339_string()
|
|
92
|
-
else:
|
|
93
|
-
end_time = pendulum.parse(end_date).to_rfc3339_string()
|
|
94
|
-
|
|
95
|
-
# Check that start time is not more than 3 months ago
|
|
96
|
-
if pendulum.now() < pendulum.parse(start_date).add(months=3):
|
|
97
|
-
toggl_api_key = config["TOGGL_TRACK_API_KEY"]
|
|
98
|
-
toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
|
|
99
|
-
toggl = initialise_toggl(toggl_api_key, toggl_workspace)
|
|
100
|
-
|
|
101
|
-
hours_dict = get_toggl_hours(start_time, end_time, toggl)
|
|
102
|
-
|
|
103
|
-
notion_api_secret = config["NOTION_API_SECRET"]
|
|
104
|
-
notion_clients_db = config["NOTION_CLIENTS_DB"]
|
|
105
|
-
notion_projects_db = config["NOTION_PROJECTS_DB"]
|
|
106
|
-
notion_workorders_db = config["NOTION_WORKORDERS_DB"]
|
|
107
|
-
notion = NotionWrapper(notion_api_secret)
|
|
108
|
-
|
|
109
|
-
project_ids = hours_dict.keys()
|
|
110
|
-
for project_id in project_ids:
|
|
111
|
-
if check_project_id(project_id):
|
|
112
|
-
project = notion.get_project(notion_projects_db, project_id, workorders_db_id=notion_workorders_db)
|
|
113
|
-
workorder_id = project.workorder
|
|
114
|
-
if workorder_id is not None:
|
|
115
|
-
workorder = notion.get_workorder(workorders_db_id=notion_workorders_db, workorder_id=workorder_id)
|
|
116
|
-
if workorder.is_complete():
|
|
117
|
-
hours = float(hours_dict[project_id])
|
|
118
|
-
print(
|
|
119
|
-
f"{workorder.id}\t"
|
|
120
|
-
f"{project_id}\t"
|
|
121
|
-
f"{workorder.payment_type}\t"
|
|
122
|
-
f"{workorder.costing_rate}\t"
|
|
123
|
-
f"{workorder.hourly_rate}\t"
|
|
124
|
-
f"{hours}\t"
|
|
125
|
-
f"{calculate_cost(hours, workorder.hourly_rate)}\t"
|
|
126
|
-
f"{project.name}\t"
|
|
127
|
-
f"{project.id}: {project.name}"
|
|
128
|
-
)
|
|
129
|
-
else:
|
|
130
|
-
warnings.warn("Start time must not be more than 3 months ago!")
|
|
131
|
-
|
|
132
|
-
if __name__ == "__main__":
|
|
133
|
-
main()
|
|
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
|