diff --git a/daily-todo.py b/daily-todo.py new file mode 100644 index 0000000..efb7239 --- /dev/null +++ b/daily-todo.py @@ -0,0 +1,168 @@ +import logging +import os +import sys +from datetime import date, datetime, timedelta +from random import randint + +from caldav import DAVClient +from dotenv import find_dotenv, load_dotenv + +from printer import p + +logging.basicConfig( + format="{asctime} {levelname:8} {name}: {message}", + datefmt="%Y-%m-%d %H:%M:%S", + style="{", + stream=sys.stdout, + force=True, +) + +log = logging.getLogger("todo-receipt") +log.setLevel(logging.DEBUG) + +if load_dotenv(find_dotenv(usecwd=True)): + log.debug("Loaded .env") +else: + log.debug("Didn't find .env") + +CALDAV_URL = os.getenv("CALDAV_URL") +CALDAV_USERNAME = os.getenv("CALDAV_USERNAME") +CALDAV_PASSWORD = os.getenv("CALDAV_PASSWORD") + +day = timedelta(days=1) +week = timedelta(days=7) +today = date.today() +yesterday = today - day +tomorrow = today + day +next_week = today + week + + +def coerce_date(datetime): + try: + return datetime.date() + except: + return datetime + + +def shuffle_with_priority(tasks): + return sorted( + tasks, + key=lambda task: ( + task.icalendar_component.get("priority", 5), + randint(0, 10), + ), + ) + + +with DAVClient( + url=CALDAV_URL, username=CALDAV_USERNAME, password=CALDAV_PASSWORD +) as client: + log.debug("Loading calendars") + principal = client.principal() + calendars = principal.calendars() + + events = [] + todos = [] + + for c in calendars: + log.debug(f"Fetching from {c.name}") + if c.name not in ["Schedule"]: + log.debug("Fetching events and todos") + events.extend(c.events()) + todos.extend(c.todos()) + else: + log.debug("Finding cancelled classes") + for event in c.events(): + if event.icalendar_component.get("status") == "CANCELED": + log.debug( + f"Found cancelled class {event.icalendar_component.get('summary')}" + ) + events.append(event) + + must_do = [] + should_do = [] + want_to_do = [] + + log.debug("Processing todos") + for task in todos: + due = task.get_due() + if due is None: + want_to_do.append(task) + continue + + task.expand_rrule(yesterday, next_week) + due = coerce_date(due) + + start = coerce_date(task.icalendar_component.get("dtstart").dt or yesterday) + + if today < start: + continue + + if due < tomorrow: + must_do.append(task) + elif due < next_week: + should_do.append(task) + else: + want_to_do.append(task) + + scheduled = [] + + log.debug("Processing events") + for event in events: + event.expand_rrule(yesterday, next_week) + start_day = coerce_date(event.icalendar_component.get("dtstart").dt) + end_day = coerce_date(event.icalendar_component.get("dtend").dt) + + if end_day < today or today < start_day: + continue + else: + scheduled.append(event) + + should_do = shuffle_with_priority(should_do) + want_to_do = shuffle_with_priority(want_to_do) + + while len(must_do) < 2: + if should_do: + must_do.append(should_do.pop(0)) + elif want_to_do: + must_do.append(want_to_do.pop(0)) + else: + break # No remaining tasks + + while len(should_do) < 3: + if want_to_do: + should_do.append(want_to_do.pop(0)) + else: + break # No remaining tasks + + categories = { + "Must do": must_do, + "Should do": should_do[:3], + "Want to do": want_to_do[:3], + } + +with p: + log.debug("Printing output") + p.title(" TODO List ", size=4) + p.subtitle(datetime.today().strftime("%Y-%m-%d")) + p.ln() + + if scheduled: + p.textln("Scheduled:") + for event in scheduled: + summary = event.icalendar_component["summary"] + if event.icalendar_component.get("status") == "CANCELED": + summary += " (CANCELED)" + start = event.icalendar_component.get("dtstart").dt + p.textln(f"[ ] {summary} @ {start.strftime('%H:%M')}") + else: + p.textln("Scheduled: (None)") + + for k, v in categories.items(): + p.ln() + if v: + p.textln(f"{k}:") + for task in v: + p.textln(f"[ ] {task.icalendar_component['summary']}") + else: + p.textln(f"{k}: (None)")