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
import sqlite3
from datetime import datetime
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Union
import tomllib
@@ -244,12 +244,7 @@ def stop_task():
finish_task(stopped_task)
rprint(f"Finished {stopped_task.name}")
time_format = "%H:%M"
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}")
)
utils.plogtime(current_task.start, stop_time, stopped_task.name)
@app.command("stats")
@@ -301,7 +296,7 @@ def print_time_needed():
time_needed/len(tasks):.2f} hrs")
# 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
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()
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
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")

View File

@@ -28,6 +28,13 @@ things_id_pattern : Pattern[str] = re.compile(THINGS_ID_PATTERN)
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]:
return ReclaimTask.search()
@@ -60,15 +67,19 @@ def get_project(task: ReclaimTask):
def get_clean_time_entry_name(name : str):
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]:
date_now = datetime.now(tz.tzutc()).date()
date_since = date_now - timedelta(days=since_days)
date_end = date_now + timedelta(days = 1) # end date is exclusive
events = ReclaimTaskEvent.search(date_since, date_end)
task_names = get_reclaim_task_names()
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]
return [event for event in events if is_task_time_entry(event.name)]
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 pathlib import Path
import tomllib
from typing import List
import toggl_python
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)
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):
return toggl_python.TimeEntries(auth=auth).list(
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
"""
@@ -61,7 +73,7 @@ def get_current_time_entry() -> TimeEntry | None:
return time_entries.current()
def get_tags():
def get_tags() -> List[toggl_python.Tag]:
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(
description: str, project: str, start: datetime | None = None, duration: int = -1
):
) -> toggl_python.TimeEntry:
"""
duration is in seconds
"""

View File

@@ -2,6 +2,7 @@ from datetime import datetime
import re
from typing import Union, Dict, TypeVar, List
import difflib
from dateutil import tz
from better_rich_prompts.prompt import ListPrompt
from rich import print as rprint
@@ -84,6 +85,23 @@ def perror(msg: str):
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:
return f"things_task:{things_task["uuid"]}"