papi-projects 0.3.1__tar.gz → 0.4.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.3.1 → papi_projects-0.4.1}/PKG-INFO +11 -16
- {papi_projects-0.3.1 → papi_projects-0.4.1}/papi/__init__.py +50 -4
- {papi_projects-0.3.1 → papi_projects-0.4.1}/papi/project.py +1 -0
- papi_projects-0.4.1/papi/workorder.py +100 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/papi/wrappers.py +225 -1
- papi_projects-0.4.1/papi_projects.egg-info/PKG-INFO +371 -0
- papi_projects-0.4.1/papi_projects.egg-info/SOURCES.txt +26 -0
- papi_projects-0.4.1/papi_projects.egg-info/dependency_links.txt +1 -0
- papi_projects-0.4.1/papi_projects.egg-info/entry_points.txt +7 -0
- papi_projects-0.4.1/papi_projects.egg-info/requires.txt +6 -0
- papi_projects-0.4.1/papi_projects.egg-info/top_level.txt +2 -0
- papi_projects-0.4.1/pyproject.toml +33 -0
- papi_projects-0.4.1/scripts/generate_timesheet.py +194 -0
- papi_projects-0.4.1/scripts/test_notion.py +30 -0
- papi_projects-0.4.1/setup.cfg +4 -0
- papi_projects-0.4.1/tests/test_project.py +142 -0
- papi_projects-0.4.1/tests/test_task.py +92 -0
- papi_projects-0.4.1/tests/test_user.py +133 -0
- papi_projects-0.4.1/tests/test_wrappers.py +227 -0
- papi_projects-0.3.1/pyproject.toml +0 -31
- {papi_projects-0.3.1 → papi_projects-0.4.1}/README.md +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/papi/task.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/papi/user.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/__init__.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/collate_toggl_hours.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/create_notion_project.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/create_notion_task.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/create_project.py +0 -0
- {papi_projects-0.3.1 → papi_projects-0.4.1}/scripts/create_toggl_project.py +0 -0
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: papi-projects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: PAPI is an API for managing projects
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Requires-Python: >=3.11,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
14
|
-
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
|
15
|
-
Requires-Dist: pyperclip (>=1.9.0,<2.0.0)
|
|
16
|
-
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
17
|
-
Requires-Dist: tinydb (>=4.8.0,<5.0.0)
|
|
5
|
+
Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
18
8
|
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pendulum>=3.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.2
|
|
11
|
+
Requires-Dist: tinydb>=4.8.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Requires-Dist: pyperclip>=1.9.0
|
|
14
|
+
Requires-Dist: pandas>=2.3.3
|
|
19
15
|
|
|
20
16
|
PAPI is an API for managing projects.
|
|
21
17
|
|
|
@@ -373,4 +369,3 @@ usr.user_id = "JS1"
|
|
|
373
369
|
```
|
|
374
370
|
|
|
375
371
|
Setting the `user_id` attribute creates the possibility of a clash in user IDs, therefore the `user` module provides a means to create a basic user database with the TinyDB library. This avoids the possibility of a clash and appends and increments integer numbers to the end of the user ID if a matching one is already in the database.
|
|
376
|
-
|
|
@@ -1,18 +1,64 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
3
|
from dotenv import dotenv_values
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
def load_config():
|
|
7
|
+
user_env_path = Path.home() / ".config" / "papi" / ".env"
|
|
8
|
+
config = {}
|
|
9
|
+
|
|
10
|
+
if user_env_path.exists():
|
|
11
|
+
config.update(dotenv_values(user_env_path))
|
|
12
|
+
|
|
13
|
+
for key in (
|
|
14
|
+
"TOGGL_TRACK_API_KEY",
|
|
15
|
+
"TOGGL_TRACK_PASSWORD",
|
|
16
|
+
"NOTION_API_SECRET",
|
|
17
|
+
"NOTION_CLIENTS_DB",
|
|
18
|
+
"NOTION_PROJECTS_DB",
|
|
19
|
+
"NOTION_TASKS_DB",
|
|
20
|
+
"NOTION_TEMPLATE_PAGE_ID",
|
|
21
|
+
"NOTION_WORKORDERS_DB",
|
|
22
|
+
"NOTION_TRAC_COSTS_DB",
|
|
23
|
+
):
|
|
24
|
+
if key in os.environ:
|
|
25
|
+
config[key] = os.environ[key]
|
|
26
|
+
|
|
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
|
+
return config
|
|
34
|
+
|
|
35
|
+
config = load_config()
|
|
7
36
|
|
|
8
37
|
TOGGL_TRACK_API_KEY = config["TOGGL_TRACK_API_KEY"]
|
|
9
38
|
TOGGL_TRACK_PASSWORD = config["TOGGL_TRACK_PASSWORD"]
|
|
10
|
-
|
|
11
39
|
NOTION_API_SECRET = config["NOTION_API_SECRET"]
|
|
12
40
|
NOTION_CLIENTS_DB = config["NOTION_CLIENTS_DB"]
|
|
13
41
|
NOTION_PROJECTS_DB = config["NOTION_PROJECTS_DB"]
|
|
14
42
|
NOTION_TASKS_DB = config["NOTION_TASKS_DB"]
|
|
15
43
|
NOTION_TEMPLATE_PAGE_ID = config["NOTION_TEMPLATE_PAGE_ID"]
|
|
44
|
+
NOTION_WORKORDERS_DB = config["NOTION_WORKORDERS_DB"]
|
|
45
|
+
NOTION_TRAC_COSTS_DB = config["NOTION_TRAC_COSTS_DB"]
|
|
46
|
+
|
|
47
|
+
required = [
|
|
48
|
+
"TOGGL_TRACK_API_KEY",
|
|
49
|
+
"TOGGL_TRACK_PASSWORD",
|
|
50
|
+
"NOTION_API_SECRET",
|
|
51
|
+
"NOTION_CLIENTS_DB",
|
|
52
|
+
"NOTION_PROJECTS_DB",
|
|
53
|
+
"NOTION_TASKS_DB",
|
|
54
|
+
"NOTION_TEMPLATE_PAGE_ID",
|
|
55
|
+
"NOTION_WORKORDERS_DB",
|
|
56
|
+
"NOTION_TRAC_COSTS_DB",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
missing = [k for k in required if k not in config]
|
|
60
|
+
if missing:
|
|
61
|
+
raise RuntimeError(f"Missing required config values: {', '.join(missing)}")
|
|
16
62
|
|
|
17
63
|
def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
|
|
18
64
|
logger = logging.getLogger('papi')
|
|
@@ -44,4 +90,4 @@ def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str =
|
|
|
44
90
|
# Set a higher log level to suppress lower-level logs
|
|
45
91
|
logger.setLevel(logging.WARNING)
|
|
46
92
|
|
|
47
|
-
return logger
|
|
93
|
+
return logger
|
|
@@ -266,6 +266,7 @@ class Project(Protocol):
|
|
|
266
266
|
f" user_id = {self.user_id},\n"
|
|
267
267
|
f" priority = {self.priority}\n"
|
|
268
268
|
f" status = {self.status}\n"
|
|
269
|
+
f" workorder = {self.workorder}\n"
|
|
269
270
|
f" owner = {self.owner}\n"
|
|
270
271
|
f" notion_page_id = {self.notion_page_id}\n"
|
|
271
272
|
f")"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class Workorder(Protocol):
|
|
9
|
+
"""This class represents a workorder and all of its associated metadata.
|
|
10
|
+
|
|
11
|
+
:param id: The Workorder ID e.g. R123456.
|
|
12
|
+
Often starts with R, M, or A depending on the workorder type.
|
|
13
|
+
:type id: str, required
|
|
14
|
+
:param user_id: The ID of the user to which the workorder belongs,
|
|
15
|
+
defaults to None.
|
|
16
|
+
:type user_id: str, optional
|
|
17
|
+
:param funder: The funding source, e.g. Internal or BBSRC, defaults to None.
|
|
18
|
+
:type funder: str, optional
|
|
19
|
+
:param trac_type: TRAC type, e.g. Internal FEC, defaults to None.
|
|
20
|
+
:type trac_type: str, optional
|
|
21
|
+
:param project_location: e.g. Internal or External, defaults to None.
|
|
22
|
+
:type project_location: str, optional
|
|
23
|
+
:param costing_rate: Charity or FEC, defaults to None.
|
|
24
|
+
:type costing_rate: str, optional
|
|
25
|
+
:param payment_type: DI or DA, defaults to None.
|
|
26
|
+
:type payment_type: str, optional
|
|
27
|
+
:param hourly_rate: The hourly rate to charge, defaults to None.
|
|
28
|
+
:type hourly_rate: float, optional
|
|
29
|
+
:param notion_page_id: The internal Notion page UUID for this workorder.
|
|
30
|
+
:type notion_page_id: str, optional
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
id: str = None,
|
|
36
|
+
user_id: str = None,
|
|
37
|
+
funder: str = None,
|
|
38
|
+
trac_type: str = None,
|
|
39
|
+
project_location: str = None,
|
|
40
|
+
costing_rate: str = None,
|
|
41
|
+
payment_type: str = None,
|
|
42
|
+
hourly_rate: float = None,
|
|
43
|
+
notion_page_id: str = None
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Constructor method"""
|
|
46
|
+
logger.debug("Creating Workorder instance")
|
|
47
|
+
self.id = id
|
|
48
|
+
self.user_id = user_id
|
|
49
|
+
self.funder = funder
|
|
50
|
+
if trac_type is not None:
|
|
51
|
+
self.trac_type = trac_type
|
|
52
|
+
self.project_location, self.costing_rate = trac_type.split()
|
|
53
|
+
else:
|
|
54
|
+
self.project_location = project_location
|
|
55
|
+
self.costing_rate = costing_rate
|
|
56
|
+
self.trac_type = f"{project_location} {costing_rate}"
|
|
57
|
+
self.payment_type = payment_type
|
|
58
|
+
self.hourly_rate = hourly_rate
|
|
59
|
+
self.notion_page_id = notion_page_id
|
|
60
|
+
self.required_fields = [
|
|
61
|
+
"id",
|
|
62
|
+
"project_location",
|
|
63
|
+
"costing_rate",
|
|
64
|
+
"trac_type",
|
|
65
|
+
"payment_type",
|
|
66
|
+
"hourly_rate",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
logger.info(f"Workorder: '{self.id}' instance created")
|
|
70
|
+
|
|
71
|
+
def __str__(self) -> str:
|
|
72
|
+
"""Machine-readable representation of class..
|
|
73
|
+
|
|
74
|
+
:return: basic Workorder() attrs.
|
|
75
|
+
:rtype: str
|
|
76
|
+
"""
|
|
77
|
+
logger.debug("Calling Workorder.__str__ method")
|
|
78
|
+
return (
|
|
79
|
+
f"Workorder(\n"
|
|
80
|
+
f" id = {self.id},\n"
|
|
81
|
+
f" user_id = {self.user_id},\n"
|
|
82
|
+
f" funder = {self.funder},\n"
|
|
83
|
+
f" trac_type = {self.trac_type},\n"
|
|
84
|
+
f" project_location = {self.project_location},\n"
|
|
85
|
+
f" costing_rate = {self.costing_rate},\n"
|
|
86
|
+
f" payment_type = {self.payment_type},\n"
|
|
87
|
+
f" hourly_rate = {self.hourly_rate},\n"
|
|
88
|
+
f" notion_page_id = {self.notion_page_id}\n"
|
|
89
|
+
f")"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def is_complete(self) -> bool:
|
|
93
|
+
"""Checks whether the workorder details are complete.
|
|
94
|
+
|
|
95
|
+
:return: True/False as appropriate
|
|
96
|
+
:rtype: bool
|
|
97
|
+
"""
|
|
98
|
+
logger.debug("Calling Workorder.is_complete method")
|
|
99
|
+
|
|
100
|
+
return all(getattr(self, field) is not None for field in self.required_fields)
|
|
@@ -10,6 +10,7 @@ from pprint import pprint
|
|
|
10
10
|
from papi.project import Project, get_project_ids, decompose_project_name
|
|
11
11
|
from papi.task import Task
|
|
12
12
|
from papi.user import User
|
|
13
|
+
from papi.workorder import Workorder
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -599,6 +600,212 @@ class NotionWrapper(Protocol):
|
|
|
599
600
|
logger.warning("No Notion users found")
|
|
600
601
|
return users
|
|
601
602
|
|
|
603
|
+
def get_trac_costs(
|
|
604
|
+
self,
|
|
605
|
+
trac_costs_db_id: str,
|
|
606
|
+
key: str,
|
|
607
|
+
) -> dict:
|
|
608
|
+
"""
|
|
609
|
+
Fetches TRAC costs give a TRAC type key.
|
|
610
|
+
|
|
611
|
+
You must pass both of `trac_costs_db_id` and `key`.
|
|
612
|
+
|
|
613
|
+
:param trac_costs_db_id: ID of the Notion TRAC Costs database to query.
|
|
614
|
+
:type trac_costs_db_id: str
|
|
615
|
+
:param key: The TRAC type key e.g. Internal Charity.
|
|
616
|
+
:return: A dictionary with the TRAC costs data.
|
|
617
|
+
:rtype: dict
|
|
618
|
+
"""
|
|
619
|
+
logger.debug("Calling NotionWrapper.get_trac_costs method")
|
|
620
|
+
headers = self._headers()
|
|
621
|
+
body = {
|
|
622
|
+
"page_size": 1,
|
|
623
|
+
"filter": {
|
|
624
|
+
"property": "key",
|
|
625
|
+
"title": {"equals": key}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
url = f"https://api.notion.com/v1/databases/{trac_costs_db_id}/query"
|
|
629
|
+
request = lambda: self._post_with_retries(
|
|
630
|
+
url, headers=headers, json=body,
|
|
631
|
+
timeout=httpx.Timeout(5.0, read=15.0),
|
|
632
|
+
)
|
|
633
|
+
try:
|
|
634
|
+
resp = request()
|
|
635
|
+
resp.raise_for_status()
|
|
636
|
+
except httpx.ReadTimeout:
|
|
637
|
+
logger.error("Notion TRAC costs lookup timed out after retries")
|
|
638
|
+
return None
|
|
639
|
+
except httpx.HTTPError as err:
|
|
640
|
+
logger.error(f"Failed to fetch TRAC costs: {err!r}")
|
|
641
|
+
return None
|
|
642
|
+
# Extract the page dict, whether from query or retrieve
|
|
643
|
+
results = resp.json().get("results", [])
|
|
644
|
+
if not results:
|
|
645
|
+
logger.warning(f"No TRAC cost found with key={key!r}")
|
|
646
|
+
return None
|
|
647
|
+
page = results[0]
|
|
648
|
+
props = page.get("properties", {})
|
|
649
|
+
rate_raw = props["Rate"]["number"]
|
|
650
|
+
try:
|
|
651
|
+
rate = float(rate_raw)
|
|
652
|
+
except (TypeError, ValueError):
|
|
653
|
+
raise ValueError(f"TRAC rate must be convertible to float, got: {rate_raw}")
|
|
654
|
+
trac_cost = {key: rate}
|
|
655
|
+
|
|
656
|
+
return trac_cost
|
|
657
|
+
|
|
658
|
+
def get_workorder(
|
|
659
|
+
self,
|
|
660
|
+
workorders_db_id: str,
|
|
661
|
+
workorder_id: str | None = None,
|
|
662
|
+
notion_page_id: str | None = None,
|
|
663
|
+
trac_costs_db_id: str | None = None,
|
|
664
|
+
) -> Workorder | None:
|
|
665
|
+
"""
|
|
666
|
+
Fetches a single workorder, either by its workorder ID
|
|
667
|
+
or by its Notion page ID.
|
|
668
|
+
|
|
669
|
+
You must pass exactly one of `workorder_id` or `notion_page_id`.
|
|
670
|
+
|
|
671
|
+
:param workorders_db_id: ID of the Notion Workorders database to query.
|
|
672
|
+
:type workorders_db_id: str
|
|
673
|
+
:param workorder_id: The value of the "Workorder" property to match.
|
|
674
|
+
:type workorder_id: str or None
|
|
675
|
+
:param notion_page_id: The Notion page ID of the workorder to retrieve.
|
|
676
|
+
:type notion_page_id: str or None
|
|
677
|
+
:param trac_costs_db_id: ID of the Notion TRAC costs database to query.
|
|
678
|
+
:type trac_costs_db_id: str
|
|
679
|
+
:return: A Workorder instance if found, otherwise None.
|
|
680
|
+
:rtype: Workorder | None
|
|
681
|
+
"""
|
|
682
|
+
logger.debug("Calling NotionWrapper.get_workorder method")
|
|
683
|
+
headers = self._headers()
|
|
684
|
+
if bool(workorder_id) == bool(notion_page_id):
|
|
685
|
+
logger.error("Must provide exactly one of workorder_id or notion_page_id")
|
|
686
|
+
return None
|
|
687
|
+
if workorder_id:
|
|
688
|
+
body = {
|
|
689
|
+
"page_size": 1,
|
|
690
|
+
"filter": {
|
|
691
|
+
"property": "Workorder",
|
|
692
|
+
"title": {"equals": workorder_id}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
url = f"https://api.notion.com/v1/databases/{workorders_db_id}/query"
|
|
696
|
+
request = lambda: self._post_with_retries(
|
|
697
|
+
url, headers=headers, json=body,
|
|
698
|
+
timeout=httpx.Timeout(5.0, read=15.0),
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
url = f"https://api.notion.com/v1/pages/{notion_page_id}"
|
|
702
|
+
request = lambda: httpx.get(
|
|
703
|
+
url, headers=headers,
|
|
704
|
+
timeout=httpx.Timeout(5.0, read=15.0),
|
|
705
|
+
)
|
|
706
|
+
try:
|
|
707
|
+
resp = request()
|
|
708
|
+
resp.raise_for_status()
|
|
709
|
+
except httpx.ReadTimeout:
|
|
710
|
+
logger.error("Notion workorder lookup timed out after retries")
|
|
711
|
+
return None
|
|
712
|
+
except httpx.HTTPError as err:
|
|
713
|
+
logger.error(f"Failed to fetch workorder: {err!r}")
|
|
714
|
+
return None
|
|
715
|
+
# Extract the page dict, whether from query or retrieve
|
|
716
|
+
if workorder_id:
|
|
717
|
+
results = resp.json().get("results", [])
|
|
718
|
+
if not results:
|
|
719
|
+
logger.warning(f"No workorder found with Workorder ID={workorder_id!r}")
|
|
720
|
+
return None
|
|
721
|
+
page = results[0]
|
|
722
|
+
else:
|
|
723
|
+
page = resp.json()
|
|
724
|
+
|
|
725
|
+
props = page.get("properties", {})
|
|
726
|
+
try:
|
|
727
|
+
notion_page_id = page["id"]
|
|
728
|
+
if props["Funder"]["select"] is not None:
|
|
729
|
+
funder = props["Funder"]["select"]["name"]
|
|
730
|
+
else:
|
|
731
|
+
funder = None
|
|
732
|
+
if props["Payment Type"]["select"] is not None:
|
|
733
|
+
payment_type = props["Payment Type"]["select"]["name"]
|
|
734
|
+
else:
|
|
735
|
+
payment_type = None
|
|
736
|
+
id = props["Workorder"]["title"][0]["plain_text"]
|
|
737
|
+
if len(props["Project Location"]["rollup"]["array"]):
|
|
738
|
+
project_location = props["Project Location"]["rollup"]["array"][0]["select"]["name"]
|
|
739
|
+
else:
|
|
740
|
+
project_location = None
|
|
741
|
+
if len(props["Costing"]["rollup"]["array"]):
|
|
742
|
+
costing_rate = props["Costing"]["rollup"]["array"][0]["select"]["name"]
|
|
743
|
+
else:
|
|
744
|
+
costing_rate = None
|
|
745
|
+
if len(props["Hourly Rate"]["rollup"]["array"]):
|
|
746
|
+
hourly_rate = props["Hourly Rate"]["rollup"]["array"][0]["number"]
|
|
747
|
+
else:
|
|
748
|
+
hourly_rate = None
|
|
749
|
+
|
|
750
|
+
workorder = Workorder(
|
|
751
|
+
id=id,
|
|
752
|
+
funder=funder,
|
|
753
|
+
project_location=project_location,
|
|
754
|
+
costing_rate=costing_rate,
|
|
755
|
+
payment_type=payment_type,
|
|
756
|
+
hourly_rate=hourly_rate,
|
|
757
|
+
notion_page_id=notion_page_id,
|
|
758
|
+
)
|
|
759
|
+
except (KeyError, IndexError) as e:
|
|
760
|
+
logger.error(f"Malformed workorder properties on page {nid!r}: {e!r}")
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
# logger.info(f"Notion project loaded: {pid!r} → page {nid}")
|
|
764
|
+
return workorder
|
|
765
|
+
|
|
766
|
+
def get_workorders(self, workorders_db_id: str) -> list:
|
|
767
|
+
"""Gets the Notion workorders from the Workorders database.
|
|
768
|
+
|
|
769
|
+
:param workorders_db_id: ID of the Notion Workorders database from which to get
|
|
770
|
+
the workorders.
|
|
771
|
+
:type workorders_db_id: str
|
|
772
|
+
:return: A list of Workorder instances from Notion.
|
|
773
|
+
:rtype: list
|
|
774
|
+
"""
|
|
775
|
+
logger.debug("Calling NotionWrapper.get_workorders method")
|
|
776
|
+
headers = self._headers()
|
|
777
|
+
data = {}
|
|
778
|
+
r = httpx.post(
|
|
779
|
+
f"https://api.notion.com/v1/databases/{workorders_db_id}/query",
|
|
780
|
+
headers=headers,
|
|
781
|
+
json=data,
|
|
782
|
+
)
|
|
783
|
+
r_json = r.json()
|
|
784
|
+
workorders = []
|
|
785
|
+
for w in r_json["results"]:
|
|
786
|
+
notion_page_id = w["id"]
|
|
787
|
+
if w["properties"]["Funder"]["select"] is not None:
|
|
788
|
+
funder = w["properties"]["Funder"]["select"]["name"]
|
|
789
|
+
else:
|
|
790
|
+
funder = None
|
|
791
|
+
if w["properties"]["Payment Type"]["select"] is not None:
|
|
792
|
+
payment_type = w["properties"]["Payment Type"]["select"]["name"]
|
|
793
|
+
else:
|
|
794
|
+
payment_type = None
|
|
795
|
+
id = w["properties"]["Workorder"]["title"][0]["plain_text"]
|
|
796
|
+
workorder = Workorder(
|
|
797
|
+
id=id,
|
|
798
|
+
funder=funder,
|
|
799
|
+
payment_type=payment_type,
|
|
800
|
+
notion_page_id=notion_page_id,
|
|
801
|
+
)
|
|
802
|
+
if len(workorders):
|
|
803
|
+
logger.info(f"{len(workorders)} Notion workorders found")
|
|
804
|
+
else:
|
|
805
|
+
logger.warning("No Notion workorders found")
|
|
806
|
+
return workorders
|
|
807
|
+
|
|
808
|
+
|
|
602
809
|
def check_project_exists(self, project: Project, projects_db_id: str) -> Project:
|
|
603
810
|
"""Checks whether a Notion project containing the specified
|
|
604
811
|
project ID already exists. If a name containing that ID is found,
|
|
@@ -793,6 +1000,7 @@ class NotionWrapper(Protocol):
|
|
|
793
1000
|
projects_db_id: str,
|
|
794
1001
|
project_id: str | None = None,
|
|
795
1002
|
notion_page_id: str | None = None,
|
|
1003
|
+
workorders_db_id: str | None = None,
|
|
796
1004
|
) -> Project | None:
|
|
797
1005
|
"""
|
|
798
1006
|
Fetches a single project, either by its custom "Project ID" property
|
|
@@ -806,6 +1014,8 @@ class NotionWrapper(Protocol):
|
|
|
806
1014
|
:type project_id: str or None
|
|
807
1015
|
:param notion_page_id: The Notion page ID of the project to retrieve.
|
|
808
1016
|
:type notion_page_id: str or None
|
|
1017
|
+
:param workorders_db_id: ID of the Notion Workorders database to query.
|
|
1018
|
+
:type workorders_db_id: str or None
|
|
809
1019
|
:return: A Project instance if found, otherwise None.
|
|
810
1020
|
:rtype: Project | None
|
|
811
1021
|
"""
|
|
@@ -867,13 +1077,27 @@ class NotionWrapper(Protocol):
|
|
|
867
1077
|
priority = props["Priority"]["select"]["name"]
|
|
868
1078
|
user_id = props["PI Code"]["rollup"]["array"][0]["rich_text"][0]["plain_text"]
|
|
869
1079
|
nid = page["id"]
|
|
1080
|
+
if len(props["Workorder"]["relation"]):
|
|
1081
|
+
workorder_page_id = props["Workorder"]["relation"][0]["id"]
|
|
1082
|
+
else:
|
|
1083
|
+
workorder_page_id = None
|
|
870
1084
|
except (KeyError, IndexError) as e:
|
|
871
1085
|
logger.error(f"Malformed project properties on page {nid!r}: {e!r}")
|
|
872
1086
|
return None
|
|
873
1087
|
|
|
1088
|
+
if workorders_db_id is not None and workorder_page_id is not None:
|
|
1089
|
+
workorder = self.get_workorder(workorders_db_id=workorders_db_id, notion_page_id=workorder_page_id)
|
|
1090
|
+
if workorder is not None:
|
|
1091
|
+
workorder_id = workorder.id
|
|
1092
|
+
else:
|
|
1093
|
+
workorder_id = None
|
|
1094
|
+
else:
|
|
1095
|
+
workorder_id = None
|
|
1096
|
+
|
|
874
1097
|
project = Project(
|
|
875
1098
|
id=pid,
|
|
876
1099
|
name=name,
|
|
1100
|
+
workorder=workorder_id,
|
|
877
1101
|
user_id=user_id,
|
|
878
1102
|
owner=owner,
|
|
879
1103
|
status=status,
|
|
@@ -1119,4 +1343,4 @@ class NotionWrapper(Protocol):
|
|
|
1119
1343
|
created = r.json()
|
|
1120
1344
|
task.notion_page_id = created.get("id")
|
|
1121
1345
|
logger.info(f'Added task "{task.name!r}" with ID {task.notion_page_id} to {project_id}')
|
|
1122
|
-
return task
|
|
1346
|
+
return task
|