]> git.walde.dev - punch/commitdiff
Add errors and query modules
authorDustin Walde <redacted>
Tue, 12 Sep 2023 17:51:42 +0000 (10:51 -0700)
committerDustin Walde <redacted>
Tue, 12 Sep 2023 17:51:42 +0000 (10:51 -0700)
- Add errors class w/ initial types
- Add query module for fetching and printing
  total times and entries of a category in a time range

src/errors.py
src/query.py [new file with mode: 0644]

index 47e6523bd747210e83ce461a1302cc2a76d05dca..87918e4e0e7f0a201aa0e5302bf480bba4812d67 100644 (file)
@@ -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 (file)
index 0000000..27fbdf8
--- /dev/null
@@ -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)
+