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:
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
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:
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])
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)
# 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
self.root = Category(
abbr = '',
name = 'Root',
+ group = True,
parent = None,
children = [],
)
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:
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]