Skip to content

Instantly share code, notes, and snippets.

@VincenzoLaSpesa
Created January 25, 2026 12:02
Show Gist options
  • Select an option

  • Save VincenzoLaSpesa/82bcb0d19abf2745ae3e053a8e141e99 to your computer and use it in GitHub Desktop.

Select an option

Save VincenzoLaSpesa/82bcb0d19abf2745ae3e053a8e141e99 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
Author: Vincenzo La Spesa
Date: 2026-01-25
Version: 1.0
Description:
This script allows to make a VCARD calendar out of the events exported from https://fosdem.sojourner.rocks/2026/
as a CSV.
The usage is:
- Generate the full events list, with: fosdem_events_to_vcards.py --generatelookup -l lookup.pkl
- Generate the ics file from the csv exported from solojourner and the lookup table generated before:
fosdem_events_to_vcards.py -l lookup.pkl -i fosdem-2026.csv -o output.ics
License: MIT License
Contact: vincenzolaspesa@gmail.com
Dependencies: bs4 (pip install beautifulsoup4)
"""
import pickle
#import yaml
import requests
import datetime
from bs4 import BeautifulSoup
from dataclasses import dataclass, asdict
from dateutil import parser
import argparse
import hashlib
import csv
URL = "https://fosdem.org/2026/schedule/events/"
DAYS={
"Saturday" : parser.parse("2026-01-31 00:00"),
"Sunday" : parser.parse("2026-02-01 00:00")
}
SERIALIZING_DATE=datetime.datetime.now()
@dataclass
class Event:
title: str = None
speakers: str = None
track: str = None
start: datetime.datetime = datetime.datetime.now()
end: datetime.datetime = datetime.datetime.now()
room: str = None
url: str = None
description: str = ""
def stable_uid(self) -> str:
m = hashlib.sha256()
m.update(self.title.encode("utf-8"))
m.update(self.speakers.encode("utf-8"))
m.update(self.track.encode("utf-8"))
m.update(datetime_to_str(self.start).encode("utf-8"))
m.update(datetime_to_str(self.end).encode("utf-8"))
m.update(self.url.encode("utf-8"))
m.digest()
digest=m.hexdigest()
chunks=[]
for i in range(0,int(0.25*len(digest))):
if i>4:
chunks.append(digest[i-4:i])
digest='-'.join(chunks)
return digest[:29]
def to_vcard(self):
return f"""BEGIN:VEVENT
UID:{self.stable_uid()}
DTSTAMP:{datetime_to_str(SERIALIZING_DATE)}
DTSTART:{datetime_to_str(self.start)}
DTEND:{datetime_to_str(self.end)}
SUMMARY:{self.title} ({self.track})
DESCRIPTION:{self.description}{self.url} {self.track}
CLASS:PUBLIC
URL:{self.url}
LOCATION:{self.room}
END:VEVENT"""
def datetime_to_str(dt: datetime.datetime):
dt_utc = dt.astimezone(datetime.timezone.utc)
return dt_utc.strftime("%Y%m%dT%H%M%SZ")
def parse_single_event(table_row, track, baseurl="https://fosdem.org") -> dict:
columns=table_row.select("td")
basetime= DAYS[columns[3].text]
start_a=[int(x) for x in columns[4].text.split(":")]
end_a=[int(x) for x in columns[5].text.split(":")]
event= Event()
event.track=track
event.title=columns[0].text
event.url=baseurl+columns[0].select("a")[0]['href']
event.speakers=columns[1].text
event.room=columns[2].text
event.start=basetime+datetime.timedelta(hours=start_a[0], minutes=start_a[1])
event.end=basetime+datetime.timedelta(hours=end_a[0], minutes=end_a[1])
return event
def parse_events():
print("Downloading events page…")
events = []
html = requests.get(URL).text
zuppa = BeautifulSoup(html, "html.parser")
tables=zuppa.select("html body.schedule-events div#main table.table.table-striped.table-bordered.table-condensed")
track=None
for table in tables:
rows=table.select("tr")
for row in rows:
h4=row.select_one("h4")
if h4:
track=h4.text
nlink=len(row.select("a"))
ntd=len(row.select("td"))
if(ntd>3 and nlink>2):
events.append(parse_single_event(row,track))
print(events[-1].title)
m=Event()
m.title="metadata"
m.start=SERIALIZING_DATE
events.append(m)
return events
def convert_csv_to_ics(csv_path, ics_path, lookup):
keys=[]
with open(csv_path, newline='', encoding='utf-8') as f:
rreader = csv.DictReader(f)
for row in rreader:
keys.append(row["Title"].lower())
events=[]
for k in keys:
events.append(lookup[k])
with open(ics_path, "w", encoding="utf-8") as f:
f.write("BEGIN:VCALENDAR\n")
f.write("VERSION:2.0\n")
f.write("PRODID:-//be.digitalia.fosdem//NONSGML 2.3.0//EN\n")
for event in events:
f.write(event.to_vcard() + "\n")
f.write("END:VCALENDAR\n")
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input",type=str, help="input csv file")
parser.add_argument("-o", "--output",type=str, help="output ics file")
parser.add_argument('-l',"--lookup", type=str, help="input lookup file")
parser.add_argument('--generatelookup', action='store_true')
args = parser.parse_args()
if args.generatelookup:
assert args.lookup != None, "The --generatelookup flag needs a --lookup file to be set"
events = parse_events()
events.sort(key=lambda x : x.title)
#with open(f"{args.lookup}.yaml", 'w') as file:
# yaml.dump(events, file)
pickle.dump(events, open(args.lookup, 'wb'))
else:
assert args.input and args.output and args.lookup, "both --input and --output and --lookup need to be set"
list_ = pickle.load(open(args.lookup, 'rb'))
lookup={}
for l in list_:
if isinstance(l, Event):
lookup[l.title.lower()]=l
convert_csv_to_ics(args.input, args.output, lookup)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment