aggiunge esportazione verso pelican
This commit is contained in:
parent
f5874420a8
commit
96891a1921
1 changed files with 225 additions and 0 deletions
225
ics2yaml.py
Executable file
225
ics2yaml.py
Executable file
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import logging
|
||||
import argparse
|
||||
import os.path
|
||||
from typing import Iterable
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
import json
|
||||
|
||||
import yaml
|
||||
from icalendar import Calendar, Event
|
||||
|
||||
|
||||
def get_parser():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("files", nargs="+", type=str)
|
||||
p.add_argument("--hm-json", default='./hackmeeting.json', type=Path)
|
||||
p.add_argument(
|
||||
"--out-talks-dir",
|
||||
help="Output directory for markdown files",
|
||||
default="talks/",
|
||||
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=["pelican"], default="pelican")
|
||||
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:
|
||||
base = os.path.splitext(os.path.basename(fpath))[0]
|
||||
if base == 'ALL':
|
||||
return '*'
|
||||
return base
|
||||
|
||||
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):
|
||||
with self.args.hm_json.open() as buf:
|
||||
self.hackmeeting_metadata = json.load(buf)
|
||||
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):
|
||||
if ev.decoded('DTSTART').year != self.hackmeeting_metadata['year']:
|
||||
continue
|
||||
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 PelicanConverter(Converter):
|
||||
"""
|
||||
add relevant output features to the base converter
|
||||
"""
|
||||
|
||||
def load_input(self):
|
||||
super().load_input()
|
||||
talks_meta = self.args.out_talks_dir / 'meta.yaml'
|
||||
with talks_meta.open() as buf:
|
||||
self.talks_metadata = yaml.safe_load(buf)
|
||||
|
||||
def output_markdown(self):
|
||||
for uid in sorted(self.talks):
|
||||
talk = self.talks[uid]
|
||||
fname = 'meta.yaml'
|
||||
talkdir = self.args.out_talks_dir / uid
|
||||
talkdir.mkdir(exist_ok=True)
|
||||
fpath = talkdir / fname
|
||||
self.changed_files.append(fpath)
|
||||
day = (talk.decoded('DTSTART').date() - self.talks_metadata['startdate']).days
|
||||
frontmatter = dict(
|
||||
key=uid,
|
||||
title=talk.decoded("SUMMARY").decode("utf8"),
|
||||
format="conference",
|
||||
start=talk.decoded("DTSTART"),
|
||||
time=talk.decoded("DTSTART").strftime('%H:%M'),
|
||||
day=day,
|
||||
end=talk.decoded("DTEND"),
|
||||
room=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"
|
||||
if "DESCRIPTION" in talk:
|
||||
frontmatter['text'] = talk.decoded("DESCRIPTION").decode("utf8")
|
||||
else:
|
||||
frontmatter['text'] = ''
|
||||
with open(str(fpath), "w") as buf:
|
||||
yaml.safe_dump(frontmatter, buf)
|
||||
# body
|
||||
|
||||
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)]}
|
||||
|
||||
# XXX: dump, finally
|
||||
|
||||
def output(self):
|
||||
self.output_markdown()
|
||||
|
||||
|
||||
def main():
|
||||
converter_register = {"pelican": PelicanConverter}
|
||||
args = get_parser().parse_args()
|
||||
c = converter_register[args.mode](args)
|
||||
c.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue