diff --git a/.DS_Store b/.DS_Store index 001c6ec..f299ed6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index f392133..dd74729 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ cython_debug/ things2reclaim/config/.toggl.toml things2reclaim/config/.things2reclaim.toml things2reclaim/data/tasks.db +api/ReclaimAI/collection.bru diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..c0e95ff --- /dev/null +++ b/.pylintrc @@ -0,0 +1,6 @@ +[MASTER] +disable= + C0114, #missing-module-docstring + C0115, #missing-class-docstring + C0116, #missing-function-docstring +init-hook='import sys; sys.path.append(".")' diff --git a/README.md b/README.md index 4d2ee40..0c92489 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ +# things2reclaim + Tool to sync things3 with reclaim diff --git a/api/ReclaimAI/Get Events.bru b/api/ReclaimAI/Get Events.bru new file mode 100644 index 0000000..7fb8aad --- /dev/null +++ b/api/ReclaimAI/Get Events.bru @@ -0,0 +1,18 @@ +meta { + name: Get Events + type: http + seq: 3 +} + +get { + url: https://api.app.reclaim.ai/api/events?start=2024-06-13&end=2024-06-14&sourceDetails=true&allConnected=true + body: none + auth: inherit +} + +params:query { + start: 2024-06-13 + end: 2024-06-14 + sourceDetails: true + allConnected: true +} diff --git a/api/ReclaimAI/Get Tasks.bru b/api/ReclaimAI/Get Tasks.bru new file mode 100644 index 0000000..9ee9730 --- /dev/null +++ b/api/ReclaimAI/Get Tasks.bru @@ -0,0 +1,11 @@ +meta { + name: Get Tasks + type: http + seq: 2 +} + +get { + url: https://api.app.reclaim.ai/api/tasks + body: none + auth: inherit +} diff --git a/api/ReclaimAI/bruno.json b/api/ReclaimAI/bruno.json new file mode 100644 index 0000000..ffb1b73 --- /dev/null +++ b/api/ReclaimAI/bruno.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "name": "ReclaimAI", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ], + "presets": { + "requestType": "http", + "requestUrl": "" + } +} \ No newline at end of file diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..31530a8 --- /dev/null +++ b/environment.yml @@ -0,0 +1,41 @@ +name: things-automation +channels: + - conda-forge + - defaults +dependencies: + - astroid=3.2.2=py312hca03da5_0 + - brotli-python=1.0.9=py312h313beb8_8 + - bzip2=1.0.8=h80987f9_6 + - ca-certificates=2024.3.11=hca03da5_0 + - certifi=2024.6.2=py312hca03da5_0 + - charset-normalizer=2.0.4=pyhd3eb1b0_0 + - click=8.1.7=py312hca03da5_0 + - dill=0.3.8=py312hca03da5_0 + - expat=2.6.2=h313beb8_0 + - isort=5.13.2=py312hca03da5_0 + - libcxx=14.0.6=h848a8c0_0 + - libffi=3.4.4=hca03da5_1 + - mccabe=0.7.0=pyhd3eb1b0_0 + - ncurses=6.4=h313beb8_0 + - openssl=3.0.13=h1a28f6b_2 + - pip=23.3.1=py312hca03da5_0 + - pylint=3.2.2=py312hca03da5_0 + - pysocks=1.7.1=py312hca03da5_0 + - python=3.12.1=h99e199e_0 + - python-dateutil=2.9.0post0=py312hca03da5_2 + - python-dotenv=0.21.0=pyhd8ed1ab_0 + - pytz=2024.1=py312hca03da5_0 + - readline=8.2=h1a28f6b_0 + - requests=2.31.0=py312hca03da5_1 + - setuptools=69.5.1=py312hca03da5_0 + - six=1.16.0=pyhd3eb1b0_1 + - sqlite=3.45.3=h80987f9_0 + - tk=8.6.14=h6ba3021_0 + - tzdata=2024a=h04d1e81_0 + - urllib3=2.2.1=py312hca03da5_0 + - wheel=0.43.0=py312hca03da5_0 + - xz=5.4.6=h80987f9_1 + - zlib=1.2.13=h18a0788_1 + - pip: + - -r file:requirements.txt +prefix: /opt/homebrew/Caskroom/miniconda/base/envs/things-automation diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4009103 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,77 @@ +annotated-types==0.7.0 +anyio==4.4.0 +astroid @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_b7z3htqpm1/croot/astroid_1717618370968/work +better-rich-prompts==1.0.2 +black==24.4.2 +Brotli @ file:///private/var/folders/k1/30mswbxs7r1g6zwn8y4fyt500000gp/T/abs_27zk0eqdh0/croot/brotli-split_1714483157007/work +build==1.2.1 +CacheControl==0.14.0 +certifi==2024.6.2 +cffi==1.16.0 +charset-normalizer @ file:///tmp/build/80754af9/charset-normalizer_1630003229654/work +cleo==2.1.0 +click==8.1.7 +crashtest==0.4.1 +dill @ file:///private/var/folders/k1/30mswbxs7r1g6zwn8y4fyt500000gp/T/abs_28zwy_olqk/croot/dill_1715094676263/work +distlib==0.3.8 +dnspython==2.6.1 +dulwich==0.21.7 +email_validator==2.1.1 +fastjsonschema==2.19.1 +filelock==3.14.0 +h11==0.14.0 +h2==4.1.0 +hpack==4.0.0 +httpcore==1.0.5 +httpx==0.27.0 +hyperframe==6.0.1 +idna==3.7 +installer==0.7.0 +isort @ file:///private/var/folders/k1/30mswbxs7r1g6zwn8y4fyt500000gp/T/abs_1bnvhw7_ex/croot/isort_1718291367347/work +jaraco.classes==3.4.0 +keyring==24.3.1 +markdown-it-py==3.0.0 +mccabe @ file:///opt/conda/conda-bld/mccabe_1644221741721/work +mdurl==0.1.2 +more-itertools==10.2.0 +msgpack==1.0.8 +mypy-extensions==1.0.0 +packaging==24.0 +pathspec==0.12.1 +pexpect==4.9.0 +pkginfo==1.10.0 +platformdirs @ file:///Users/builder/cbouss/perseverance-python-buildout/croot/platformdirs_1701803010714/work +poetry==1.8.3 +poetry-core==1.9.0 +poetry-plugin-export==1.8.0 +ptyprocess==0.7.0 +pycparser==2.22 +pydantic==2.7.3 +pydantic_core==2.18.4 +Pygments==2.18.0 +pylint @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_b623auwyzd/croot/pylint_1717622581741/work +pyproject_hooks==1.1.0 +PySocks @ file:///Users/builder/cbouss/perseverance-python-buildout/croot/pysocks_1699237568675/work +python-dateutil==2.9.0.post0 +python-dotenv @ file:///home/conda/feedstock_root/build_artifacts/python-dotenv_1662230030282/work +pytz @ file:///private/var/folders/k1/30mswbxs7r1g6zwn8y4fyt500000gp/T/abs_a4b76c83ik/croot/pytz_1713974318928/work +rapidfuzz==3.9.1 +reclaim-sdk @ git+https://github.com/cato447/reclaim-sdk.git@dd1bd0792be24b06fe57f40633a197699f7888c0 +requests @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_b3tnputioh/croot/requests_1707355573919/work +requests-toolbelt==1.0.0 +rich==13.7.1 +setuptools==69.5.1 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +things.py @ git+https://github.com/cato447/things.py.git@10fa2c8a07142cd149ef59bff22931ae0856c114 +toggl_python @ file:///Users/cato/Code/Cato447/toggl-python +toml==0.10.2 +tomlkit @ file:///Users/builder/cbouss/perseverance-python-buildout/croot/tomlkit_1699238737474/work +trove-classifiers==2024.5.22 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3 @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_22oz53beet/croot/urllib3_1715635830593/work +virtualenv==20.26.2 +wheel==0.43.0 +xattr==1.1.0 diff --git a/things2reclaim/database_handler.py b/things2reclaim/database_handler.py index 5e8006e..d98d979 100644 --- a/things2reclaim/database_handler.py +++ b/things2reclaim/database_handler.py @@ -1,7 +1,7 @@ import sqlite3 -class UploadedTasksDB(object): +class UploadedTasksDB: def __init__(self, filename): self.conn: sqlite3.Connection = sqlite3.connect(filename) self.__create_tables() @@ -9,7 +9,7 @@ class UploadedTasksDB(object): def __enter__(self): return self - def __exit__(self, type, value, traceback): + def __exit__(self, *exc): self.conn.close() def __create_tables(self): diff --git a/things2reclaim/main.py b/things2reclaim/main.py index b0d38a7..6f88c1e 100755 --- a/things2reclaim/main.py +++ b/things2reclaim/main.py @@ -1,25 +1,26 @@ #!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3 -from datetime import datetime -from dateutil import tz -from typing import Optional, List, Dict -import tomllib -from pathlib import Path - import sqlite3 -import typer +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional +import tomllib + +from dateutil import tz from rich import print as rprint from rich.console import Console +from rich.prompt import Confirm from rich.table import Table from rich.text import Text -from rich.prompt import Confirm from typing_extensions import Annotated +import typer + +from things2reclaim import reclaim_handler +from things2reclaim import things_handler +from things2reclaim import toggl_handler +from things2reclaim import utils +from things2reclaim.database_handler import UploadedTasksDB -import utils -import things_handler -import reclaim_handler -from database_handler import UploadedTasksDB -import toggl_handler CONFIG_PATH = Path("config/.things2reclaim.toml") @@ -138,18 +139,18 @@ def list_reclaim_tasks(subject: Annotated[Optional[str], typer.Argument()] = Non reclaim_tasks = reclaim_handler.filter_for_subject(subject, reclaim_tasks) current_date = datetime.now(tz.tzutc()) table = Table("Index", "Task", "Days left", title="Task list") - for id, task in enumerate(reclaim_tasks): + for index, task in enumerate(reclaim_tasks): if current_date > task.due_date: days_behind = (current_date - task.due_date).days table.add_row( - f"({id + 1})", + f"({index + 1})", task.name, Text(f"{days_behind} days overdue", style="bold red"), ) else: days_left = (task.due_date - current_date).days table.add_row( - f"({id + 1})", + f"({index + 1})", task.name, Text(f"{days_left} days left", style="bold white"), ) @@ -223,7 +224,8 @@ def stop_task(): time_format = "%H:%M" local_zone = tz.gettz() rprint( - f"Logged work from {current_task.start.astimezone(local_zone).strftime(time_format)} to {stop_time.astimezone(local_zone).strftime(time_format)} for {stopped_task.name}" + f"""Logged work from {current_task.start.astimezone(local_zone).strftime(time_format)} + to {stop_time.astimezone(local_zone).strftime(time_format)} for {stopped_task.name}""" ) @@ -278,7 +280,8 @@ def print_time_needed(): today = datetime.now(tz.tzutc()) print( - f"Last task is scheduled for {last_task_date.strftime('%d.%m.%Y')} ({last_task_date - today} till completion)" + f"""Last task is scheduled for {last_task_date.strftime('%d.%m.%Y')} + ({last_task_date - today} till completion)""" ) diff --git a/things2reclaim/reclaim_handler.py b/things2reclaim/reclaim_handler.py index 83bf985..a730ce6 100644 --- a/things2reclaim/reclaim_handler.py +++ b/things2reclaim/reclaim_handler.py @@ -32,6 +32,10 @@ def get_project(task: ReclaimTask): return task.name.split(" ")[0] +def get_events(since_days: int = 1): + return ReclaimTaskEvent.search() + + def start_task(task: ReclaimTask): task.prioritize() @@ -61,7 +65,7 @@ def log_work_for_task(task: ReclaimTask, start: datetime, end: datetime): last_event.start = start.astimezone(utc) last_event.end = end.astimezone(utc) - last_event._update() + last_event.save() def finish_task(task: ReclaimTask): diff --git a/things2reclaim/things_handler.py b/things2reclaim/things_handler.py index de0a6c3..c9c62e1 100644 --- a/things2reclaim/things_handler.py +++ b/things2reclaim/things_handler.py @@ -4,7 +4,7 @@ import tomllib import things -from database_handler import UploadedTasksDB +from things2reclaim.database_handler import UploadedTasksDB _config = {} CONFIG_PATH = Path("config/.things2reclaim.toml") @@ -49,7 +49,7 @@ def get_all_uploaded_things_tasks() -> List: def get_task_tags(things_task: Dict) -> Dict[str, str]: - return {k: v for (k, v) in [tag.split(": ") for tag in things_task["tags"]]} + return dict([tag.split(": ") for tag in things_task["tags"]]) def full_name(things_task) -> str: diff --git a/things2reclaim/toggl_handler.py b/things2reclaim/toggl_handler.py index 82e07ec..020bc62 100644 --- a/things2reclaim/toggl_handler.py +++ b/things2reclaim/toggl_handler.py @@ -1,13 +1,12 @@ -from toggl_python.entities import TimeEntry -import toggl_python -import tomllib -from pathlib import Path -from dateutil import tz import difflib - from datetime import datetime, timedelta -from better_rich_prompts.prompt import ListPrompt +from pathlib import Path +import tomllib +import toggl_python +from better_rich_prompts.prompt import ListPrompt +from dateutil import tz +from toggl_python.entities import TimeEntry _config = {} @@ -29,8 +28,8 @@ time_entry_editor = toggl_python.WorkspaceTimeEntries( ) -def get_time_entry(id: int): - return toggl_python.TimeEntries(auth=auth).retrieve(id) +def get_time_entry(time_entry_id: int): + return toggl_python.TimeEntries(auth=auth).retrieve(time_entry_id) def get_time_entries(since_days: int = 30): @@ -65,14 +64,13 @@ def get_approriate_tag(description: str) -> str | None: if not possible_tags: print("Found no matching tags") - return + return None possible_tags = list(possible_tags) if len(possible_tags) == 1: return possible_tags[0] - else: - return ListPrompt.ask("Select the best fitting tag", possible_tags) + return ListPrompt.ask("Select the best fitting tag", possible_tags) def create_task_time_entry( @@ -95,9 +93,8 @@ def create_task_time_entry( if start is not None: if start.tzinfo is None: raise ValueError("start has to be timezone aware") - else: - start = start.astimezone(tz.tzutc()) - time_entry.start = start + start = start.astimezone(tz.tzutc()) + time_entry.start = start tag = get_approriate_tag(description) if tag: diff --git a/things2reclaim/utils.py b/things2reclaim/utils.py index df15058..2e6b3ce 100644 --- a/things2reclaim/utils.py +++ b/things2reclaim/utils.py @@ -3,14 +3,15 @@ import re from typing import Union, Dict, Any, List import difflib -from rich import print as rprint -import things_handler from better_rich_prompts.prompt import ListPrompt +from rich import print as rprint -regex = ( +from things2reclaim import things_handler + +TIME_PATTERN = ( r"((\d+\.?\d*) (hours|hrs|hour|hr|h))? ?((\d+\.?\d*) (mins|min|minutes|minute|m))?" ) -pattern = re.compile(regex) +pattern = re.compile(TIME_PATTERN) def calculate_time_on_unit(tag_value: str) -> float: @@ -65,8 +66,8 @@ def get_closest_match(name: str, candidates: Dict[str, Any]) -> Any | None: return None if len(possible_candidates) == 1: return candidates[possible_candidates[0]] - else: - return candidates[ListPrompt.ask("Select a candidate", possible_candidates)] + + return candidates[ListPrompt.ask("Select a candidate", possible_candidates)] def pinfo(msg: str):