Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save saerdnaer/ea054ca0f997b2655441fbcb732d89c6 to your computer and use it in GitHub Desktop.

Select an option

Save saerdnaer/ea054ca0f997b2655441fbcb732d89c6 to your computer and use it in GitHub Desktop.
generate a VCARD calendar out of the events exported from https://fosdem.sojourner.rocks/2026/
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Filename: fosdem_events_to_vcards.py
Date: 2026-01-31
Version: 1.1
Description:
This script allows to make a VCARD calendar out of the events exported from https://fosdem.sojourner.rocks/2026/
as a CSV.
Usage:
- Generate the ics file from the csv exported from solojourner:
fosdem_events_to_vcards.py -s https://fosdem.org/2026/schedule/xml -i fosdem-2026.csv -o your-favs.ics
License: MIT License
"""
from datetime import datetime, timezone
from html2text import HTML2Text
import icalendar
import argparse
import csv
from schedulexml import ScheduleXML, Event
DOMAIN = "fosdem.org"
html2text = HTML2Text()
html2text.ignore_links = False
html2text.body_width = 0 # no forced line wrapping
now = datetime.now(timezone.utc)
def to_ical(self: Event) -> icalendar.Event:
event = icalendar.Event()
event.add("uid", f"{self['guid']}@{DOMAIN}")
event.add("summary", self["title"])
# Add dtstart and dtend to event
event.add("dtstart", self.start)
event.add("dtend", self.end)
event.add("dtstamp", now)
if self.get("room") is not None:
event.add("location", self["room"], encode=False)
description = ""
if self.get('persons'):
for person in self.participants:
if person:
attendee = icalendar.vCalAddress(f"urn:{DOMAIN}:person:{person.id}")
attendee.params["CN"] = icalendar.vText(person.name)
attendee.params["ROLE"] = icalendar.vText("SPEAKER")
attendee.params["PARTSTAT"] = icalendar.vText("ACCEPTED")
attendee.params["RSVP"] = icalendar.vText("TRUE")
event.add("attendee", attendee, encode=False)
#description += f"{person.name}\n"
description += f"\n{html2text.handle(self['abstract'])}" if self.get("abstract") else ""
description += f"\n\n{html2text.handle(self['description'])}" if self.get("description") else ""
event.add("description", description)
event.add("url", self["url"])
return event
def convert_fav_csv_to_ics(csv_path, ics_path, events_lookup):
favs=[]
with open(csv_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
favs.append(row["ID"].lower())
events=[]
for id in favs:
events.append(events_lookup[id])
cal = icalendar.Calendar()
cal.add("prodid", f"-//{DOMAIN}///Favs to iCal//EN")
cal.add("version", "2.0")
for event in events:
cal.add_component(to_ical(event))
with open(ics_path, "w", encoding="utf-8") as f:
f.write(cal.to_ical().decode("utf-8"))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-s',"--schedule", type=str, help="input lookup file", required=True)
parser.add_argument("-i", "--input",type=str, help="input csv file", required=True)
parser.add_argument("-o", "--output",type=str, help="output ics file", required=True)
args = parser.parse_args()
schedule = ScheduleXML.from_url(args.schedule)
lookup = { e["guid"]: e for e in schedule.events() }
convert_fav_csv_to_ics(args.input, args.output, lookup)
if __name__ == "__main__":
main()
[project]
name = "fosdem_events_to_vcards.py"
version = "1.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
"html2text>=2025.4.15",
"icalendar>=6.3.2",
"python-dateutil>=2.9.0.post0",
"requests>=2.32.5",
]
from collections.abc import Mapping
from dataclasses import dataclass
import re
from typing import Union
from xml.etree import ElementTree
from datetime import datetime, timedelta
from dateutil.parser import parse as parse_datetime
import requests
class ScheduleXML:
"""
Schedule from XML document using etree, with inspirations from
- https://github.com/pretalx/pretalx-downstream/blob/master/pretalx_downstream/tasks.py#L67
- https://github.com/Zverik/schedule-convert/blob/master/schedule_convert/importers/frab_xml.py#L55
"""
_schedule: ElementTree = None
tz = None
def __init__(self, tree):
self._schedule = tree
@classmethod
def from_url(cls, url):
r = requests.get(url)
if r.ok is False:
raise Exception(f'Request failed, HTTP {r.status_code}.')
schedule = ElementTree.fromstring(r.text)
# Close the raw file handle if it's still open
if hasattr(r, 'raw') and r.raw.closed is False:
r.raw.close()
return ScheduleXML(tree=schedule)
def __getitem__(self, key):
return self._schedule.find(key)
def schedule(self):
return self._schedule
def version(self):
v = self._schedule.find('version')
return v.text if v is not None else None
def days(self):
return self._schedule.findall('day')
def rooms(self):
rooms = {}
for day in self.days():
for room in day.findall('room'):
room_name = room.attrib['name']
room_guid = room.attrib.get('guid')
room_desc = room.attrib.get('description')
rooms[room_name] = {'guid': room_guid, 'name': room_name, 'description': room_desc}
return list(rooms.values())
def events(self):
for day in self.days():
for room in day.findall('room'):
for event in room.findall('event'):
e = Event(event)
e['id'] = event.attrib.get('id')
e['guid'] = event.attrib.get('guid')
yield e
def __str__(self):
return ElementTree.tounicode(self._schedule, pretty_print=True)
@dataclass
class Person:
name: str
id: str
@dataclass
class Event(Mapping):
def __init__(self,
tree: ElementTree,
start: datetime|None = None
):
self._event = tree
self._values = {}
if start is not None:
self.start: datetime = start
else:
self.start = parse_datetime(self['date'])
self.duration = str2timedelta(self["duration"])
@property
def end(self):
return self.start + self.duration
@property
def participants(self) -> list[Person]:
participants = []
for person in self._event.find('persons').findall('person'):
name = person.text
if name:
participants.append(Person(name, person.attrib.get('id')))
return participants
def __getitem__(self, key):
if key in self._values:
return self._values[key]
item = self._event.find(key)
return item.text if item is not None else None
def __setitem__(self, key, value):
self._values[key] = value
def __iter__(self):
return self._event.__iter__()
def __len__(self):
return len(self._event)
def __lt__(self, other):
return self.start < other.start
def items(self):
return self._event.items()
def format_duration(value: Union[int, timedelta]) -> str:
if type(value) == timedelta:
minutes = round(value.total_seconds() / 60)
else:
minutes = value
return '%d:%02d' % divmod(minutes, 60)
# from https://git.cccv.de/hub/hub/-/blob/develop/src/core/utils.py
_RE_STR2TIMEDELTA = re.compile(r'((?P<days>\d+?)hr?\s*)?((?P<hours>\d+?)m(ins?)?\s*)?((?P<minutes>\d+?)s)?')
def str2timedelta(s) -> timedelta:
if ':' in s:
parts = s.split(':')
kwargs = {'minutes': int(parts.pop())}
if parts:
kwargs['hours'] = int(parts.pop())
if parts:
kwargs['days'] = int(parts.pop())
return timedelta(**kwargs)
parts = _RE_STR2TIMEDELTA.match(s)
if not parts:
raise ValueError(f'Could not parse time duration string: {s}')
parts = parts.groupdict()
time_params = {}
for name, param in parts.items():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment