Implemented Toggl -> Reclaim sync for the current day
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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"]}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user