Implemented Toggl -> Reclaim sync for the current day

This commit is contained in:
2024-06-19 23:45:06 +02:00
parent bf7bc7177a
commit cda9543463
4 changed files with 80 additions and 17 deletions

View File

@@ -1,7 +1,7 @@
#!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3 #!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
import tomllib import tomllib
@@ -244,12 +244,7 @@ def stop_task():
finish_task(stopped_task) finish_task(stopped_task)
rprint(f"Finished {stopped_task.name}") rprint(f"Finished {stopped_task.name}")
time_format = "%H:%M" utils.plogtime(current_task.start, stop_time, stopped_task.name)
local_zone = tz.gettz()
rprint(
(f"Logged work from {current_task.start.astimezone(local_zone).strftime(time_format)} "
f"to {stop_time.astimezone(local_zone).strftime(time_format)} for {stopped_task.name}")
)
@app.command("stats") @app.command("stats")
@@ -301,7 +296,7 @@ def print_time_needed():
time_needed/len(tasks):.2f} hrs") time_needed/len(tasks):.2f} hrs")
# sort unscheduled tasks to the end of the list # sort unscheduled tasks to the end of the list
tasks.sort(key=lambda x: x.scheduled_start_date if x.scheduled_start_date is not None else float('inf')) tasks.sort(key=lambda x: x.scheduled_start_date if x.scheduled_start_date is not None else datetime.max.replace(tzinfo=tz.tzutc()))
last_task_date = tasks[-1].scheduled_start_date last_task_date = tasks[-1].scheduled_start_date
if last_task_date is None: # last task on todo list is not scheduled if last_task_date is None: # last task on todo list is not scheduled
@@ -340,8 +335,35 @@ def sync_toggl_reclaim_tracking():
reclaim_handler.get_reclaim_tasks() reclaim_handler.get_reclaim_tasks()
toggl_time_entries = toggl_handler.get_time_entries_since(since_days = since_days) # end date is inclusive toggl_time_entries = toggl_handler.get_time_entries_since(since_days = since_days) # end date is inclusive
reclaim_time_entries = reclaim_handler.get_task_events_since(since_days = since_days) # end date is inclusive reclaim_time_entries = reclaim_handler.get_task_events_since(since_days = since_days) # end date is inclusive
rprint(reclaim_time_entries) reclaim_time_entry_names = [reclaim_handler.get_clean_time_entry_name(time_entry.name) for time_entry in reclaim_time_entries]
missing_reclaim_entries = [time_entry for time_entry in toggl_time_entries if time_entry.description not in reclaim_time_entry_names]
if not missing_reclaim_entries:
utils.pinfo("Toggl and Reclaim time entries are synced.")
else:
for time_entry in missing_reclaim_entries:
name = time_entry.description
if not name:
raise ValueError("Toggl time entry has empty description")
reclaim_task = reclaim_handler.get_reclaim_task(name)
if not reclaim_task:
utils.pwarning(f"Couldn't find {time_entry.description} in Reclaim.")
continue
start = toggl_handler.get_start_time(time_entry)
stop = toggl_handler.get_stop_time(time_entry)
reclaim_handler.log_work_for_task(reclaim_task, start, stop)
utils.plogtime(start, stop, reclaim_task.name)
@app.command("current")
def display_current_task():
current_task = toggl_handler.get_current_time_entry()
if current_task is None:
utils.perror("No task is currently tracked in toggl")
return
rprint(f"Current task: {current_task.description}\nStarted at: {current_task.start.astimezone(tz.gettz()).strftime("%H:%M")}")
@app.command("sync") @app.command("sync")

View File

@@ -28,6 +28,13 @@ things_id_pattern : Pattern[str] = re.compile(THINGS_ID_PATTERN)
ReclaimClient(token=RECLAIM_TOKEN) ReclaimClient(token=RECLAIM_TOKEN)
def get_reclaim_task(name : str) -> Optional[ReclaimTask]:
res = ReclaimTask.search(title = name)
if not res:
return None
else:
return res[0]
def get_reclaim_tasks() -> List[ReclaimTask]: def get_reclaim_tasks() -> List[ReclaimTask]:
return ReclaimTask.search() return ReclaimTask.search()
@@ -60,15 +67,19 @@ def get_project(task: ReclaimTask):
def get_clean_time_entry_name(name : str): def get_clean_time_entry_name(name : str):
return emoji.replace_emoji(name).lstrip() return emoji.replace_emoji(name).lstrip()
def is_task_time_entry(name: str):
decoded_name = emoji.demojize(name)
# task entries start either with a :thumbs_up: or a :check_mark_button: emoji
return decoded_name.startswith(":thumbs_up:") | decoded_name.startswith(":check_mark_button:")
def get_task_events_since(since_days: int = 0) -> List[ReclaimTaskEvent]: def get_task_events_since(since_days: int = 0) -> List[ReclaimTaskEvent]:
date_now = datetime.now(tz.tzutc()).date() date_now = datetime.now(tz.tzutc()).date()
date_since = date_now - timedelta(days=since_days) date_since = date_now - timedelta(days=since_days)
date_end = date_now + timedelta(days = 1) # end date is exclusive date_end = date_now + timedelta(days = 1) # end date is exclusive
events = ReclaimTaskEvent.search(date_since, date_end) events = ReclaimTaskEvent.search(date_since, date_end)
task_names = get_reclaim_task_names() return [event for event in events if is_task_time_entry(event.name)]
print(task_names)
[print(get_clean_time_entry_name(event.name)) for event in events]
return [event for event in events if get_clean_time_entry_name(event.name) in task_names]
def get_events_date_range(from_date: date, to_date: date): def get_events_date_range(from_date: date, to_date: date):

View File

@@ -2,6 +2,7 @@ import difflib
from datetime import datetime, timedelta, date, time from datetime import datetime, timedelta, date, time
from pathlib import Path from pathlib import Path
import tomllib import tomllib
from typing import List
import toggl_python import toggl_python
from better_rich_prompts.prompt import ListPrompt from better_rich_prompts.prompt import ListPrompt
@@ -28,17 +29,28 @@ time_entry_editor = toggl_python.WorkspaceTimeEntries(
) )
def get_time_entry(time_entry_id: int): def get_time_entry(time_entry_id: int) -> toggl_python.TimeEntry:
return toggl_python.TimeEntries(auth=auth).retrieve(time_entry_id) return toggl_python.TimeEntries(auth=auth).retrieve(time_entry_id)
def get_start_time(time_entry : toggl_python.TimeEntry):
return time_entry.start() if callable(time_entry.start) else time_entry.start
def get_stop_time(time_entry: toggl_python.TimeEntry):
if time_entry.stop is None:
return get_start_time(time_entry) + timedelta(seconds=time_entry.duration)
else:
return time_entry.stop() if callable(time_entry.stop) else time_entry.stop
def get_time_entries_date_range(from_date: date, to_date: date): def get_time_entries_date_range(from_date: date, to_date: date):
return toggl_python.TimeEntries(auth=auth).list( return toggl_python.TimeEntries(auth=auth).list(
start_date=from_date.isoformat(), end_date=to_date.isoformat() start_date=from_date.isoformat(), end_date=to_date.isoformat()
) )
def get_time_entries_since(since_days: int = 30): def get_time_entries_since(since_days: int = 30) -> List[toggl_python.TimeEntry]:
""" """
get time entries since days at midnight get time entries since days at midnight
""" """
@@ -61,7 +73,7 @@ def get_current_time_entry() -> TimeEntry | None:
return time_entries.current() return time_entries.current()
def get_tags(): def get_tags() -> List[toggl_python.Tag]:
return toggl_python.Workspaces(auth=auth).tags(_id=workspace.id) return toggl_python.Workspaces(auth=auth).tags(_id=workspace.id)
@@ -85,7 +97,7 @@ def get_approriate_tag(description: str) -> str | None:
def create_task_time_entry( def create_task_time_entry(
description: str, project: str, start: datetime | None = None, duration: int = -1 description: str, project: str, start: datetime | None = None, duration: int = -1
): ) -> toggl_python.TimeEntry:
""" """
duration is in seconds duration is in seconds
""" """

View File

@@ -2,6 +2,7 @@ from datetime import datetime
import re import re
from typing import Union, Dict, TypeVar, List from typing import Union, Dict, TypeVar, List
import difflib import difflib
from dateutil import tz
from better_rich_prompts.prompt import ListPrompt from better_rich_prompts.prompt import ListPrompt
from rich import print as rprint from rich import print as rprint
@@ -84,6 +85,23 @@ def perror(msg: str):
rprint(f"[bold red]Error: {msg}[/bold red]") rprint(f"[bold red]Error: {msg}[/bold red]")
def plogtime(start_time: datetime, end_time: datetime, task_name: str):
time_format = "%H:%M"
local_zone = tz.gettz()
if start_time.tzinfo is None:
raise ValueError("start_time has to be timezone aware.")
if end_time.tzinfo is None:
raise ValueError("end_time has to be timezone aware.")
rprint(
(f"Logged work from {start_time.astimezone(local_zone).strftime(time_format)} "
f"to {end_time.astimezone(local_zone).strftime(time_format)} for {task_name}")
)
def generate_things_id_tag(things_task) -> str: def generate_things_id_tag(things_task) -> str:
return f"things_task:{things_task["uuid"]}" return f"things_task:{things_task["uuid"]}"