papi-projects 0.3.0__tar.gz → 0.4.0__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.0 → papi_projects-0.4.0}/PKG-INFO +10 -16
- {papi_projects-0.3.0 → papi_projects-0.4.0}/papi/__init__.py +50 -4
- {papi_projects-0.3.0 → papi_projects-0.4.0}/papi/project.py +1 -0
- papi_projects-0.4.0/papi/workorder.py +100 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/papi/wrappers.py +227 -1
- papi_projects-0.4.0/papi_projects.egg-info/PKG-INFO +370 -0
- papi_projects-0.4.0/papi_projects.egg-info/SOURCES.txt +26 -0
- papi_projects-0.4.0/papi_projects.egg-info/dependency_links.txt +1 -0
- papi_projects-0.4.0/papi_projects.egg-info/entry_points.txt +7 -0
- papi_projects-0.4.0/papi_projects.egg-info/requires.txt +5 -0
- papi_projects-0.4.0/papi_projects.egg-info/top_level.txt +2 -0
- papi_projects-0.4.0/pyproject.toml +32 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/create_notion_task.py +1 -1
- papi_projects-0.4.0/scripts/generate_timesheet.py +133 -0
- papi_projects-0.4.0/scripts/test_notion.py +30 -0
- papi_projects-0.4.0/setup.cfg +4 -0
- papi_projects-0.4.0/tests/test_project.py +142 -0
- papi_projects-0.4.0/tests/test_task.py +92 -0
- papi_projects-0.4.0/tests/test_user.py +133 -0
- papi_projects-0.4.0/tests/test_wrappers.py +227 -0
- papi_projects-0.3.0/pyproject.toml +0 -31
- {papi_projects-0.3.0 → papi_projects-0.4.0}/README.md +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/papi/task.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/papi/user.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/__init__.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/collate_toggl_hours.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/create_notion_project.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/create_project.py +0 -0
- {papi_projects-0.3.0 → papi_projects-0.4.0}/scripts/create_toggl_project.py +0 -0
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: papi-projects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
19
14
|
|
|
20
15
|
PAPI is an API for managing projects.
|
|
21
16
|
|
|
@@ -373,4 +368,3 @@ usr.user_id = "JS1"
|
|
|
373
368
|
```
|
|
374
369
|
|
|
375
370
|
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,213 @@ 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
|
+
print(workorder)
|
|
803
|
+
if len(workorders):
|
|
804
|
+
logger.info(f"{len(workorders)} Notion workorders found")
|
|
805
|
+
else:
|
|
806
|
+
logger.warning("No Notion workorders found")
|
|
807
|
+
return workorders
|
|
808
|
+
|
|
809
|
+
|
|
602
810
|
def check_project_exists(self, project: Project, projects_db_id: str) -> Project:
|
|
603
811
|
"""Checks whether a Notion project containing the specified
|
|
604
812
|
project ID already exists. If a name containing that ID is found,
|
|
@@ -766,6 +974,7 @@ class NotionWrapper(Protocol):
|
|
|
766
974
|
project_owner =[owner["name"] for owner in p["properties"]["Owner"]["people"]]
|
|
767
975
|
project_status = p["properties"]["Status"]["status"]["name"]
|
|
768
976
|
project_priority = p["properties"]["Priority"]["select"]["name"]
|
|
977
|
+
print(p["properties"])
|
|
769
978
|
notion_page_id = p["id"]
|
|
770
979
|
if "TEMPLATE" not in project_name:
|
|
771
980
|
user_id = p["properties"]["PI Code"]["rollup"]["array"][0]["rich_text"][0][
|
|
@@ -793,6 +1002,7 @@ class NotionWrapper(Protocol):
|
|
|
793
1002
|
projects_db_id: str,
|
|
794
1003
|
project_id: str | None = None,
|
|
795
1004
|
notion_page_id: str | None = None,
|
|
1005
|
+
workorders_db_id: str | None = None,
|
|
796
1006
|
) -> Project | None:
|
|
797
1007
|
"""
|
|
798
1008
|
Fetches a single project, either by its custom "Project ID" property
|
|
@@ -806,6 +1016,8 @@ class NotionWrapper(Protocol):
|
|
|
806
1016
|
:type project_id: str or None
|
|
807
1017
|
:param notion_page_id: The Notion page ID of the project to retrieve.
|
|
808
1018
|
:type notion_page_id: str or None
|
|
1019
|
+
:param workorders_db_id: ID of the Notion Workorders database to query.
|
|
1020
|
+
:type workorders_db_id: str or None
|
|
809
1021
|
:return: A Project instance if found, otherwise None.
|
|
810
1022
|
:rtype: Project | None
|
|
811
1023
|
"""
|
|
@@ -867,13 +1079,27 @@ class NotionWrapper(Protocol):
|
|
|
867
1079
|
priority = props["Priority"]["select"]["name"]
|
|
868
1080
|
user_id = props["PI Code"]["rollup"]["array"][0]["rich_text"][0]["plain_text"]
|
|
869
1081
|
nid = page["id"]
|
|
1082
|
+
if len(props["Workorder"]["relation"]):
|
|
1083
|
+
workorder_page_id = props["Workorder"]["relation"][0]["id"]
|
|
1084
|
+
else:
|
|
1085
|
+
workorder_page_id = None
|
|
870
1086
|
except (KeyError, IndexError) as e:
|
|
871
1087
|
logger.error(f"Malformed project properties on page {nid!r}: {e!r}")
|
|
872
1088
|
return None
|
|
873
1089
|
|
|
1090
|
+
if workorders_db_id is not None and workorder_page_id is not None:
|
|
1091
|
+
workorder = self.get_workorder(workorders_db_id=workorders_db_id, notion_page_id=workorder_page_id)
|
|
1092
|
+
if workorder is not None:
|
|
1093
|
+
workorder_id = workorder.id
|
|
1094
|
+
else:
|
|
1095
|
+
workorder_id = None
|
|
1096
|
+
else:
|
|
1097
|
+
workorder_id = None
|
|
1098
|
+
|
|
874
1099
|
project = Project(
|
|
875
1100
|
id=pid,
|
|
876
1101
|
name=name,
|
|
1102
|
+
workorder=workorder_id,
|
|
877
1103
|
user_id=user_id,
|
|
878
1104
|
owner=owner,
|
|
879
1105
|
status=status,
|
|
@@ -1119,4 +1345,4 @@ class NotionWrapper(Protocol):
|
|
|
1119
1345
|
created = r.json()
|
|
1120
1346
|
task.notion_page_id = created.get("id")
|
|
1121
1347
|
logger.info(f'Added task "{task.name!r}" with ID {task.notion_page_id} to {project_id}')
|
|
1122
|
-
return task
|
|
1348
|
+
return task
|