--- /dev/null
+# ©2023 Dustin Walde — GPL-3.0-or-later
+
+from typing import Optional, List
+
+
+class Category:
+
+ def __init__(self, abbr: str, name: str,
+ color: str = '#888',
+ parent: Optional[str] = None,
+ children: Optional[List[str]] = None) -> None:
+ self.abbr = abbr
+ self.name = name
+ self.color = color
+ self.parent = parent
+ self.children = children
+
+
+ @property
+ def is_group(self) -> bool:
+ return self.children is not None
+
+
+ def as_rows(self) -> List[List[str]]:
+ parent = self.parent
+ if parent is None:
+ parent = ""
+ if self.children is None:
+ color = self.color
+ if color is None:
+ color = ""
+ return [[self.abbr, "category", self.name, parent, color]]
+ else:
+ return [["", "group", self.abbr, self.name, parent]]
+
--- /dev/null
+# ©2023 Dustin Walde — GPL-3.0-or-later
+
+from datetime import datetime, timedelta
+from typing import List, Optional
+
+
+class Entry:
+
+ def __init__(self, category: str, t_in: datetime, t_out: Optional[datetime]) -> None:
+ self.category = category
+ self.t_in = t_in
+ self.t_out = t_out
+
+
+ def as_rows(self) -> List[List[str]]:
+ rows = []
+ rows.append([self.category, "in", self.t_in.isoformat()])
+ if self.t_out is not None:
+ rows.append([self.category, "out", self.t_out.isoformat()])
+ return rows
+
+
+ def duration(self, use_now: bool = False) -> timedelta:
+ out = self.t_out
+ if out is None and use_now:
+ out = datetime.now()
+ if out is None:
+ return timedelta(0)
+ return out - self.t_in
+