#!/usr/bin/python3 import logging import argparse import os.path from typing import Iterable from pathlib import Path import yaml from datetime import timedelta from icalendar import Calendar, Event def get_parser(): p = argparse.ArgumentParser() p.add_argument("files", nargs="+", type=str) p.add_argument( "--out-md-dir", help="Output directory for markdown files", default="content/talks/", type=Path, ) p.add_argument( "--out-schedule", help="Output schedule.yml file", default="data/schedule.yml", type=Path, ) p.add_argument("--trust-location", type=str, nargs="*", default=[]) p.add_argument( "--slot-size", type=int, metavar="MINUTES", default=15, help="Round times to the nearest */MINUTES", ) p.add_argument("--night-threshold", metavar="HOUR", default=5, type=int) p.add_argument("--mode", choices=["hugo"], default="hugo") return p def round_down(num, divisor): """ >>> round_down(1000, 10) 1000 >>> round_down(1001, 10) 1000 >>> round_down(1009, 10) 1000 """ return num - (num % divisor) def round_down_time(hhmm: str, divisor: int): hh = hhmm[:-2] mm = hhmm[-2:] mm = round_down(int(mm, base=10), divisor) return int('%s%02d' % (hh,mm) , base=10) class Converter: """ This class takes care of everything converter-related. Objects are used to enable multiple output formats to be added pretty easily by subclassing """ def __init__(self, args): self.args = args self.rooms = [] self.talks = {} self.talk_room = {} # map talk uid to room name self.talk_location = {} # same, but see --trust-location self.changed_files = [] def _fname_to_room(self, fpath: str) -> str: return os.path.splitext(os.path.basename(fpath))[0] def get_vevents_from_calendar(self, cal: Calendar) -> Iterable[Event]: for subc in cal.subcomponents: if type(subc) is Event: yield subc def load_input(self): for fpath in self.args.files: room = self._fname_to_room(fpath) with open(fpath) as buf: file_content = buf.read() cal = Calendar.from_ical(file_content, multiple=True) for subcal in cal: for ev in self.get_vevents_from_calendar(subcal): uid = ev.decoded("uid").decode("ascii") self.talks[uid] = ev self.talk_room[uid] = room self.talk_location[uid] = room if fpath in self.args.trust_location: try: self.talk_location[uid] = ev.decoded("location").decode( "utf8" ) except: pass def run(self): self.rooms = [self._fname_to_room(fpath) for fpath in self.args.files] self.load_input() self.output() for fpath in self.changed_files: print(fpath) class HugoConverter(Converter): """ add relevant output features to the base converter """ def output_markdown(self): for uid in sorted(self.talks): talk = self.talks[uid] fname = "%s.md" % uid fpath = self.args.out_md_dir / fname self.changed_files.append(fpath) frontmatter = dict( key=uid, title=talk.decoded("SUMMARY").decode("utf8"), format="conference", start=talk.decoded("DTSTART"), end=talk.decoded("DTEND"), location=self.talk_location[uid], duration=int( (talk.decoded("DTEND") - talk.decoded("DTSTART")).total_seconds() // 60 ), tags=[], ) if "CATEGORIES" in talk: try: vobject = talk.get("CATEGORIES") if hasattr(vobject, "cats"): vobject = vobject.cats frontmatter["tags"] = [str(t) for t in vobject] else: frontmatter["tags"] = [str(vobject)] except Exception as exc: logging.warning("Error parsing categories: %s", str(exc)) if "base" in frontmatter["tags"]: frontmatter["level"] = "beginner" with open(str(fpath), "w") as buf: buf.write("---\n") yaml.safe_dump(frontmatter, buf) buf.write("---\n\n") # body if "DESCRIPTION" in talk: buf.write(talk.decoded("DESCRIPTION").decode("utf8")) def output_schedule(self): days = {} for uid in sorted(self.talks): talk = self.talks[uid] # TODO: talk just after midnight should belong to the preceding day dt = talk.decoded("dtstart") after_midnight = dt.time().hour < self.args.night_threshold if after_midnight: dt = dt - timedelta(days=1) day = dt.strftime("%Y-%m-%d") hour = talk.decoded("dtstart").time().hour minute = talk.decoded("dtstart").time().minute if after_midnight: hour += 24 start = "%02d:%02d" % (hour, minute) if day not in days: days[day] = dict(day=day, start=start, rooms={}) if days[day]["start"] > start: days[day]["start"] = start room = self.talk_room[uid] days[day]["rooms"].setdefault(room, dict(room=room, slots=[])) talkstart = round_down_time('%02d%02d' % (hour, minute), self.args.slot_size) duration = talk.decoded("dtend") - talk.decoded("dtstart") duration_minutes = int(duration.total_seconds() // 60) duration_minutes = round_down(duration_minutes, self.args.slot_size) slot = "%04d-%dmin" % (talkstart, duration_minutes) days[day]["rooms"][room]["slots"].append(dict(slot=slot, talk=uid)) # convert from our intermediate format to the correct one for d in sorted(days): # vanity: let's sort for room in sorted(days[d]["rooms"]): days[d]["rooms"][room]["slots"].sort(key=lambda x: x["slot"]) # convert dict to list days[d]["rooms"] = [days[d]["rooms"][k] for k in sorted(days[d]["rooms"])] out = {"schedule": [days[k] for k in sorted(days)]} # dump, finally with open(str(self.args.out_schedule), "w") as buf: yaml.safe_dump(out, buf) self.changed_files.append(self.args.out_schedule) def output(self): self.output_markdown() self.output_schedule() def main(): converter_register = {"hugo": HugoConverter} args = get_parser().parse_args() c = converter_register[args.mode](args) c.run() if __name__ == "__main__": main()