]> git.walde.dev - punch/commitdiff
Clean up interface for query
authorDustin Walde <redacted>
Tue, 12 Sep 2023 18:01:11 +0000 (11:01 -0700)
committerDustin Walde <redacted>
Tue, 12 Sep 2023 18:01:11 +0000 (11:01 -0700)
- 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

src/category.py
src/punch.py
src/puncher.py
src/time_sheet.py

index 9c83d9011ab87b4ef60ea65f81754d36639fbc4c..efe69d9d6b3aaaaf94f1fd8ad7da336690269841 100644 (file)
@@ -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]]
index 3a7448a942dac6b217b1e5bd30a2adb015ac2f65..e19558f715376886b48c0879ef7563987b6e8ffc 100644 (file)
@@ -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 <command> <...args>")
+        print("Available commands: daemon, help, in, out, pop, query, service, to, undo")
+        print("List additional command info with: punch help <command>")
+    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] <id> <name> [<parent> [<color>]]')
-            exit(ERR_MISSING_ARG)
-        if args[2] == 'group':
-            if len(args) < 5:
-                print('Missing args for new group: new group <id> <name> [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 <id> <name> [parent] [color]')
-                exit(ERR_MISSING_ARG)
-            sheet.new_category(*args[2:])
+            type, args = ('category', args[2:])
+        if len(args) < 2:
+            raise MissingArgumentError(f"{type} <id> <name> [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: <category>')
-            exit(ERR_MISSING_ARG)
+            raise MissingArgumentError("in <category>")
         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]}: <category>')
-            exit(ERR_MISSING_ARG)
-        sheet.list_category(args[2])
+            raise MissingArgumentError(f"pop {args[2]} <category>")
+        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 <category>")
         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)
 
index 4d1ff2742c64c5532708cf423196e8fb1a035bf5..33fb93b7500fa6fc68c906cb1a880534d5fd5719 100644 (file)
@@ -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
 
 
index a95de22d63a150c7307d220dc436ab3e4365fb1a..292fa3ab66829dd5924f4ed7e2e2503c6899f4af 100644 (file)
@@ -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]