From: Dustin Walde Date: Tue, 12 Sep 2023 17:51:42 +0000 (-0700) Subject: Add errors and query modules X-Git-Url: https://git.walde.dev/?a=commitdiff_plain;h=2d2da9020d290631f786bf9b298a5c503b378045;p=punch Add errors and query modules - Add errors class w/ initial types - Add query module for fetching and printing total times and entries of a category in a time range --- diff --git a/src/errors.py b/src/errors.py index 47e6523..87918e4 100644 --- a/src/errors.py +++ b/src/errors.py @@ -1,6 +1,7 @@ ERR_NO_SUCH_CATEGORY = 1 ERR_MISSING_ARG = 2 ERR_UNKNOWN_COMMAND = 3 +ERR_INVALID_ARG = 4 ERR_ILLEGAL_STATE = 127 class TimeTrackError(Exception): @@ -14,6 +15,11 @@ class IllegalStateError(TimeTrackError): return ERR_ILLEGAL_STATE +class InvalidArgumentError(TimeTrackError): + def exit_code(self) -> int: + return ERR_INVALID_ARG + + class MissingArgumentError(TimeTrackError): def __init__(self, *args: object) -> None: super().__init__( diff --git a/src/query.py b/src/query.py new file mode 100644 index 0000000..27fbdf8 --- /dev/null +++ b/src/query.py @@ -0,0 +1,222 @@ +# Punch(card) — Personal time tracking +# ©2023 Dustin Walde +# SPDX-License-Identifier: GPL-3.0-or-later + +import calendar +from datetime import datetime, timedelta, date, time +import logging +from math import floor + +from typing import List, Optional + +from errors import InvalidArgumentError, NoSuchCategoryError +from puncher import Category, Puncher + + +logger = logging.getLogger(__name__) + + +def query(puncher: Puncher, category: Category, + start_date: date, end_date: date): + result = { + "id": category.abbr, + "name": category.name, + "children": [], + "card": [], + "card-total": timedelta(0), + "total": timedelta(0), + "start": start_date, + "end": end_date, + } + + for child in category.children: + child_q = query(puncher, child, start_date, end_date) + result["children"].append(child_q) + result["total"] += child_q["total"] + + entries = puncher.get_entries(category.abbr, start_date, end_date) + if len(entries) == 0: + return result + + start_time = datetime.combine(start_date, time(), entries[0].t_in.tzinfo) + end_time = datetime.combine(end_date, time(), entries[0].t_in.tzinfo) + for entry in entries: + in_time = max(start_time, entry.t_in) + if entry.is_complete and entry.t_out is not None: + out_time = entry.t_out + else: + out_time = end_time + result["card"].append(entry) + result["card-total"] += out_time - in_time + + result["total"] += result["card-total"] + + return result + + +def process_args(query_args: List[str]): + today = datetime.today() + if len(query_args) == 0: + print("Today is:\n", today.date().strftime('%d %B %Y'), "\n") + calendar.prmonth(today.year, today.month) + return + + cat_id = query_args[0] + options = set() + + snap_to_week = False + set_start = False + set_end = False + duration = None + offset: Optional[timedelta] = None + end = None + count = 1 + unit = 'day' + parsing_time = timedelta(0) + + def set_current(): + nonlocal duration, end, offset, parsing_time, set_end, set_start + if parsing_time.total_seconds() == 0: + return + + if set_start: + offset = -parsing_time + elif set_end: + end = -parsing_time + elif offset is None: + offset = -parsing_time + elif duration is None: + duration = parsing_time - offset + + parsing_time = timedelta(0) + set_start = False + set_end = False + + for arg in query_args[1:]: + if arg.startswith('--'): + options.add(arg[2:]) + elif arg == 'today': + set_current() + count = 1 + parsing_time = timedelta(seconds=1) + elif arg == 'yesterday': + set_current() + count = 1 + parsing_time = timedelta(days=-1) + elif arg == 'last': + set_current() + count = 1 + if not set_end: + set_start = True + elif arg == 'week' or arg == 'weeks': + unit = 'week' + snap_to_week = True + parsing_time += timedelta(days=7*count) + elif arg.startswith('day'): + parsing_time += timedelta(days=count) + elif arg == 'ago': + if not set_end: + set_start = True + elif arg == 'from': + set_current() + set_start = True + elif arg == 'to': + set_current() + set_end = True + else: + set_current() + try: + count = int(arg) + except: + raise InvalidArgumentError("Ivalid command line argument " + arg) + + set_current() + + if offset is None: + offset = timedelta(0) + + if snap_to_week: + start_date = (today + offset).date() + offset += timedelta(days=-start_date.weekday()) + + if end is None: + if duration is None: + unit_offset = 0 + if unit == 'week': + unit_offset = 6 + end = offset + timedelta(days=unit_offset) + else: + end = offset + duration + + return (cat_id, (today+offset).date(), (today+end).date(), options) + + +def print_query(puncher: Puncher, query_args: List[str]): + args = process_args(query_args) + if args is None: + return + cat, start_date, end_date, options = args + category = puncher.get_category(cat) + + if category is None: + raise NoSuchCategoryError(cat) + + res = query(puncher, category, start_date, end_date) + if res is None: + return + + print(f"Querying {category.name} from:", res["start"], "to:", res["end"]) + + _print_tree(res, 0, "full" in options) + + +def _delta_str(delta: timedelta) -> str: + seconds = delta.total_seconds() + if seconds == 0: + return "" + minutes = floor(seconds / 60) + hours = floor(minutes / 60) + if hours == 0: + return f" {minutes%60:2d}m" + if minutes % 60 == 0: + return f"{hours}h -" + return f"{hours:02d}h {minutes%60:2d}m" + + +def _print_tree(res, depth: int, print_entries: bool): + # TODO: currently based off of punch in time only + # I might want and option to separate out by day when + # and entry spans through midnight. + lead = " "*depth*2 + print(lead + res["name"], f'({res["id"]})', _delta_str(res["total"])) + if print_entries: + last_date = None + date_count = 0 + day_total = timedelta() + for entry in res["card"]: + start_date = entry.t_in.date() + if start_date != last_date: + if date_count > 1: + print(lead + " " + " "*15 + _delta_str(day_total)) + print(lead + " ", start_date) + last_date = start_date + date_count = 1 + day_total = timedelta() + else: + date_count += 1 + start_time = entry.t_in + start_time_fmt = entry.t_in.strftime('%H:%M') + end_time = entry.t_out + if end_time is None: + end_time = datetime.now() + end_time_fmt = end_time.strftime('%H:%M') + day_total += (end_time - start_time) + print(lead + " ", start_time_fmt, "-", end_time_fmt, "", _delta_str(end_time - entry.t_in)) + if date_count > 1: + print(lead + " " + " "*15 + _delta_str(day_total)) + if len(res["children"]) > 0: + print(lead + "↑", _delta_str(res["card-total"])) + + for child in res["children"]: + _print_tree(child, depth+1, print_entries) +