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
|
#!/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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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"]}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user