Compare commits

...

11 Commits

8 changed files with 221 additions and 16 deletions

62
main.py
View File

@@ -1,12 +1,62 @@
#!/usr/bin/python3
from datetime import datetime, timedelta
import argparse
from util.str_to_datetime import str_to_datetime
parser = argparse.ArgumentParser(description='Time tracker')
parser.add_argument('command', type=str, help='Manage time slots.', choices=['start', 'stop', 'add', 'ps', 'ls'])
parser.add_argument('-b', '--begin', type=str, help="The start time of a time slot.")
parser.add_argument('-e', '--end', type=str, help="The end time of a time slot.")
from model.TimeSlotContainer import time_slot_container as tsc
from model.TimeSlot import TimeSlot
args = parser.parse_args()
from util.views import convert_to_table
from util.parser import create_parser
from util import timeslotlist
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
if args.command in ["ls", "acc"]:
match args.span:
case "w":
start_of_week = datetime.now() - timedelta(days=datetime.now().weekday())
start_of_week_midnight = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
slots = tsc.get_time_slots_by_date(start=start_of_week_midnight)
case "lw":
start_of_week = datetime.now() - timedelta(days=datetime.now().weekday())
start_of_week_midnight = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
slots = tsc.get_time_slots_by_date(
start=start_of_week_midnight - timedelta(days=7),
end=start_of_week_midnight,
)
case "d":
last_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
slots = tsc.get_time_slots_by_date(start=last_midnight)
case _:
slots = tsc.get_all_time_slots()
if args.command == "ls":
print(convert_to_table(slots))
elif args.command == "start":
if args.start is not None:
tsc.open_time_slot(args.name, str_to_datetime(args.start))
else:
tsc.open_time_slot(args.name)
print(f"Started event {args.name}.")
elif args.command == "end":
if args.end is not None:
tsc.close_time_slot(str_to_datetime(args.end))
else:
tsc.close_time_slot()
print("Ended event.")
elif args.command == "ps":
print(convert_to_table(tsc.get_open_time_slots()))
elif args.command == "add":
tsc.add_time_slot(args.name, str_to_datetime(args.start), str_to_datetime(args.end))
elif args.command == "acc":
d = timeslotlist.accumulate_duration(timeslotlist.filter(slots, args.query))
print(f"{d.seconds // 3600}:{d.seconds % 3600 // 60:02}")

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from .TimeSlot import TimeSlot
from model.TimeSlot import TimeSlot
import json
@@ -7,23 +7,32 @@ class DataStore:
def __init__(self, filename: Path, create=False):
self._filename = filename
if create:
# Store emtpty list
self._time_slots = []
self._write_update()
self.write_update()
else:
# Load dicts from file
with open(self._filename, "r") as f:
self._time_slots = list(json.load(f))
def _write_update(self):
# Convert dicts into TimeSlot objects
def deserialize(d: dict) -> TimeSlot:
ts = TimeSlot.__new__(TimeSlot) # Create a new, empty instance
ts.__dict__.update(d)
return ts
self._time_slots = [deserialize(d) for d in self._time_slots]
def write_update(self):
"""Saves the updates made to the DataStore object to disk."""
with open(self._filename, "w+") as f:
json.dump(self._time_slots, f, default=vars)
def add_time_slot(self, time_slot: TimeSlot):
self._time_slots.append(time_slot)
self._write_update()
def remove_time_slot(self, time_slot: TimeSlot):
self._time_slots.remove(time_slot)
self._write_update()
def get_all_time_slots(self):
def get_all_time_slots(self) -> list[TimeSlot]:
return self._time_slots[:]

View File

@@ -24,14 +24,26 @@ class TimeSlot():
@start.setter
def start(self, d: datetime):
self._start = d.timestamp()
_d = d.replace(second=0, microsecond=0)
self._start = _d.timestamp()
@property
def end(self):
return datetime.fromtimestamp(self._end)
if "_end" in self.__dict__:
return datetime.fromtimestamp(self._end)
return None
@end.setter
def end(self, d: datetime):
if d < self.start:
_d = d.replace(second=0, microsecond=0)
if _d < self.start:
raise ValueError("End date must be after the start date.")
self._end = d.timestamp()
self._end = _d.timestamp()
def end_now(self):
self.end = datetime.now()
def duration(self) -> datetime.timedelta:
end = self.end
if not end: end = datetime.now()
return end - self.start

View File

@@ -0,0 +1,51 @@
from datetime import date, datetime, timedelta
from model.DataStore import DataStore
from model.TimeSlot import TimeSlot
class TimeSlotContainer:
def __init__(self, ds: DataStore):
self._ds = ds
def get_open_time_slots(self) -> list[TimeSlot]:
return [ts for ts in self._ds.get_all_time_slots() if ts.end is None]
def get_all_time_slots(self) -> list[TimeSlot]:
return self._ds.get_all_time_slots()
def get_time_slots_by_date(self, start: datetime, end: datetime = datetime.now()):
# The selection will be oriented at the start date
return [ts for ts in self._ds.get_all_time_slots()
if ts.start >= start and ts.start <= end]
def open_time_slot(self, name: str, start_dt: datetime = datetime.now()):
if len(self.get_open_time_slots()) > 0:
raise Exception("Ein event ist bereits aktiv.")
ts = TimeSlot(name)
ts.start = start_dt
self._ds.add_time_slot(ts)
self._ds.write_update()
def close_time_slot(self, end_dt: datetime = datetime.now()):
ts = self.get_open_time_slots()[0]
self._ds.remove_time_slot(ts)
ts.end = end_dt
self._ds.add_time_slot(ts)
self._ds.write_update()
def add_time_slot(self, name: str, start_dt: datetime, end_dt: datetime):
self.open_time_slot(name, start_dt)
self.close_time_slot(end_dt)
def update_time_slot(self, old: TimeSlot, new: TimeSlot):
raise NotImplementedError
def delete_time_slot(self, ts: TimeSlot):
raise NotImplementedError
try:
time_slot_container = TimeSlotContainer(DataStore("data.json"))
except FileNotFoundError:
time_slot_container = TimeSlotContainer(DataStore("data.json", create=True))

36
util/parser.py Normal file
View File

@@ -0,0 +1,36 @@
import argparse
def create_parser():
# Create the top-level parser
parser = argparse.ArgumentParser(description="Time tracker.")
subparsers = parser.add_subparsers(dest="command", help="Sub-command help")
# Create the parser for the "ls" command
parser_ls = subparsers.add_parser("ls", help="List events.")
parser_ls.add_argument("-s", "--span", choices=['d', 'w', 'lw'], help="Display only envent in a certain time span (d = current day; w = current week; lw = last week)")
# Create the parser for the "ps" command
_ = subparsers.add_parser("ps", help="Show the currently running event.")
# Create the parser for the "add" command
parser_add = subparsers.add_parser("add", help="Adding a new event.")
parser_add.add_argument("name", help="Name of the event")
parser_add.add_argument("-s", "--start", required=True, help="Start datetime (default now)")
parser_add.add_argument("-e", "--end", required=True, help="End datetime")
# Create the parser for the "start" command
parser_start = subparsers.add_parser("start", help="Starting a new event.")
parser_start.add_argument("name", help="Name of the event")
parser_start.add_argument("-s", "--start", help="Start datetime (default now)")
# Create the parser for the "end" command
parser_end = subparsers.add_parser("end", help="Ending the current event.")
parser_end.add_argument("-e", "--end", help="End datetime")
# Create the parser for the "acc" command
parser_end = subparsers.add_parser("acc", help="Accumulate the duration of events.")
parser_end.add_argument("query", help="The regex matching for the name of the event.")
parser_end.add_argument("-s", "--span", choices=['d', 'w', 'lw'], help="Display only envent in a certain time span (d = current day; w = current week; lw = last week)")
return parser

19
util/str_to_datetime.py Normal file
View File

@@ -0,0 +1,19 @@
from datetime import datetime as dt
def str_to_datetime(s: str) -> dt:
s = s.strip()
try:
return dt.strptime(s, '%d.%m.%y %H:%M')
except ValueError:
pass
try:
tim = dt.strptime(s, '%H:%M').time()
dat = dt.now().date()
return dt.combine(dat, tim)
except ValueError:
pass
raise ValueError("The given string can not be interpreted as a datetime.")

12
util/timeslotlist.py Normal file
View File

@@ -0,0 +1,12 @@
import re
from datetime import timedelta
from model.TimeSlot import TimeSlot
def accumulate_duration(timeslots: list[TimeSlot]) -> timedelta:
return sum([ts.duration() for ts in timeslots], start=timedelta(0))
def filter(timeslots: list[TimeSlot], query: str) -> list[TimeSlot]:
pattern = re.compile(query)
return [ts for ts in timeslots if pattern.search(ts.name)]

16
util/views.py Normal file
View File

@@ -0,0 +1,16 @@
from model.TimeSlot import TimeSlot
def convert_to_table(time_slot_list: list[TimeSlot]) -> str:
widths = [30, 26, 26]
r_str = f"+{'-'*(widths[0]+2)}+{'-'*(widths[1]+2)}+{'-'*(widths[2]+2)}+\n"
r_str += f"| {'Name':<30} | {'Startzeitpunkt':<26} | {'Endzeitpunkt':<26} |\n"
r_str += f"+{'-'*(widths[0]+2)}+{'-'*(widths[1]+2)}+{'-'*(widths[2]+2)}+\n"
for time_slot in time_slot_list:
r_str += f"| {time_slot.name:<30} | {str(time_slot.start):<26} | {str(time_slot.end):<26} |\n"
r_str += f"+{'-'*(widths[0]+2)}+{'-'*(widths[1]+2)}+{'-'*(widths[2]+2)}+"
return r_str