From: Dustin Walde Date: Tue, 12 Sep 2023 18:01:11 +0000 (-0700) Subject: Clean up interface for query X-Git-Url: https://git.walde.dev/?a=commitdiff_plain;h=0a56c0e7b9a272988f47f69765950c19b9b49f86;p=punch Clean up interface for query - Add Puncher get_category, get_entries, and get_last_entry - TimeSheet impl new Puncher methods, remove printing - punch.py add printing for CLI from TimeSheet, add query command - Category allow punchable categories to have sub-categories --- diff --git a/src/category.py b/src/category.py index 9c83d90..efe69d9 100644 --- a/src/category.py +++ b/src/category.py @@ -7,18 +7,23 @@ class Category: def __init__(self, abbr: str, name: str, color: str = '#888', + group: bool = False, parent: Optional['Category'] = None, children: Optional[List['Category']] = None) -> None: self.abbr = abbr self.name = name self.color = color + self.group = group self.parent = parent - self.children = children + if children is None: + self.children = [] + else: + self.children = children @property def is_group(self) -> bool: - return self.children is not None + return self.group def as_rows(self) -> List[List[str]]: @@ -29,7 +34,7 @@ class Category: color = self.color if color is None: color = "" - if self.children is None: + if not self.group: return [[self.abbr, "category", self.name, parent, color]] else: return [["", "group", self.abbr, self.name, parent, color]] diff --git a/src/punch.py b/src/punch.py index 3a7448a..e19558f 100644 --- a/src/punch.py +++ b/src/punch.py @@ -4,19 +4,80 @@ from datetime import date, datetime, time, timedelta import sys -from typing import List, Optional +from typing import List, Optional, Tuple +from category import Category +from errors import ( + IllegalStateError, + MissingArgumentError, + NoSuchCategoryError, + TimeTrackError, + UnknownCommandError, + ) from puncher import Puncher +from query import print_query from time_sheet import TimeSheet import logging logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) +logger = logging.getLogger(__name__) -ERR_MISSING_ARG = 2 -ERR_UNKNOWN_COMMAND = 3 +def print_usage(command: Optional[str] = None): + if command is None: + print("Punch usage:") + print("punch <...args>") + print("Available commands: daemon, help, in, out, pop, query, service, to, undo") + print("List additional command info with: punch help ") + else: + print("No usage info for", command) + + +def list_category(puncher: Puncher, cat_id: str) -> None: + category = puncher.get_category(cat_id) + + if category is None: + raise NoSuchCategoryError(cat_id) + + entry = puncher.get_last_entry(cat_id) + if entry is None: + print(f"No entries yet for {category.name}") + return + if entry.is_complete: + side, time = ("out", entry.t_out) + else: + side, time = ("in", entry.t_in) + + if time is None: + raise IllegalStateError("Last entry missing timestamp.") + else: + print(f"{cat_id}:\t{category.name} punched {side} at {time.isoformat()}") + + +def list_all(puncher: Puncher) -> None: + root = puncher.get_category("") + if root is None: + raise IllegalStateError("Cannot get root category!") + + stack: List[Tuple[Category, int]] = [(root, -1)] + while len(stack) > 0: + category, depth = stack.pop() + if depth >= 0: + print("\t"*depth, end='') + if category.is_group: + print('> ' + category.name) + else: + last_entry = puncher.get_last_entry(category.abbr) + if last_entry is None or last_entry.is_complete: + print('| ' + category.abbr + "\t" + category.name) + else: + print('| ' + category.abbr + "\t" + category.name + + f"\t punched in at {last_entry.t_in}") + if category.children is not None: + for child in reversed(category.children): + stack.append((child, depth+1)) def load_time_sheet() -> TimeSheet: @@ -48,8 +109,7 @@ def punch(sheet: TimeSheet, dir: str, category: Optional[str] = None, args: List def main(args): if len(args) < 2: - print('Missing command (daemon, new, in, out, list, print, query)') - exit(ERR_MISSING_ARG) + raise MissingArgumentError("command (daemon, new, in, out, list, print, query)") if args[1] == 'daemon' or args[1] == 'service': import service @@ -59,19 +119,17 @@ def main(args): if args[1] == 'new': if len(args) < 3: - print("Missing arg group|category") - print('punch new [group] [ []]') - exit(ERR_MISSING_ARG) - if args[2] == 'group': - if len(args) < 5: - print('Missing args for new group: new group [parent] [color]') - exit(ERR_MISSING_ARG) - sheet.new_group(*args[3:]) + raise MissingArgumentError("group|category") + is_group = args[2] == 'group' + if is_group: + type, args = ('group', args[3:]) else: - if len(args) < 4: - print('Missing args for new category: new [parent] [color]') - exit(ERR_MISSING_ARG) - sheet.new_category(*args[2:]) + type, args = ('category', args[2:]) + if len(args) < 2: + raise MissingArgumentError(f"{type} [parent] [color]") + cat = sheet.new_category(*args) + if cat is not None: + logger.info(f"Added category {cat.name} ({cat.abbr})") elif args[1] == 'out': if len(args) < 3: @@ -81,30 +139,27 @@ def main(args): elif args[1] == 'in': if len(args) < 3: - print(f'Missing arg for punch in: ') - exit(ERR_MISSING_ARG) + raise MissingArgumentError("in ") punch(sheet, 'in', args[2], args[3:]) - elif args[1] == 'pop' or args[1] == 'rm' or args[1] == 'undo': + elif args[1] == 'pop' or args[1] == 'undo': if len(args) < 3: - print(f'Missing arg for pop {args[2]}: ') - exit(ERR_MISSING_ARG) - sheet.list_category(args[2]) + raise MissingArgumentError(f"pop {args[2]} ") + list_category(sheet, args[2]) resp = input("Remove entry [y/N]? ") if resp.lower() == 'y': sheet.pop(args[2]) - sheet.list_category(args[2]) + list_category(sheet, args[2]) elif args[1] == 'list': if len(args) > 2: - sheet.list_category(args[2]) + list_category(sheet, args[2]) else: - sheet.list_all() + list_all(sheet) elif args[1] == 'to': if len(args) < 3: - print(f'Missing category to punch in to.') - exit(ERR_MISSING_ARG) + raise MissingArgumentError(f"to ") punch(sheet, 'out') punch(sheet, 'in', args[2]) @@ -114,19 +169,27 @@ def main(args): dt = date.fromisoformat(args[2]) else: dt = date.today() - #dt -= timedelta(days=1) - #image = image_generator.week(sheet, dt) image = image_generator.full_week(sheet, dt) + #image = image_generator.week(sheet, dt) image.save("test.png") elif args[1] == 'query': - raise NotImplementedError + print_query(sheet, args[2:]) + + elif args[1] == 'help': + print_usage(*args[2:3]) else: - print(f"Unknown subcommand {args[1]}") - exit(ERR_UNKNOWN_COMMAND) + raise UnknownCommandError(args[1]) if __name__ == "__main__": - main(sys.argv) + try: + main(sys.argv) + except TimeTrackError as e: + command = None + if len(sys.argv) > 1: + command = sys.argv[1] + print_usage(command) + logger.error(e) diff --git a/src/puncher.py b/src/puncher.py index 4d1ff27..33fb93b 100644 --- a/src/puncher.py +++ b/src/puncher.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from abc import ABC, abstractmethod -from datetime import datetime -from typing import List, Optional +from datetime import date, datetime +from typing import List, Optional, Union from category import Category from entry import Entry @@ -13,12 +13,20 @@ from entry import Entry class Puncher(ABC): @abstractmethod - def list_all(self) -> None: + def get_category(self, abbr: str) -> Optional[Category]: return NotImplemented @abstractmethod - def list_category(self, category: str) -> None: + def get_entries(self, cat_id: str, + start_time: Optional[Union[date,datetime]], + end_time: Optional[Union[date,datetime]], + ) -> List[Entry]: + return NotImplemented + + + @abstractmethod + def get_last_entry(self, cat_id: str) -> Optional[Entry]: return NotImplemented diff --git a/src/time_sheet.py b/src/time_sheet.py index a95de22..292fa3a 100644 --- a/src/time_sheet.py +++ b/src/time_sheet.py @@ -3,11 +3,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later import csv -from datetime import datetime +from datetime import date, datetime, time from io import TextIOWrapper import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Union from category import Category from entry import Entry @@ -29,6 +29,7 @@ class TimeSheet(Puncher): self.root = Category( abbr = '', name = 'Root', + group = True, parent = None, children = [], ) @@ -45,54 +46,40 @@ class TimeSheet(Puncher): return entries - def list_category(self, category: str) -> None: - """override""" - if category not in self.cat_entries: - logger.info("%s:\tCategory doesn't exist.", category) - return + def get_category(self, abbr: str) -> Optional[Category]: + return self.categories.get(abbr, None) - entries = self.cat_entries[category] - cat = self.categories[category] - if len(entries) == 0: - logger.info("%s:\tNo entries for %s", category, cat.name) - return - entry = self.entries[entries[-1]] - if entry is None: - logger.error("Last entry is empty") - return - if entry.is_complete: - side = "out" - time = entry.t_out - else: - side = "in" - time = entry.t_in - logger.info("%s:\t%s punched %s at %s", - category, cat.name, side, time.isoformat()) + def get_entries(self, cat_id: str, start_time: Optional[Union[date, datetime]], end_time: Optional[Union[date, datetime]]) -> List[Entry]: + entries = [] + all_entries = self.cat_entries.get(cat_id, None) + if all_entries is None or len(all_entries) == 0: + return entries + + tzi = self.entries[all_entries[0]].t_in.tzinfo + if type(start_time) is date: + start_time = datetime.combine(start_time, time(), tzinfo=tzi) + if type(end_time) is date: + end_time = datetime.combine(end_time, time(), tzinfo=tzi) + + for entry_idx in all_entries: + entry = self.entries[entry_idx] + if entry is None: + continue + if (end_time is not None and entry.t_in >= end_time) or \ + (start_time is not None and entry.is_complete and entry.t_out <= start_time): + continue + entries.append(entry) + return entries - def list_all(self) -> None: - """override""" - stack: List[Tuple[Category, int]] = [(self.root, -1)] - while len(stack) > 0: - category, depth = stack.pop() - if depth >= 0: - print("\t"*depth, end='') - text = "" - if category.is_group: - print(category.name) - else: - entries = self.cat_entries.get(category.abbr, []) - if len(entries) == 0: - print(category.abbr + ":\t" + category.name) - else: - last = self.entries[entries[-1]] - if last is not None and not last.is_complete: - text = f"\t punched in at {last.t_in}" - print(category.abbr + ":\t" + category.name + " " + text) - if category.children is not None: - for child in reversed(category.children): - stack.append((child, depth+1)) + + def get_last_entry(self, cat_id: str) -> Optional[Entry]: + entries = self.cat_entries[cat_id] + if len(entries) == 0: + return None + + return self.entries[entries[-1]] def load_csv(self, filepath: str) -> None: @@ -246,7 +233,7 @@ class TimeSheet(Puncher): self.categories[row[2]] = Category( abbr = row[2], name = row[3], - children = [], + group = True, ) if len(row) > 5: self.categories[row[2]].color = row[5]