papi-projects 0.2.3__tar.gz → 0.2.5__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.2.3 → papi_projects-0.2.5}/PKG-INFO +1 -1
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/wrappers.py +170 -169
- {papi_projects-0.2.3 → papi_projects-0.2.5}/pyproject.toml +1 -1
- papi_projects-0.2.5/scripts/collate_toggl_hours.py +72 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/scripts/create_notion_project.py +4 -3
- {papi_projects-0.2.3 → papi_projects-0.2.5}/scripts/create_project.py +3 -2
- papi_projects-0.2.3/scripts/collate_toggl_hours.py +0 -66
- {papi_projects-0.2.3 → papi_projects-0.2.5}/README.md +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/__init__.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/mocks.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/project.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/tests/__init__.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/tests/test_project.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/tests/test_user.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/tests/test_userdb.json +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/tests/test_wrappers.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/papi/user.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/scripts/__init__.py +0 -0
- {papi_projects-0.2.3 → papi_projects-0.2.5}/scripts/create_toggl_project.py +0 -0
|
@@ -436,7 +436,8 @@ class TogglTrackWrapper(Protocol):
|
|
|
436
436
|
self, start_time=None, end_time=pendulum.now().to_rfc3339_string()
|
|
437
437
|
) -> dict:
|
|
438
438
|
"""Gets all of the Toggl Track user's tracked hours for a given
|
|
439
|
-
time period. If no end_time is given, then the current time is used.
|
|
439
|
+
time period. If no end_time is given, then the current time is used.
|
|
440
|
+
Only works with start dates up to a maximum of three months ago.
|
|
440
441
|
|
|
441
442
|
:return: A dictionary containing the Toggl Track projects and hours tracked.
|
|
442
443
|
:rtype: dict
|
|
@@ -449,16 +450,20 @@ class TogglTrackWrapper(Protocol):
|
|
|
449
450
|
params={"start_date": start_time, "end_date": end_time},
|
|
450
451
|
)
|
|
451
452
|
times_json = r.json()
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
453
|
+
if isinstance(times_json, list):
|
|
454
|
+
times = {}
|
|
455
|
+
for t in times_json:
|
|
456
|
+
pid = t["pid"]
|
|
457
|
+
seconds = t["duration"]
|
|
458
|
+
hours = seconds / 60 / 60
|
|
459
|
+
if pid not in times:
|
|
460
|
+
times[pid] = hours
|
|
461
|
+
else:
|
|
462
|
+
times[pid] += hours
|
|
463
|
+
return times
|
|
464
|
+
else:
|
|
465
|
+
warnings.warn(times_json)
|
|
466
|
+
return None
|
|
462
467
|
else:
|
|
463
468
|
warnings.warn("Please provide a valid start date/time in RFC3339 format!")
|
|
464
469
|
return None
|
|
@@ -618,6 +623,89 @@ class NotionWrapper(Protocol):
|
|
|
618
623
|
self.api_secret = api_secret
|
|
619
624
|
logger.info("NotionWrapper instance created")
|
|
620
625
|
|
|
626
|
+
def _headers(self) -> dict:
|
|
627
|
+
return {
|
|
628
|
+
"Authorization": f"Bearer {self.api_secret}",
|
|
629
|
+
"Notion-Version": "2022-06-28",
|
|
630
|
+
"Content-Type": "application/json",
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
def _fetch_template_page(self, template_page_id: str) -> dict:
|
|
634
|
+
"""Retrieve the template page’s properties."""
|
|
635
|
+
r = httpx.get(
|
|
636
|
+
f"https://api.notion.com/v1/pages/{template_page_id}",
|
|
637
|
+
headers=self._headers()
|
|
638
|
+
)
|
|
639
|
+
r.raise_for_status()
|
|
640
|
+
return r.json()
|
|
641
|
+
|
|
642
|
+
def _fetch_template_blocks(self, template_page_id: str) -> list[dict]:
|
|
643
|
+
"""Page through and collect all child blocks of the template."""
|
|
644
|
+
blocks: list[dict] = []
|
|
645
|
+
url = f"https://api.notion.com/v1/blocks/{template_page_id}/children"
|
|
646
|
+
params = {"page_size": 100}
|
|
647
|
+
while True:
|
|
648
|
+
r = httpx.get(url, headers=self._headers(), params=params)
|
|
649
|
+
r.raise_for_status()
|
|
650
|
+
js = r.json()
|
|
651
|
+
blocks.extend(js["results"])
|
|
652
|
+
if not js.get("has_more"):
|
|
653
|
+
break
|
|
654
|
+
params = {"start_cursor": js["next_cursor"], "page_size": 100}
|
|
655
|
+
return blocks
|
|
656
|
+
|
|
657
|
+
def _clean_properties(self, raw: dict) -> dict:
|
|
658
|
+
"""Strip out IDs, types, rollup/formula wrappers, leaving only the API-legal values."""
|
|
659
|
+
clean = {}
|
|
660
|
+
for name, meta in raw.items():
|
|
661
|
+
t = meta.get("type")
|
|
662
|
+
if t == "title":
|
|
663
|
+
clean[name] = meta["title"]
|
|
664
|
+
elif t == "rich_text":
|
|
665
|
+
clean[name] = meta["rich_text"]
|
|
666
|
+
elif t == "select":
|
|
667
|
+
sel = meta.get("select")
|
|
668
|
+
if sel and sel.get("name"):
|
|
669
|
+
clean[name] = {"name": sel["name"]}
|
|
670
|
+
elif t == "multi_select":
|
|
671
|
+
clean[name] = [
|
|
672
|
+
{"name": m["name"]} for m in meta.get("multi_select", []) if m.get("name")
|
|
673
|
+
]
|
|
674
|
+
elif t == "status":
|
|
675
|
+
st = meta.get("status")
|
|
676
|
+
if st and st.get("name"):
|
|
677
|
+
clean[name] = {"name": st["name"]}
|
|
678
|
+
elif t == "date":
|
|
679
|
+
d = meta.get("date")
|
|
680
|
+
if d and d.get("start"):
|
|
681
|
+
# include end only if present
|
|
682
|
+
clean[name] = {"start": d["start"], **({"end": d["end"]} if d.get("end") else {})}
|
|
683
|
+
elif t in ("people", "relation"):
|
|
684
|
+
# both use a list of {id:…}
|
|
685
|
+
key = t
|
|
686
|
+
clean[name] = [ {"id": x["id"]} for x in meta.get(key, []) if x.get("id") ]
|
|
687
|
+
# skip formula, rollup, files, etc.
|
|
688
|
+
return clean
|
|
689
|
+
|
|
690
|
+
def _clean_blocks(self, raw_blocks: list[dict]) -> list[dict]:
|
|
691
|
+
ALLOWED = {
|
|
692
|
+
"paragraph","heading_1","heading_2","heading_3",
|
|
693
|
+
"to_do","bulleted_list_item","numbered_list_item",
|
|
694
|
+
"toggle","quote","callout",
|
|
695
|
+
"image","video","file","embed","bookmark",
|
|
696
|
+
}
|
|
697
|
+
clean = []
|
|
698
|
+
for b in raw_blocks:
|
|
699
|
+
t = b["type"]
|
|
700
|
+
if t not in ALLOWED:
|
|
701
|
+
continue
|
|
702
|
+
minimal = { "type": t, t: b[t] }
|
|
703
|
+
if b.get("has_children"):
|
|
704
|
+
child_raw = self._fetch_template_blocks(b["id"])
|
|
705
|
+
minimal["children"] = self._clean_blocks(child_raw)
|
|
706
|
+
clean.append(minimal)
|
|
707
|
+
return clean
|
|
708
|
+
|
|
621
709
|
def create_user(self, user: User, clients_db_id: str) -> str:
|
|
622
710
|
"""Creates a Notion client (user) from a `User` instance.
|
|
623
711
|
|
|
@@ -634,10 +722,7 @@ class NotionWrapper(Protocol):
|
|
|
634
722
|
user_name = user.user_name
|
|
635
723
|
user_id = user.user_id
|
|
636
724
|
email = user.email
|
|
637
|
-
headers =
|
|
638
|
-
"Authorization": f"Bearer {self.api_secret}",
|
|
639
|
-
"Notion-Version": "2022-06-28",
|
|
640
|
-
}
|
|
725
|
+
headers = self._headers()
|
|
641
726
|
data = {
|
|
642
727
|
"parent": {"database_id": clients_db_id},
|
|
643
728
|
"properties": {
|
|
@@ -707,10 +792,7 @@ class NotionWrapper(Protocol):
|
|
|
707
792
|
"""
|
|
708
793
|
logger.debug("Calling NotionWrapper.get_user_page_id method")
|
|
709
794
|
user_id = user.user_id
|
|
710
|
-
headers =
|
|
711
|
-
"Authorization": f"Bearer {self.api_secret}",
|
|
712
|
-
"Notion-Version": "2022-06-28",
|
|
713
|
-
}
|
|
795
|
+
headers = self._headers()
|
|
714
796
|
data = {
|
|
715
797
|
"filter": {
|
|
716
798
|
"property": "Code",
|
|
@@ -744,10 +826,7 @@ class NotionWrapper(Protocol):
|
|
|
744
826
|
:rtype: list
|
|
745
827
|
"""
|
|
746
828
|
logger.debug("Calling NotionWrapper.get_users method")
|
|
747
|
-
headers =
|
|
748
|
-
"Authorization": f"Bearer {self.api_secret}",
|
|
749
|
-
"Notion-Version": "2022-06-28",
|
|
750
|
-
}
|
|
829
|
+
headers = self._headers()
|
|
751
830
|
data = {}
|
|
752
831
|
r = httpx.post(
|
|
753
832
|
f"https://api.notion.com/v1/databases/{clients_db_id}/query",
|
|
@@ -768,155 +847,80 @@ class NotionWrapper(Protocol):
|
|
|
768
847
|
logger.warning("No Notion users found")
|
|
769
848
|
return users
|
|
770
849
|
|
|
771
|
-
def create_project(
|
|
772
|
-
|
|
773
|
-
|
|
850
|
+
def create_project(
|
|
851
|
+
self,
|
|
852
|
+
project: Project,
|
|
853
|
+
user: User,
|
|
854
|
+
projects_db_id: str,
|
|
855
|
+
user_page_id: str | None = None,
|
|
856
|
+
template_page_id: str | None = None,
|
|
857
|
+
) -> str:
|
|
858
|
+
"""
|
|
859
|
+
Creates a new project on Notion, optionally cloning from a database template.
|
|
860
|
+
|
|
774
861
|
:param project: A Project instance.
|
|
775
|
-
:type project: Project
|
|
776
862
|
:param user: A User instance.
|
|
777
|
-
:
|
|
778
|
-
:param
|
|
779
|
-
:
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
:type user_page_id: str
|
|
783
|
-
:return: The page ID of the project created in Notion.
|
|
784
|
-
:rtype: str
|
|
863
|
+
:param projects_db_id: ID of the Notion projects database.
|
|
864
|
+
:param user_page_id: ID of the user in Notion (for the PI relation).
|
|
865
|
+
:param template_page_id: If given, we’ll fetch that page’s properties & children
|
|
866
|
+
and use them as the base for this new record.
|
|
867
|
+
:return: The page ID of the created project.
|
|
785
868
|
"""
|
|
786
869
|
logger.debug("Calling NotionWrapper.create_project method")
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if
|
|
791
|
-
|
|
870
|
+
headers = self._headers()
|
|
871
|
+
|
|
872
|
+
# 1) Fetch & clean template if requested
|
|
873
|
+
if template_page_id:
|
|
874
|
+
tpl_raw = self._fetch_template_page(template_page_id)
|
|
875
|
+
base_props = self._clean_properties(tpl_raw["properties"])
|
|
876
|
+
raw_blocks = self._fetch_template_blocks(template_page_id)
|
|
877
|
+
base_blocks = self._clean_blocks(raw_blocks)
|
|
792
878
|
else:
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
879
|
+
base_props, base_blocks = {}, []
|
|
880
|
+
|
|
881
|
+
# 2) Build project-specific overrides in the exact shape Notion expects
|
|
882
|
+
overrides = {
|
|
883
|
+
"Project ID": [
|
|
884
|
+
{
|
|
885
|
+
"type": "text",
|
|
886
|
+
"text": {"content": project.id}
|
|
887
|
+
}
|
|
888
|
+
],
|
|
889
|
+
"Description": [
|
|
890
|
+
{
|
|
891
|
+
"type": "text",
|
|
892
|
+
"text": {"content": project.name}
|
|
893
|
+
}
|
|
894
|
+
],
|
|
895
|
+
"PI": [{"id": user_page_id}] if user_page_id else [],
|
|
896
|
+
# you can add more dynamic fields here (dates, selects, etc.)
|
|
797
897
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
"rich_text": [
|
|
807
|
-
{
|
|
808
|
-
"type": "text",
|
|
809
|
-
"text": {
|
|
810
|
-
"content": project_name,
|
|
811
|
-
"link": None,
|
|
812
|
-
},
|
|
813
|
-
"annotations": {
|
|
814
|
-
"bold": False,
|
|
815
|
-
"italic": False,
|
|
816
|
-
"strikethrough": False,
|
|
817
|
-
"underline": False,
|
|
818
|
-
"code": False,
|
|
819
|
-
"color": "default",
|
|
820
|
-
},
|
|
821
|
-
"plain_text": project_name,
|
|
822
|
-
"href": None,
|
|
823
|
-
}
|
|
824
|
-
],
|
|
825
|
-
},
|
|
826
|
-
"Hourly Rate": {
|
|
827
|
-
"type": "number",
|
|
828
|
-
"number": 0.0, # Set an appropriate number
|
|
829
|
-
},
|
|
830
|
-
"Slug": {
|
|
831
|
-
"type": "rich_text",
|
|
832
|
-
"rich_text": [
|
|
833
|
-
{
|
|
834
|
-
"type": "text",
|
|
835
|
-
"text": {"content": project_name, "link": None},
|
|
836
|
-
"annotations": {
|
|
837
|
-
"bold": False,
|
|
838
|
-
"italic": False,
|
|
839
|
-
"strikethrough": False,
|
|
840
|
-
"underline": False,
|
|
841
|
-
"code": False,
|
|
842
|
-
"color": "default",
|
|
843
|
-
},
|
|
844
|
-
"plain_text": project_name,
|
|
845
|
-
"href": None,
|
|
846
|
-
}
|
|
847
|
-
],
|
|
848
|
-
},
|
|
849
|
-
"Type": {
|
|
850
|
-
"type": "select", # Assuming Type is a select property
|
|
851
|
-
"select": None,
|
|
852
|
-
},
|
|
853
|
-
"PI Code": {
|
|
854
|
-
"type": "rich_text",
|
|
855
|
-
"rich_text": [
|
|
856
|
-
{
|
|
857
|
-
"type": "text",
|
|
858
|
-
"text": {"content": user_id, "link": None},
|
|
859
|
-
"annotations": {
|
|
860
|
-
"bold": False,
|
|
861
|
-
"italic": False,
|
|
862
|
-
"strikethrough": False,
|
|
863
|
-
"underline": False,
|
|
864
|
-
"code": False,
|
|
865
|
-
"color": "default",
|
|
866
|
-
},
|
|
867
|
-
"plain_text": user_id,
|
|
868
|
-
"href": None,
|
|
869
|
-
}
|
|
870
|
-
],
|
|
871
|
-
},
|
|
872
|
-
"Priority": {
|
|
873
|
-
"type": "select",
|
|
874
|
-
"select": None,
|
|
875
|
-
},
|
|
876
|
-
"Tags": {
|
|
877
|
-
"type": "multi_select",
|
|
878
|
-
"multi_select": [],
|
|
879
|
-
},
|
|
880
|
-
"PI": {
|
|
881
|
-
"type": "relation",
|
|
882
|
-
"relation": pi_relation,
|
|
883
|
-
"has_more": False,
|
|
884
|
-
},
|
|
885
|
-
"Start Date": {
|
|
886
|
-
"type": "date",
|
|
887
|
-
# "date": {"start": "2024-09-12", "end": None, "time_zone": None},
|
|
888
|
-
"date": None,
|
|
889
|
-
},
|
|
890
|
-
"Owner": {
|
|
891
|
-
"type": "people",
|
|
892
|
-
"people": [],
|
|
893
|
-
},
|
|
894
|
-
"Project ID": {
|
|
895
|
-
"type": "title",
|
|
896
|
-
"title": [
|
|
897
|
-
{
|
|
898
|
-
"type": "text",
|
|
899
|
-
"text": {"content": project_id, "link": None},
|
|
900
|
-
"annotations": {
|
|
901
|
-
"bold": False,
|
|
902
|
-
"italic": False,
|
|
903
|
-
"strikethrough": False,
|
|
904
|
-
"underline": False,
|
|
905
|
-
"code": False,
|
|
906
|
-
"color": "default",
|
|
907
|
-
},
|
|
908
|
-
"plain_text": project_id,
|
|
909
|
-
"href": None,
|
|
910
|
-
}
|
|
911
|
-
],
|
|
912
|
-
},
|
|
913
|
-
},
|
|
898
|
+
|
|
899
|
+
# 3) Merge so overrides win
|
|
900
|
+
merged_props = {**base_props, **overrides}
|
|
901
|
+
|
|
902
|
+
# 4) Build payload
|
|
903
|
+
payload: dict = {
|
|
904
|
+
"parent": {"database_id": projects_db_id},
|
|
905
|
+
"properties": merged_props,
|
|
914
906
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
907
|
+
if base_blocks:
|
|
908
|
+
payload["children"] = base_blocks
|
|
909
|
+
|
|
910
|
+
# 5) Send and handle errors
|
|
911
|
+
r = httpx.post("https://api.notion.com/v1/pages", headers=headers, json=payload)
|
|
912
|
+
try:
|
|
913
|
+
r.raise_for_status()
|
|
914
|
+
except httpx.HTTPStatusError:
|
|
915
|
+
print("→ Request JSON:")
|
|
916
|
+
print(json.dumps(payload, indent=2))
|
|
917
|
+
print("→ Notion error response:")
|
|
918
|
+
print(r.json())
|
|
919
|
+
raise
|
|
920
|
+
|
|
921
|
+
new_page_id = r.json()["id"]
|
|
922
|
+
logger.info(f"Notion project cloned from template as page ID {new_page_id}")
|
|
923
|
+
return new_page_id
|
|
920
924
|
|
|
921
925
|
def get_projects(self, projects_db_id: str) -> list:
|
|
922
926
|
"""Gets the Notion projects from the Clients database.
|
|
@@ -928,10 +932,7 @@ class NotionWrapper(Protocol):
|
|
|
928
932
|
:rtype: list
|
|
929
933
|
"""
|
|
930
934
|
logger.debug("Calling NotionWrapper.get_projects method")
|
|
931
|
-
headers =
|
|
932
|
-
"Authorization": f"Bearer {self.api_secret}",
|
|
933
|
-
"Notion-Version": "2022-06-28",
|
|
934
|
-
}
|
|
935
|
+
headers = self._headers()
|
|
935
936
|
data = {}
|
|
936
937
|
r = httpx.post(
|
|
937
938
|
f"https://api.notion.com/v1/databases/{projects_db_id}/query",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pendulum
|
|
3
|
+
import warnings
|
|
4
|
+
from papi.wrappers import TogglTrackWrapper
|
|
5
|
+
from papi import config
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
"""Main function of collate-toggl-hours script"""
|
|
9
|
+
|
|
10
|
+
# Set up argparse
|
|
11
|
+
parser = argparse.ArgumentParser()
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"-s", "--start", type=str, help="start date in YYYY-MM-DD format", required=True
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"-e", "--end", type=str, help="end date in YYYY-MM-DD format, if none supplied then end date is now", default=False
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-o",
|
|
20
|
+
"--output",
|
|
21
|
+
type=str,
|
|
22
|
+
help="output TSV filename, omit to write to stdout",
|
|
23
|
+
default=False,
|
|
24
|
+
)
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
# Set up start/end date
|
|
28
|
+
start_date = args.start
|
|
29
|
+
start_time = pendulum.parse(start_date).to_rfc3339_string()
|
|
30
|
+
end_date = args.end
|
|
31
|
+
if not end_date:
|
|
32
|
+
end_time = pendulum.now().to_rfc3339_string()
|
|
33
|
+
else:
|
|
34
|
+
end_time = pendulum.parse(end_date).to_rfc3339_string()
|
|
35
|
+
|
|
36
|
+
# Check that start time is not more than 3 months ago
|
|
37
|
+
if pendulum.now() < pendulum.parse(start_date).add(months=3):
|
|
38
|
+
# Set up Toggl Track API wrapper
|
|
39
|
+
#
|
|
40
|
+
# NOTE: you must have added Toggl Track API key and password, with
|
|
41
|
+
# variable names below to .env file in this directory
|
|
42
|
+
toggl_api_key = config["TOGGL_TRACK_API_KEY"]
|
|
43
|
+
toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
|
|
44
|
+
toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
|
|
45
|
+
|
|
46
|
+
# Tell wrapper which workspace to set as default
|
|
47
|
+
toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
|
|
48
|
+
toggl.set_default_workspace(toggl_workspace)
|
|
49
|
+
toggl.set_me()
|
|
50
|
+
|
|
51
|
+
# Get tracked hours and tracked project IDs/names
|
|
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
|
+
|
|
57
|
+
output = args.output
|
|
58
|
+
|
|
59
|
+
if output:
|
|
60
|
+
# If output filename provided, write to file
|
|
61
|
+
with open(output, "w") as out:
|
|
62
|
+
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
63
|
+
out.write(f"{h[0]}\t{h[1]}\n")
|
|
64
|
+
else:
|
|
65
|
+
# Otherwise, print out project names and tracked hours to stdout
|
|
66
|
+
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
67
|
+
print(f"{h[0]}\t{h[1]}")
|
|
68
|
+
else:
|
|
69
|
+
warnings.warn("Start time must not be more than 3 months ago!")
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -39,12 +39,13 @@ def main():
|
|
|
39
39
|
#
|
|
40
40
|
# NOTE: you must have added your Notion API credentials and user and projects
|
|
41
41
|
# database IDs with variable names below to .env file in this directory
|
|
42
|
-
if "NOTION_API_SECRET"not in config and "NOTION_CLIENTS_DB" not in config and "NOTION_PROJECTS_DB" not in config:
|
|
42
|
+
if "NOTION_API_SECRET" not in config and "NOTION_CLIENTS_DB" not in config and "NOTION_PROJECTS_DB" not in config:
|
|
43
43
|
logger.warning("Please create a .env file with NOTION_API_SECRET and NOTION_CLIENTS_DB and NOTION_PROJECTS_DB set!")
|
|
44
44
|
return
|
|
45
45
|
notion_api_secret = config["NOTION_API_SECRET"]
|
|
46
46
|
notion_clients_db = config["NOTION_CLIENTS_DB"]
|
|
47
47
|
notion_projects_db = config["NOTION_PROJECTS_DB"]
|
|
48
|
+
notion_template_page_id = config["NOTION_TEMPLATE_PAGE_ID"]
|
|
48
49
|
notion = NotionWrapper(notion_api_secret)
|
|
49
50
|
|
|
50
51
|
user_id = args.user_id
|
|
@@ -75,9 +76,9 @@ def main():
|
|
|
75
76
|
# Create project on Notion
|
|
76
77
|
user_page_id = notion.get_user_page_id(notion_clients_db, user)
|
|
77
78
|
if user_page_id is not None:
|
|
78
|
-
notion_proj_id = notion.create_project(project, user, notion_projects_db, user_page_id=user_page_id)
|
|
79
|
+
notion_proj_id = notion.create_project(project, user, notion_projects_db, user_page_id=user_page_id, template_page_id=notion_template_page_id)
|
|
79
80
|
else:
|
|
80
|
-
notion_proj_id = notion.create_project(project, user, notion_projects_db)
|
|
81
|
+
notion_proj_id = notion.create_project(project, user, notion_projects_db, template_page_id=notion_template_page_id)
|
|
81
82
|
|
|
82
83
|
pyperclip.copy(project.id)
|
|
83
84
|
|
|
@@ -159,14 +159,15 @@ def main():
|
|
|
159
159
|
notion_api_secret = config["NOTION_API_SECRET"]
|
|
160
160
|
notion_clients_db = config["NOTION_CLIENTS_DB"]
|
|
161
161
|
notion_projects_db = config["NOTION_PROJECTS_DB"]
|
|
162
|
+
notion_template_page_id = config["NOTION_TEMPLATE_PAGE_ID"]
|
|
162
163
|
notion = NotionWrapper(notion_api_secret)
|
|
163
164
|
|
|
164
165
|
# Create project on Notion
|
|
165
166
|
user_page_id = notion.get_user_page_id(notion_clients_db, user)
|
|
166
167
|
if user_page_id is not None:
|
|
167
|
-
notion_proj_id = notion.create_project(project, user, notion_projects_db, user_page_id=user_page_id)
|
|
168
|
+
notion_proj_id = notion.create_project(project, user, notion_projects_db, user_page_id=user_page_id, template_page_id=notion_template_page_id)
|
|
168
169
|
else:
|
|
169
|
-
notion_proj_id = notion.create_project(project, user, notion_projects_db)
|
|
170
|
+
notion_proj_id = notion.create_project(project, user, notion_projects_db, template_page_id=notion_template_page_id)
|
|
170
171
|
|
|
171
172
|
pyperclip.copy(project.id)
|
|
172
173
|
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import pendulum
|
|
3
|
-
from papi.wrappers import TogglTrackWrapper
|
|
4
|
-
from papi import config
|
|
5
|
-
|
|
6
|
-
def main():
|
|
7
|
-
"""Main function of collate-toggl-hours script"""
|
|
8
|
-
|
|
9
|
-
# Set up argparse
|
|
10
|
-
parser = argparse.ArgumentParser()
|
|
11
|
-
parser.add_argument(
|
|
12
|
-
"-s", "--start", type=str, help="start date in YYYY-MM-DD format", required=True
|
|
13
|
-
)
|
|
14
|
-
parser.add_argument(
|
|
15
|
-
"-e", "--end", type=str, help="end date in YYYY-MM-DD format, if none supplied then end date is now", default=False
|
|
16
|
-
)
|
|
17
|
-
parser.add_argument(
|
|
18
|
-
"-o",
|
|
19
|
-
"--output",
|
|
20
|
-
type=str,
|
|
21
|
-
help="output TSV filename, omit to write to stdout",
|
|
22
|
-
default=False,
|
|
23
|
-
)
|
|
24
|
-
args = parser.parse_args()
|
|
25
|
-
|
|
26
|
-
# Set up start/end date
|
|
27
|
-
start_date = args.start
|
|
28
|
-
start_time = pendulum.parse(start_date).to_rfc3339_string()
|
|
29
|
-
end_date = args.end
|
|
30
|
-
if not end_date:
|
|
31
|
-
end_time = pendulum.now().to_rfc3339_string()
|
|
32
|
-
else:
|
|
33
|
-
end_time = pendulum.parse(end_date).to_rfc3339_string()
|
|
34
|
-
|
|
35
|
-
# Set up Toggl Track API wrapper
|
|
36
|
-
#
|
|
37
|
-
# NOTE: you must have added Toggl Track API key and password, with
|
|
38
|
-
# variable names below to .env file in this directory
|
|
39
|
-
toggl_api_key = config["TOGGL_TRACK_API_KEY"]
|
|
40
|
-
toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
|
|
41
|
-
toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
|
|
42
|
-
|
|
43
|
-
# Tell wrapper which workspace to set as default
|
|
44
|
-
toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
|
|
45
|
-
toggl.set_default_workspace(toggl_workspace)
|
|
46
|
-
toggl.set_me()
|
|
47
|
-
|
|
48
|
-
# Get tracked hours and tracked project IDs/names
|
|
49
|
-
tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
|
|
50
|
-
projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
|
|
51
|
-
hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
|
|
52
|
-
|
|
53
|
-
output = args.output
|
|
54
|
-
|
|
55
|
-
if output:
|
|
56
|
-
# If output filename provided, write to file
|
|
57
|
-
with open(output, "w") as out:
|
|
58
|
-
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
59
|
-
out.write(f"{h[0]}\t{h[1]}\n")
|
|
60
|
-
else:
|
|
61
|
-
# Otherwise, print out project names and tracked hours to stdout
|
|
62
|
-
for h in sorted(hours_per_project, key=lambda x:x[1], reverse=True):
|
|
63
|
-
print(f"{h[0]}\t{h[1]}")
|
|
64
|
-
|
|
65
|
-
if __name__ == "__main__":
|
|
66
|
-
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
|