[WIP] toggl returns all tasks ever created no matter if they got deleted

This commit is contained in:
2024-06-21 04:01:24 +02:00
parent dedb0bc195
commit 31f5794e72
6 changed files with 150 additions and 79 deletions

View File

@@ -1,4 +1,5 @@
import sqlite3
from typing import List
class UploadedTasksDB:
@@ -32,7 +33,7 @@ class UploadedTasksDB:
cursor.execute(insert_statement, [task_id])
self.conn.commit()
def get_all_uploaded_tasks(self):
def get_all_uploaded_tasks(self) -> List[str]:
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM uploaded_tasks")
rows = cursor.fetchall()

View File

@@ -1,10 +1,10 @@
#!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3
import sqlite3
from datetime import datetime
from pathlib import Path
from datetime import datetime, time
from typing import Dict, List, Optional, Union
import tomllib
import itertools
from dateutil import tz
from rich import print as rprint
@@ -22,13 +22,13 @@ import toggl_handler
import utils
from database_handler import UploadedTasksDB
CONFIG_PATH = Path("config/.things2reclaim.toml")
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.things2reclaim.toml"
_config = {}
with open(CONFIG_PATH, "rb") as f:
_config = tomllib.load(f)
DATABASE_PATH = _config["database"]["path"]
DATABASE_PATH = utils.get_project_root() / _config["database"]["path"]
app = typer.Typer(
add_completion=False, no_args_is_help=True
@@ -113,36 +113,24 @@ def initialize_uploaded_database(verbose: bool = False):
@app.command("upload")
def upload_things_to_reclaim(verbose: bool = False):
def upload_things_to_reclaim():
"""
Upload things tasks to reclaim
"""
projects = things_handler.extract_uni_projects()
reclaim_task_names = reclaim_handler.get_reclaim_task_names()
tasks_uploaded = 0
tasks = things_handler.get_all_things_tasks()
with UploadedTasksDB(DATABASE_PATH) as db:
for project in projects:
things_tasks = things_handler.get_tasks_for_project(project)
for things_task in things_tasks:
full_task_name = things_handler.full_name(
things_task=things_task)
if full_task_name not in reclaim_task_names:
tasks_uploaded += 1
print(f"Creating task {full_task_name} in Reclaim")
things_to_reclaim(things_task)
db.add_uploaded_task(things_task["uuid"])
else:
if verbose:
print(
f"Task {things_task['title']} \
already exists in Reclaim")
if tasks_uploaded == 0:
rprint("No new tasks were found")
elif tasks_uploaded == 1:
rprint(f"Uploaded {tasks_uploaded} task{
's' if tasks_uploaded > 1 else ''}")
uploaded_task_ids = db.get_all_uploaded_tasks()
tasks_to_upload = [task for task in tasks if task["uuid"] not in uploaded_task_ids]
if not tasks_to_upload:
print("No new tasks were found")
else:
for task in tasks_to_upload:
print(f"Creating task {things_handler.full_name(task)} in Reclaim")
things_to_reclaim(task)
db.add_uploaded_task(task["uuid"])
print(f"Uploaded {len(tasks_to_upload)} task{'s' if len(tasks_to_upload) > 1 else ''}")
@app.command("list")
def list_reclaim_tasks(subject: Annotated[Optional[str],
typer.Argument()] = None):
@@ -315,36 +303,46 @@ def remove_finished_tasks_from_things():
Complete finished reclaim tasks in things
"""
reclaim_things_uuids = reclaim_handler.get_reclaim_things_ids()
finished_someting = False
for task in things_handler.get_all_uploaded_things_tasks():
if task["uuid"] not in reclaim_things_uuids:
finished_someting = True
tasks_to_be_removed = [task for task in things_handler.get_all_uploaded_things_tasks()
if task["uuid"] not in reclaim_things_uuids]
if not tasks_to_be_removed:
print("Reclaim and Things are synced!")
else:
for task in tasks_to_be_removed:
print(
f"Found completed task: {
things_handler.full_name(things_task=task)}"
)
finish_task(task["uuid"])
if not finished_someting:
print("Reclaim and Things are synced")
@app.command("tracking")
def sync_toggl_reclaim_tracking():
since_days = 0
reclaim_handler.get_reclaim_tasks()
def sync_toggl_reclaim_tracking(since_days : Annotated[int, typer.Argument()] = 0):
toggl_time_entries = toggl_handler.get_time_entries_since(since_days = since_days) # end date is inclusive
if toggl_time_entries is None:
utils.pwarning("No tasks tracked today in Toggl")
utils.pwarning(f"No tasks tracked in Toggl since {since_days} days")
return
reclaim_time_entries = reclaim_handler.get_task_events_since(since_days = since_days) # end date is inclusive
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]
reclaim_time_entries_dict = {utils.get_clean_time_entry_name(k) : list(g) for k,g in itertools.groupby(reclaim_time_entries, lambda r : r.name)}
non_existent_time_entries = [time_entry for time_entry in toggl_time_entries if time_entry.description not in reclaim_time_entries_dict.keys()]
# add all existing mismatched_time_entries
time_entries_to_adjust : Dict[toggl_handler.TimeEntry, reclaim_handler.ReclaimTaskEvent] = {}
for toggl_time_entry in toggl_time_entries:
if toggl_time_entry.description in reclaim_time_entries_dict.keys():
nearest_entry = utils.nearest_time_entry(reclaim_time_entries_dict.get(toggl_time_entry.description), toggl_time_entry) #pyright: ignore
if toggl_time_entry in time_entries_to_adjust:
time_entries_to_adjust[toggl_time_entry] = utils.nearest_time_entry([time_entries_to_adjust[toggl_time_entry], nearest_entry], toggl_time_entry) #pyright: ignore
elif nearest_entry is not None:
time_entries_to_adjust[toggl_time_entry] = nearest_entry
if not missing_reclaim_entries:
rprint(non_existent_time_entries)
rprint(time_entries_to_adjust)
return
if not mismatched_time_entries:
utils.pinfo("Toggl and Reclaim time entries are synced.")
else:
for time_entry in missing_reclaim_entries:
for time_entry in mismatched_time_entries:
name = time_entry.description
if not name:
raise ValueError("Toggl time entry has empty description")
@@ -353,10 +351,21 @@ def sync_toggl_reclaim_tracking():
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)
toggl_start = toggl_handler.get_start_time(time_entry)
toggl_stop = toggl_handler.get_stop_time(time_entry)
reclaim_handler.log_work_for_task(reclaim_task, start, stop)
if toggl_start.tzinfo is None:
raise ValueError("Toggl start time is not timezone aware")
if toggl_stop.tzinfo is None:
raise ValueError("Toggl stop time is not timezone aware")
reclaim_time_entry = reclaim_time_entries_dict[reclaim_task.name]
if reclaim_time_entry is None:
reclaim_handler.log_work_for_task(reclaim_task, start, stop)
else:
reclaim_handler.adjust_time_entry(reclaim_time_entry, start, stop)
utils.plogtime(start, stop, reclaim_task.name)
@@ -372,7 +381,7 @@ def display_current_task():
@app.command("sync")
def sync_things_and_reclaim(verbose: bool = False):
def sync_things_and_reclaim():
"""
Sync tasks between things and reclaim
First updated all finished tasks in reclaim to completed in things
@@ -382,7 +391,7 @@ def sync_things_and_reclaim(verbose: bool = False):
remove_finished_tasks_from_things()
rprint("---------------------------------------------")
utils.pinfo("Pushing to Reclaim")
upload_things_to_reclaim(verbose=verbose)
upload_things_to_reclaim()
rprint("---------------------------------------------")

View File

@@ -1,5 +1,4 @@
from datetime import datetime, date, timedelta
from pathlib import Path
from typing import List, Dict, Pattern, Optional
import re
@@ -11,8 +10,9 @@ from reclaim_sdk.models.task import ReclaimTask
from reclaim_sdk.models.task_event import ReclaimTaskEvent
from deadline_status import DeadlineStatus
import utils
CONFIG_PATH = Path("config/.reclaim.toml")
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.reclaim.toml"
_config = {}
@@ -64,10 +64,6 @@ def get_project(task: ReclaimTask):
return task.name.split(" ")[0]
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
@@ -81,7 +77,6 @@ def get_task_events_since(since_days: int = 0) -> List[ReclaimTaskEvent]:
events = ReclaimTaskEvent.search(date_since, date_end)
return [event for event in events if is_task_time_entry(event.name)]
def get_events_date_range(from_date: date, to_date: date):
return ReclaimTaskEvent.search(from_date, to_date)
@@ -97,13 +92,6 @@ def log_work_for_task(task: ReclaimTask, start: datetime, end: datetime):
"""
start and end are in Europe/Berlin timezone
"""
utc = tz.tzutc()
if start.tzinfo is None:
raise ValueError("start is not timezone aware")
if end.tzinfo is None:
raise ValueError("end is not timezone aware")
if not task.is_scheduled:
raise ValueError("Task is not scheduled")
@@ -111,10 +99,20 @@ def log_work_for_task(task: ReclaimTask, start: datetime, end: datetime):
raise ValueError("Event list is empty")
last_event: ReclaimTaskEvent = task.events[-1]
adjust_time_entry(last_event, start, end)
last_event.start = start.astimezone(utc)
last_event.end = end.astimezone(utc)
last_event.save()
def adjust_time_entry(time_entry: ReclaimTaskEvent, start: datetime, end: datetime):
utc = tz.tzutc()
if start.tzinfo is None:
raise ValueError("start is not timezone aware")
if end.tzinfo is None:
raise ValueError("end is not timezone aware")
time_entry.start = start.astimezone(utc)
time_entry.end = end.astimezone(utc)
time_entry.save()
def finish_task(task: ReclaimTask):

View File

@@ -1,18 +1,18 @@
from typing import Dict, List
from pathlib import Path
import tomllib
import things
from database_handler import UploadedTasksDB
import utils
_config = {}
CONFIG_PATH = Path("config/.things2reclaim.toml")
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.things2reclaim.toml"
with open(CONFIG_PATH, "rb") as f:
_config = tomllib.load(f)
DATABASE_PATH = _config["database"]["path"]
DATABASE_PATH = utils.get_project_root() / _config["database"]["path"]
def extract_uni_projects():
@@ -32,6 +32,10 @@ def get_tasks_for_project(project) -> Dict | List[Dict]:
return things.tasks(project=project["uuid"], type="to-do")
def is_equal_fullname(things_task : Dict, name : str):
return full_name(things_task=things_task) == name
def get_all_things_tasks() -> List:
tasks = []
projects = extract_uni_projects()

View File

@@ -1,6 +1,5 @@
import difflib
from datetime import datetime, timedelta, date, time
from pathlib import Path
import tomllib
from typing import List
@@ -9,9 +8,11 @@ from better_rich_prompts.prompt import ListPrompt
from dateutil import tz
from toggl_python.entities import TimeEntry
import utils
_config = {}
CONFIG_PATH = Path("config/.toggl.toml")
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.toggl.toml"
with open(CONFIG_PATH, "rb") as f:
_config = tomllib.load(f)
@@ -32,6 +33,8 @@ time_entry_editor = toggl_python.WorkspaceTimeEntries(
def get_time_entry(time_entry_id: int) -> toggl_python.TimeEntry:
return toggl_python.TimeEntries(auth=auth).retrieve(time_entry_id)
def delete_time_entry(time_entry_id: int) -> bool:
return time_entry_editor.delete_timeentry(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

View File

@@ -1,13 +1,15 @@
from datetime import datetime
from datetime import datetime, timedelta
import re
from typing import Union, Dict, TypeVar, List
from typing import Union, Dict, TypeVar, List, Optional
import difflib
from dateutil import tz
import emoji
from pathlib import Path
from better_rich_prompts.prompt import ListPrompt
from rich import print as rprint
import things_handler
from toggl_python import TimeEntry
from reclaim_sdk.models.task_event import ReclaimTaskEvent
TIME_PATTERN = (
r"((\d+\.?\d*) (hours|hrs|hour|hr|h))? ?((\d+\.?\d*) (mins|min|minutes|minute|m))?"
@@ -36,6 +38,21 @@ def calculate_time_on_unit(tag_value: str) -> float:
return time
def get_start_time(toggl_time_entry: TimeEntry):
return toggl_time_entry.start() if callable(toggl_time_entry.start) else toggl_time_entry.start
def get_stop_time(time_entry: 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_clean_time_entry_name(name : str):
return emoji.replace_emoji(name).lstrip()
def map_tag_values(
things_task,
tags_dict: Dict[str, str],
@@ -73,6 +90,45 @@ def get_closest_match(name: str, candidates: Dict[str, T]) -> T | None:
return candidates[ListPrompt.ask("Select a candidate", possible_candidates)]
def nearest_time_entry(items: Optional[List[ReclaimTaskEvent]], pivot: TimeEntry) -> Optional[ReclaimTaskEvent]:
if not items:
return None
return min(items, key=lambda x: abs(x.start - get_start_time(pivot)))
def is_matching_time_entry(toggl_time_entry : Optional[TimeEntry], reclaim_time_entry : Optional[ReclaimTaskEvent]):
if toggl_time_entry is None or reclaim_time_entry is None:
print("One is none")
return False
if toggl_time_entry.description != get_clean_time_entry_name(reclaim_time_entry.name):
print(f"toggl title: {toggl_time_entry.description}")
print(f"reclaim title: {get_clean_time_entry_name(reclaim_time_entry.name)}")
return False
toggl_start = get_start_time(toggl_time_entry)
toggl_stop = get_stop_time(toggl_time_entry)
reclaim_start = reclaim_time_entry.start
reclaim_end = reclaim_time_entry.end
if toggl_start.tzinfo is None:
raise ValueError("Toggl start is not timezone aware.")
if toggl_stop.tzinfo is None:
raise ValueError("Toggl stop is not timezone aware.")
if reclaim_start.tzinfo is None:
raise ValueError("Reclaim start is not timezone aware.")
if reclaim_end.tzinfo is None:
raise ValueError("Reclaim stop is not timezone aware.")
if toggl_start.astimezone(tz.tzutc()) != reclaim_start.astimezone(tz.tzutc()):
print(f"toggl_start: {toggl_start.isoformat()}")
print(f"reclaim_start: {reclaim_start.isoformat()}")
return False
if toggl_stop.astimezone(tz.tzutc()) != reclaim_end.astimezone(tz.tzutc()):
print(f"toggl_end: {toggl_stop.isoformat()}")
print(f"reclaim_end: {reclaim_end.isoformat()}")
return False
return True
def pinfo(msg: str):
rprint(f"[bold white]{msg}[/bold white]")
@@ -122,5 +178,5 @@ def reclaim_task_pretty_print(task):
print(f"\tDuration: {task.duration}")
def things_reclaim_is_equal(things_task, reclaim_task) -> bool:
return things_handler.full_name(things_task=things_task) == reclaim_task.name
def get_project_root() -> Path:
return Path(__file__).parent.parent