refactoring
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class DeadlineStatus(Enum):
|
class DeadlineStatus(Enum):
|
||||||
FINE = 1
|
FINE = 1
|
||||||
OVERDUE = 2
|
OVERDUE = 2
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ with open(CONFIG_PATH, "rb") as f:
|
|||||||
|
|
||||||
DATABASE_PATH = utils.get_project_root() / _config["database"]["path"]
|
DATABASE_PATH = utils.get_project_root() / _config["database"]["path"]
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
||||||
add_completion=False, no_args_is_help=True
|
|
||||||
)
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +66,7 @@ def things_to_reclaim(things_task):
|
|||||||
reclaim_handler.create_reaclaim_task_from_dict(params)
|
reclaim_handler.create_reaclaim_task_from_dict(params)
|
||||||
|
|
||||||
|
|
||||||
def finish_task(task : Union[reclaim_handler.ReclaimTask, str]):
|
def finish_task(task: Union[reclaim_handler.ReclaimTask, str]):
|
||||||
"""
|
"""
|
||||||
Finish task
|
Finish task
|
||||||
"""
|
"""
|
||||||
@@ -114,14 +112,16 @@ def initialize_uploaded_database(verbose: bool = False):
|
|||||||
|
|
||||||
|
|
||||||
@app.command("upload")
|
@app.command("upload")
|
||||||
def upload_things_to_reclaim(dry_run : bool = False):
|
def upload_things_to_reclaim(dry_run: bool = False):
|
||||||
"""
|
"""
|
||||||
Upload things tasks to reclaim
|
Upload things tasks to reclaim
|
||||||
"""
|
"""
|
||||||
tasks = things_handler.get_all_things_tasks()
|
tasks = things_handler.get_all_things_tasks()
|
||||||
with UploadedTasksDB(DATABASE_PATH) as db:
|
with UploadedTasksDB(DATABASE_PATH) as db:
|
||||||
uploaded_task_ids = db.get_all_uploaded_tasks()
|
uploaded_task_ids = db.get_all_uploaded_tasks()
|
||||||
tasks_to_upload = [task for task in tasks if task["uuid"] not in uploaded_task_ids]
|
tasks_to_upload = [
|
||||||
|
task for task in tasks if task["uuid"] not in uploaded_task_ids
|
||||||
|
]
|
||||||
if not tasks_to_upload:
|
if not tasks_to_upload:
|
||||||
print("No new tasks were found")
|
print("No new tasks were found")
|
||||||
else:
|
else:
|
||||||
@@ -130,26 +130,26 @@ def upload_things_to_reclaim(dry_run : bool = False):
|
|||||||
if not dry_run:
|
if not dry_run:
|
||||||
things_to_reclaim(task)
|
things_to_reclaim(task)
|
||||||
db.add_uploaded_task(task["uuid"])
|
db.add_uploaded_task(task["uuid"])
|
||||||
print(f"Uploaded {len(tasks_to_upload)} task{'s' if len(tasks_to_upload) > 1 else ''}")
|
print(
|
||||||
|
f"Uploaded {len(tasks_to_upload)} task{'s' if len(tasks_to_upload) > 1 else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("list")
|
@app.command("list")
|
||||||
def list_reclaim_tasks(subject: Annotated[Optional[str],
|
def list_reclaim_tasks(subject: Annotated[Optional[str], typer.Argument()] = None):
|
||||||
typer.Argument()] = None):
|
|
||||||
"""
|
"""
|
||||||
List all current tasks
|
List all current tasks
|
||||||
"""
|
"""
|
||||||
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
if subject is not None:
|
if subject is not None:
|
||||||
reclaim_tasks = reclaim_handler.filter_for_subject(
|
reclaim_tasks = reclaim_handler.filter_for_subject(subject, reclaim_tasks)
|
||||||
subject, reclaim_tasks)
|
|
||||||
current_date = datetime.now(tz.tzutc())
|
current_date = datetime.now(tz.tzutc())
|
||||||
table = Table("Index", "Task", "Days left", title="Task list")
|
table = Table("Index", "Task", "Days left", title="Task list")
|
||||||
for index, task in enumerate(reclaim_tasks):
|
for index, task in enumerate(reclaim_tasks):
|
||||||
due_date = task.due_date
|
due_date = task.due_date
|
||||||
if due_date is None:
|
if due_date is None:
|
||||||
date_str = Text()
|
date_str = Text()
|
||||||
else:
|
else:
|
||||||
if current_date > due_date:
|
if current_date > due_date:
|
||||||
days_behind = (current_date - due_date).days
|
days_behind = (current_date - due_date).days
|
||||||
date_str = Text(f"{days_behind} days overdue")
|
date_str = Text(f"{days_behind} days overdue")
|
||||||
@@ -158,10 +158,7 @@ def list_reclaim_tasks(subject: Annotated[Optional[str],
|
|||||||
days_left = (due_date - current_date).days
|
days_left = (due_date - current_date).days
|
||||||
date_str = Text(f"{days_left} days left")
|
date_str = Text(f"{days_left} days left")
|
||||||
date_str.stylize("bold white")
|
date_str.stylize("bold white")
|
||||||
table.add_row(
|
table.add_row(f"({index + 1})", task.name, date_str)
|
||||||
f"({index + 1})",
|
|
||||||
task.name,
|
|
||||||
date_str)
|
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
@@ -198,43 +195,42 @@ def stop_task():
|
|||||||
utils.perror("No task is currently tracked in toggl")
|
utils.perror("No task is currently tracked in toggl")
|
||||||
return
|
return
|
||||||
|
|
||||||
stopped_task_name = current_task.description
|
current_task_name = current_task.description
|
||||||
|
|
||||||
if stopped_task_name is None:
|
if current_task_name is None:
|
||||||
utils.perror("Current toggl task has no name")
|
utils.perror("Current toggl task has no name")
|
||||||
return
|
return
|
||||||
|
|
||||||
reclaim_dict = {
|
reclaim_dict = {task.name: task for task in reclaim_handler.get_reclaim_tasks()}
|
||||||
task.name: task for task in reclaim_handler.get_reclaim_tasks()}
|
|
||||||
|
|
||||||
if stopped_task_name in reclaim_dict.keys():
|
if current_task_name in reclaim_dict.keys():
|
||||||
stopped_task = reclaim_dict[stopped_task_name]
|
reclaim_task = reclaim_dict[current_task_name]
|
||||||
else:
|
else:
|
||||||
stopped_task = utils.get_closest_match(stopped_task_name, reclaim_dict)
|
reclaim_task = utils.get_closest_match(current_task_name, reclaim_dict)
|
||||||
|
|
||||||
if stopped_task is None:
|
if reclaim_task is None:
|
||||||
utils.perror(f"{stopped_task} not found in reclaim")
|
utils.perror(f"{current_task_name} not found in reclaim")
|
||||||
return
|
return
|
||||||
|
|
||||||
stop_time = datetime.now(tz.tzutc())
|
stopped_task = toggl_handler.stop_task(current_task)
|
||||||
if callable(current_task.start):
|
if stopped_task is None:
|
||||||
current_task.start = current_task.start()
|
utils.perror(f"{current_task_name} could not be stopped")
|
||||||
|
return
|
||||||
toggl_handler.stop_current_task()
|
start_time = toggl_handler.get_start_time(stopped_task)
|
||||||
|
stop_time = toggl_handler.get_stop_time(stopped_task)
|
||||||
|
|
||||||
is_task_finished = Confirm.ask("Is task finished?", default=False)
|
is_task_finished = Confirm.ask("Is task finished?", default=False)
|
||||||
|
|
||||||
if stopped_task.is_scheduled:
|
try:
|
||||||
reclaim_handler.log_work_for_task(
|
reclaim_handler.log_work_for_task(reclaim_task, start_time, stop_time)
|
||||||
stopped_task, current_task.start, stop_time)
|
except ValueError:
|
||||||
else:
|
|
||||||
utils.pwarning("Work could not be logged in reclaim!")
|
utils.pwarning("Work could not be logged in reclaim!")
|
||||||
|
|
||||||
if is_task_finished:
|
if is_task_finished:
|
||||||
finish_task(stopped_task)
|
finish_task(reclaim_task)
|
||||||
rprint(f"Finished {stopped_task.name}")
|
rprint(f"Finished {reclaim_task.name}")
|
||||||
|
|
||||||
utils.plogtime(current_task.start, stop_time, stopped_task.name)
|
utils.plogtime(start_time, stop_time, reclaim_task.name)
|
||||||
|
|
||||||
|
|
||||||
@app.command("stats")
|
@app.command("stats")
|
||||||
@@ -245,20 +241,22 @@ def show_task_stats():
|
|||||||
current_date = datetime.now(tz.tzutc())
|
current_date = datetime.now(tz.tzutc())
|
||||||
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
|
|
||||||
tasks_fine = reclaim_handler.filter_for_deadline_status(current_date, DeadlineStatus.FINE, reclaim_tasks)
|
tasks_fine = reclaim_handler.filter_for_deadline_status(
|
||||||
tasks_overdue = reclaim_handler.filter_for_deadline_status(current_date, DeadlineStatus.OVERDUE, reclaim_tasks)
|
current_date, DeadlineStatus.FINE, reclaim_tasks
|
||||||
|
)
|
||||||
|
tasks_overdue = reclaim_handler.filter_for_deadline_status(
|
||||||
|
current_date, DeadlineStatus.OVERDUE, reclaim_tasks
|
||||||
|
)
|
||||||
|
|
||||||
fine_per_course = ["Fine"]
|
fine_per_course = ["Fine"]
|
||||||
overdue_per_course = ["Overdue"]
|
overdue_per_course = ["Overdue"]
|
||||||
course_names = things_handler.get_course_names()
|
course_names = things_handler.get_course_names()
|
||||||
for course_name in course_names:
|
for course_name in course_names:
|
||||||
fine_per_course.append(
|
fine_per_course.append(
|
||||||
str(len(reclaim_handler.filter_for_subject
|
str(len(reclaim_handler.filter_for_subject(course_name, tasks_fine)))
|
||||||
(course_name, tasks_fine)))
|
|
||||||
)
|
)
|
||||||
overdue_per_course.append(
|
overdue_per_course.append(
|
||||||
str(len(reclaim_handler.filter_for_subject
|
str(len(reclaim_handler.filter_for_subject(course_name, tasks_overdue)))
|
||||||
(course_name, tasks_overdue)))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
table = Table(*(["Status"] + course_names))
|
table = Table(*(["Status"] + course_names))
|
||||||
@@ -269,11 +267,13 @@ def show_task_stats():
|
|||||||
|
|
||||||
|
|
||||||
@app.command("time")
|
@app.command("time")
|
||||||
def print_time_needed():
|
def print_time_needed(subject: Annotated[Optional[str], typer.Argument()] = None):
|
||||||
"""
|
"""
|
||||||
Print sum of time needed for all reclaim tasks
|
Print sum of time needed for all reclaim tasks
|
||||||
"""
|
"""
|
||||||
tasks = reclaim_handler.get_reclaim_tasks()
|
tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
|
if subject is not None:
|
||||||
|
tasks = reclaim_handler.filter_for_subject(subject, tasks)
|
||||||
time_needed = 0
|
time_needed = 0
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
@@ -282,14 +282,22 @@ def print_time_needed():
|
|||||||
time_needed += task_duration
|
time_needed += task_duration
|
||||||
|
|
||||||
print(f"Time needed to complete {len(tasks)} Tasks: {time_needed} hrs")
|
print(f"Time needed to complete {len(tasks)} Tasks: {time_needed} hrs")
|
||||||
print(f"Average time needed to complete a Task: {
|
print(
|
||||||
time_needed/len(tasks):.2f} hrs")
|
f"Average time needed to complete a Task: {
|
||||||
|
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 datetime.max.replace(tzinfo=tz.tzutc()))
|
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
|
||||||
print("Too many tasks on todo list. Not all are scheduled.")
|
print("Too many tasks on todo list. Not all are scheduled.")
|
||||||
else:
|
else:
|
||||||
today = datetime.now(tz.tzutc())
|
today = datetime.now(tz.tzutc())
|
||||||
@@ -300,101 +308,102 @@ def print_time_needed():
|
|||||||
|
|
||||||
|
|
||||||
@app.command("finished")
|
@app.command("finished")
|
||||||
def remove_finished_tasks_from_things(dry_run : bool = False):
|
def remove_finished_tasks_from_things(dry_run: bool = False):
|
||||||
"""
|
"""
|
||||||
Complete finished reclaim tasks in things
|
Complete finished reclaim tasks in things
|
||||||
"""
|
"""
|
||||||
reclaim_things_uuids = reclaim_handler.get_reclaim_things_ids()
|
reclaim_things_uuids = reclaim_handler.get_reclaim_things_ids()
|
||||||
tasks_to_be_removed = [task for task in things_handler.get_all_uploaded_things_tasks()
|
tasks_to_be_removed = [
|
||||||
if task["uuid"] not in reclaim_things_uuids]
|
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:
|
if not tasks_to_be_removed:
|
||||||
print("Reclaim and Things are synced!")
|
print("Reclaim and Things are synced!")
|
||||||
|
return 0
|
||||||
else:
|
else:
|
||||||
for task in tasks_to_be_removed:
|
for task in tasks_to_be_removed:
|
||||||
print(
|
print(
|
||||||
f"Found completed task: {
|
f"Found completed task: {
|
||||||
things_handler.full_name(things_task=task)}"
|
things_handler.full_name(things_task=task)}"
|
||||||
)
|
)
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
finish_task(task["uuid"])
|
finish_task(task["uuid"])
|
||||||
|
return len(tasks_to_be_removed)
|
||||||
|
|
||||||
|
|
||||||
@app.command("tracking")
|
@app.command("tracking")
|
||||||
def sync_toggl_reclaim_tracking(since_days : Annotated[int, typer.Argument()] = 0):
|
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
|
toggl_time_entries = toggl_handler.get_time_entries_since(
|
||||||
|
since_days=since_days
|
||||||
|
) # end date is inclusive
|
||||||
if toggl_time_entries is None:
|
if toggl_time_entries is None:
|
||||||
utils.pwarning(f"No tasks tracked in Toggl since {since_days} days")
|
utils.pwarning(f"No tasks tracked in Toggl since {since_days} days")
|
||||||
return
|
return
|
||||||
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(
|
||||||
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)}
|
since_days=since_days
|
||||||
non_existent_time_entries = [time_entry for time_entry in toggl_time_entries if time_entry.description not in reclaim_time_entries_dict.keys()]
|
) # end date is inclusive
|
||||||
|
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
|
# add all existing mismatched_time_entries
|
||||||
time_entries_to_adjust : Dict[toggl_handler.TimeEntry, reclaim_handler.ReclaimTaskEvent] = {}
|
time_entries_to_adjust: Dict[
|
||||||
|
toggl_handler.TimeEntry, reclaim_handler.ReclaimTaskEvent
|
||||||
|
] = {}
|
||||||
for toggl_time_entry in toggl_time_entries:
|
for toggl_time_entry in toggl_time_entries:
|
||||||
if toggl_time_entry.description in reclaim_time_entries_dict.keys():
|
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
|
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:
|
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
|
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:
|
elif nearest_entry is not None:
|
||||||
time_entries_to_adjust[toggl_time_entry] = nearest_entry
|
time_entries_to_adjust[toggl_time_entry] = nearest_entry
|
||||||
|
|
||||||
rprint(non_existent_time_entries)
|
rprint(non_existent_time_entries)
|
||||||
rprint(time_entries_to_adjust)
|
rprint(time_entries_to_adjust)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
if not mismatched_time_entries:
|
|
||||||
utils.pinfo("Toggl and Reclaim time entries are synced.")
|
|
||||||
else:
|
|
||||||
for time_entry in mismatched_time_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
|
|
||||||
|
|
||||||
toggl_start = toggl_handler.get_start_time(time_entry)
|
|
||||||
toggl_stop = toggl_handler.get_stop_time(time_entry)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("current")
|
@app.command("current")
|
||||||
def display_current_task():
|
def display_current_task():
|
||||||
current_task = toggl_handler.get_current_time_entry()
|
current_task = toggl_handler.get_current_time_entry()
|
||||||
if current_task is None:
|
if current_task is None:
|
||||||
utils.perror("No task is currently tracked in toggl")
|
utils.perror("No task is currently tracked in toggl")
|
||||||
return
|
return
|
||||||
rprint(f"Current task: {current_task.description}\nStarted at:",
|
rprint(
|
||||||
f"{toggl_handler.get_start_time(current_task)
|
f"Current task: {current_task.description}\nStarted at:",
|
||||||
.astimezone(tz.gettz()).strftime("%H:%M")}")
|
f"{toggl_handler.get_start_time(current_task)
|
||||||
|
.astimezone(tz.gettz()).strftime("%H:%M")}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("sync")
|
@app.command("sync")
|
||||||
def sync_things_and_reclaim(dry_run : bool = False):
|
def sync_things_and_reclaim(dry_run: bool = False):
|
||||||
"""
|
"""
|
||||||
Sync tasks between things and reclaim
|
Sync tasks between things and reclaim
|
||||||
First updated all finished tasks in reclaim to completed in things
|
First updated all finished tasks in reclaim to completed in things
|
||||||
Then upload all new tasks from things to reclaim
|
Then upload all new tasks from things to reclaim
|
||||||
"""
|
"""
|
||||||
utils.pinfo("Pulling from Reclaim")
|
utils.pinfo("Pulling from Reclaim")
|
||||||
remove_finished_tasks_from_things(dry_run)
|
removed_tasks = remove_finished_tasks_from_things(dry_run)
|
||||||
rprint("---------------------------------------------")
|
rprint("---------------------------------------------")
|
||||||
time.sleep(1) # stop tool from uploading recently finished tasks
|
time_to_wait = 2 * removed_tasks
|
||||||
|
if time_to_wait > 0:
|
||||||
|
rprint(f"Waiting {time_to_wait}s for things to mark tasks as completed")
|
||||||
|
time.sleep(
|
||||||
|
time_to_wait
|
||||||
|
) # wait for os.system call in things_handler.complete to take effect
|
||||||
utils.pinfo("Pushing to Reclaim")
|
utils.pinfo("Pushing to Reclaim")
|
||||||
upload_things_to_reclaim(dry_run)
|
upload_things_to_reclaim(dry_run)
|
||||||
rprint("---------------------------------------------")
|
rprint("---------------------------------------------")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from reclaim_sdk.client import ReclaimClient
|
|||||||
from reclaim_sdk.models.task import ReclaimTask
|
from reclaim_sdk.models.task import ReclaimTask
|
||||||
from reclaim_sdk.models.task_event import ReclaimTaskEvent
|
from reclaim_sdk.models.task_event import ReclaimTaskEvent
|
||||||
|
|
||||||
from deadline_status import DeadlineStatus
|
from deadline_status import DeadlineStatus
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.reclaim.toml"
|
CONFIG_PATH = utils.get_project_root() / "things2reclaim/config/.reclaim.toml"
|
||||||
@@ -21,25 +21,26 @@ with open(CONFIG_PATH, "rb") as f:
|
|||||||
|
|
||||||
RECLAIM_TOKEN = _config["reclaim_ai"]["token"]
|
RECLAIM_TOKEN = _config["reclaim_ai"]["token"]
|
||||||
|
|
||||||
THINGS_ID_PATTERN = "things_task:[a-zA-z0-9]+"
|
THINGS_ID_PATTERN = "things_task:[a-zA-z0-9]+"
|
||||||
|
|
||||||
things_id_pattern : Pattern[str] = re.compile(THINGS_ID_PATTERN)
|
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]:
|
def get_reclaim_task(name: str) -> Optional[ReclaimTask]:
|
||||||
res = ReclaimTask.search(title = name)
|
res = ReclaimTask.search(title=name)
|
||||||
if not res:
|
if not res:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return res[0]
|
return res[0]
|
||||||
|
|
||||||
|
|
||||||
def get_reclaim_tasks() -> List[ReclaimTask]:
|
def get_reclaim_tasks() -> List[ReclaimTask]:
|
||||||
return ReclaimTask.search()
|
return ReclaimTask.search()
|
||||||
|
|
||||||
|
|
||||||
def get_reclaim_task_names(tasks : Optional[List[ReclaimTask]] = None):
|
def get_reclaim_task_names(tasks: Optional[List[ReclaimTask]] = None):
|
||||||
if not tasks:
|
if not tasks:
|
||||||
tasks = get_reclaim_tasks()
|
tasks = get_reclaim_tasks()
|
||||||
return [task.name for task in tasks]
|
return [task.name for task in tasks]
|
||||||
@@ -49,13 +50,19 @@ def filter_for_subject(subject, tasks):
|
|||||||
return [task for task in tasks if task.name.startswith(subject)]
|
return [task for task in tasks if task.name.startswith(subject)]
|
||||||
|
|
||||||
|
|
||||||
def filter_for_deadline_status(cur_date : datetime, deadline_status : DeadlineStatus, tasks : List[ReclaimTask]):
|
def filter_for_deadline_status(
|
||||||
|
cur_date: datetime, deadline_status: DeadlineStatus, tasks: List[ReclaimTask]
|
||||||
|
):
|
||||||
# [task for task in reclaim_tasks if task.due_date >= current_date]
|
# [task for task in reclaim_tasks if task.due_date >= current_date]
|
||||||
match deadline_status:
|
match deadline_status:
|
||||||
case DeadlineStatus.FINE:
|
case DeadlineStatus.FINE:
|
||||||
return [task for task in tasks if (task.due_date and task.due_date >= cur_date)]
|
return [
|
||||||
|
task for task in tasks if (task.due_date and task.due_date >= cur_date)
|
||||||
|
]
|
||||||
case DeadlineStatus.OVERDUE:
|
case DeadlineStatus.OVERDUE:
|
||||||
return [task for task in tasks if (task.due_date and task.due_date < cur_date)]
|
return [
|
||||||
|
task for task in tasks if (task.due_date and task.due_date < cur_date)
|
||||||
|
]
|
||||||
case DeadlineStatus.NONE:
|
case DeadlineStatus.NONE:
|
||||||
return [task for task in tasks if task.due_date is None]
|
return [task for task in tasks if task.due_date is None]
|
||||||
|
|
||||||
@@ -64,19 +71,26 @@ def get_project(task: ReclaimTask):
|
|||||||
return task.name.split(" ")[0]
|
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):
|
def is_task_time_entry(name: str):
|
||||||
decoded_name = emoji.demojize(name)
|
decoded_name = emoji.demojize(name)
|
||||||
# task entries start either with a :thumbs_up: or a :check_mark_button: emoji
|
# 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:")
|
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.tzlocal()).date()
|
date_now = datetime.now(tz.tzlocal()).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)
|
||||||
return [event for event in events if is_task_time_entry(event.name)]
|
return [event for event in events if is_task_time_entry(event.name)]
|
||||||
|
|
||||||
|
|
||||||
def get_events_date_range(from_date: date, to_date: date):
|
def get_events_date_range(from_date: date, to_date: date):
|
||||||
return ReclaimTaskEvent.search(from_date, to_date)
|
return ReclaimTaskEvent.search(from_date, to_date)
|
||||||
|
|
||||||
@@ -122,10 +136,13 @@ def finish_task(task: ReclaimTask):
|
|||||||
def get_things_id(task: ReclaimTask):
|
def get_things_id(task: ReclaimTask):
|
||||||
things_id_match = things_id_pattern.match(task.description)
|
things_id_match = things_id_pattern.match(task.description)
|
||||||
if things_id_match is None:
|
if things_id_match is None:
|
||||||
raise ValueError(f"things id could not be found in description of reclaim task {task.name}")
|
raise ValueError(
|
||||||
things_id_tag : str = things_id_match.group()
|
f"things id could not be found in description of reclaim task {task.name}"
|
||||||
|
)
|
||||||
|
things_id_tag: str = things_id_match.group()
|
||||||
|
|
||||||
return things_id_tag.split(":")[1]
|
return things_id_tag.split(":")[1]
|
||||||
|
|
||||||
def get_reclaim_things_ids() -> List[str]:
|
|
||||||
|
def get_reclaim_things_ids() -> List[str]:
|
||||||
return [get_things_id(task) for task in ReclaimTask.search()]
|
return [get_things_id(task) for task in ReclaimTask.search()]
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ def get_tasks_for_project(project) -> Dict | List[Dict]:
|
|||||||
return things.tasks(project=project["uuid"], type="to-do")
|
return things.tasks(project=project["uuid"], type="to-do")
|
||||||
|
|
||||||
|
|
||||||
def is_equal_fullname(things_task : Dict, name : str):
|
def is_equal_fullname(things_task: Dict, name: str):
|
||||||
return full_name(things_task=things_task) == name
|
return full_name(things_task=things_task) == name
|
||||||
|
|
||||||
|
|
||||||
def get_all_things_tasks() -> List:
|
def get_all_things_tasks() -> List:
|
||||||
|
|||||||
@@ -33,18 +33,19 @@ time_entry_editor = toggl_python.WorkspaceTimeEntries(
|
|||||||
def get_time_entry(time_entry_id: int) -> toggl_python.TimeEntry:
|
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 delete_time_entry(time_entry_id: int) -> bool:
|
def delete_time_entry(time_entry_id: int) -> bool:
|
||||||
return time_entry_editor.delete_timeentry(time_entry_id)
|
return time_entry_editor.delete_timeentry(time_entry_id)
|
||||||
|
|
||||||
def get_start_time(time_entry : toggl_python.TimeEntry):
|
|
||||||
|
def get_start_time(time_entry: toggl_python.TimeEntry):
|
||||||
return time_entry.start() if callable(time_entry.start) else time_entry.start
|
return time_entry.start() if callable(time_entry.start) else time_entry.start
|
||||||
|
|
||||||
|
|
||||||
def get_stop_time(time_entry: toggl_python.TimeEntry):
|
def get_stop_time(time_entry: toggl_python.TimeEntry):
|
||||||
if time_entry.stop is None:
|
if time_entry.stop is None:
|
||||||
return get_start_time(time_entry) + timedelta(seconds=time_entry.duration)
|
raise ValueError(f"TimeEntry {time_entry.description} is still running")
|
||||||
else:
|
return time_entry.stop() if callable(time_entry.stop) else time_entry.stop
|
||||||
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):
|
||||||
@@ -115,11 +116,14 @@ def create_task_time_entry(
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
if start is not None:
|
if start is None:
|
||||||
|
start = datetime.now(tz.tzutc())
|
||||||
|
else:
|
||||||
if start.tzinfo is None:
|
if start.tzinfo is None:
|
||||||
raise ValueError("start has to be timezone aware")
|
raise ValueError("start has to be timezone aware")
|
||||||
start = start.astimezone(tz.tzutc())
|
start = start.astimezone(tz.tzutc())
|
||||||
time_entry.start = start
|
|
||||||
|
time_entry.start = start
|
||||||
|
|
||||||
tag = get_approriate_tag(description)
|
tag = get_approriate_tag(description)
|
||||||
if tag:
|
if tag:
|
||||||
@@ -132,14 +136,21 @@ def start_task(description: str, project: str):
|
|||||||
time_entry_editor.create(create_task_time_entry(description, project))
|
time_entry_editor.create(create_task_time_entry(description, project))
|
||||||
|
|
||||||
|
|
||||||
|
def stop_task(task: TimeEntry) -> TimeEntry | None:
|
||||||
|
if time_entry_editor.DETAIL_URL is None:
|
||||||
|
raise ValueError("DetailURL not set")
|
||||||
|
|
||||||
|
url = time_entry_editor.BASE_URL.join(
|
||||||
|
time_entry_editor.DETAIL_URL.format(id=task.id)
|
||||||
|
)
|
||||||
|
response = time_entry_editor.patch(url)
|
||||||
|
data = response.json()
|
||||||
|
if data:
|
||||||
|
return TimeEntry(**data)
|
||||||
|
|
||||||
|
|
||||||
def stop_current_task():
|
def stop_current_task():
|
||||||
cur_task = get_current_time_entry()
|
cur_task = get_current_time_entry()
|
||||||
if cur_task is None:
|
if cur_task is None:
|
||||||
raise RuntimeError("No time entry is currently running")
|
raise RuntimeError("No time entry is currently running")
|
||||||
if time_entry_editor.DETAIL_URL is None:
|
return stop_task(cur_task)
|
||||||
raise ValueError("DetailURL not set")
|
|
||||||
url = time_entry_editor.BASE_URL.join(
|
|
||||||
time_entry_editor.DETAIL_URL.format(id=cur_task.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return time_entry_editor.patch(url)
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ TIME_PATTERN = (
|
|||||||
)
|
)
|
||||||
pattern = re.compile(TIME_PATTERN)
|
pattern = re.compile(TIME_PATTERN)
|
||||||
|
|
||||||
T = TypeVar("T") # generic type
|
T = TypeVar("T") # generic type
|
||||||
|
|
||||||
|
|
||||||
def calculate_time_on_unit(tag_value: str) -> float:
|
def calculate_time_on_unit(tag_value: str) -> float:
|
||||||
# This is a regex to match time in the format of 1h 30m
|
# This is a regex to match time in the format of 1h 30m
|
||||||
@@ -39,18 +40,22 @@ def calculate_time_on_unit(tag_value: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def get_start_time(toggl_time_entry: TimeEntry):
|
def get_start_time(toggl_time_entry: TimeEntry):
|
||||||
return toggl_time_entry.start() if callable(toggl_time_entry.start) else toggl_time_entry.start
|
return (
|
||||||
|
toggl_time_entry.start()
|
||||||
|
if callable(toggl_time_entry.start)
|
||||||
|
else toggl_time_entry.start
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_stop_time(time_entry: TimeEntry):
|
def get_stop_time(time_entry: TimeEntry):
|
||||||
if time_entry.stop is None:
|
if time_entry.stop is None:
|
||||||
return get_start_time(time_entry) + timedelta(seconds=time_entry.duration)
|
return get_start_time(time_entry) + timedelta(seconds=time_entry.duration)
|
||||||
else:
|
else:
|
||||||
return time_entry.stop() if callable(time_entry.stop) else time_entry.stop
|
return time_entry.stop() if callable(time_entry.stop) else time_entry.stop
|
||||||
|
|
||||||
|
|
||||||
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 map_tag_values(
|
def map_tag_values(
|
||||||
@@ -90,17 +95,24 @@ def get_closest_match(name: str, candidates: Dict[str, T]) -> T | None:
|
|||||||
return candidates[ListPrompt.ask("Select a candidate", possible_candidates)]
|
return candidates[ListPrompt.ask("Select a candidate", possible_candidates)]
|
||||||
|
|
||||||
|
|
||||||
def nearest_time_entry(items: Optional[List[ReclaimTaskEvent]], pivot: TimeEntry) -> Optional[ReclaimTaskEvent]:
|
def nearest_time_entry(
|
||||||
|
items: Optional[List[ReclaimTaskEvent]], pivot: TimeEntry
|
||||||
|
) -> Optional[ReclaimTaskEvent]:
|
||||||
if not items:
|
if not items:
|
||||||
return None
|
return None
|
||||||
return min(items, key=lambda x: abs(x.start - get_start_time(pivot)))
|
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]):
|
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:
|
if toggl_time_entry is None or reclaim_time_entry is None:
|
||||||
print("One is none")
|
print("One is none")
|
||||||
return False
|
return False
|
||||||
if toggl_time_entry.description != get_clean_time_entry_name(reclaim_time_entry.name):
|
if toggl_time_entry.description != get_clean_time_entry_name(
|
||||||
|
reclaim_time_entry.name
|
||||||
|
):
|
||||||
print(f"toggl title: {toggl_time_entry.description}")
|
print(f"toggl title: {toggl_time_entry.description}")
|
||||||
print(f"reclaim title: {get_clean_time_entry_name(reclaim_time_entry.name)}")
|
print(f"reclaim title: {get_clean_time_entry_name(reclaim_time_entry.name)}")
|
||||||
return False
|
return False
|
||||||
@@ -129,6 +141,7 @@ def is_matching_time_entry(toggl_time_entry : Optional[TimeEntry], reclaim_time_
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def pinfo(msg: str):
|
def pinfo(msg: str):
|
||||||
rprint(f"[bold white]{msg}[/bold white]")
|
rprint(f"[bold white]{msg}[/bold white]")
|
||||||
|
|
||||||
@@ -151,10 +164,11 @@ def plogtime(start_time: datetime, end_time: datetime, task_name: str):
|
|||||||
if end_time.tzinfo is None:
|
if end_time.tzinfo is None:
|
||||||
raise ValueError("end_time has to be timezone aware.")
|
raise ValueError("end_time has to be timezone aware.")
|
||||||
|
|
||||||
|
|
||||||
rprint(
|
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}")
|
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}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user