Skip to content

Commit 4c39900

Browse files
committedAug 11, 2023
Add a CSV to ICS helper tool
I wrote this to extract the schedule from the website of the EuroBSDCon: https://2023.eurobsdcon.org/program/ Copy-paste the tables into a spreadsheet (like LibreOffice Calc), export as CSV (, as delimiter, " as quote). It expects: - lines that start with the event type and day - lines starting with an empty cell that lists the tracks, - lines that start with start_time (eventually end), then each talk. Lot of hardcoding and debug output to cleanup.
1 parent 9d78693 commit 4c39900

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed
 

‎tools/csv2ics.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2023, François Revol <revol@free.fr>
3+
# MIT license
4+
#
5+
# csv2ics: convert a sensible schedule table to an icalendar program
6+
# Initially wrote for https://2023.eurobsdcon.org/program/
7+
8+
import csv
9+
from datetime import datetime, timedelta
10+
import pytz
11+
import re
12+
import sys
13+
from icalendar import Calendar, Event
14+
15+
16+
# TODO: pass this as arguments
17+
cal_name = "EuroBSDCon 2023"
18+
timezone = "Europe/Lisbon"
19+
day_filter = "(?P<event_type>.*)s:.*\((?P<day_str>.*)\)"
20+
# You might need to run this with LC_TIME=C to parse English dates
21+
day_format = "%d %B %Y"
22+
time_filter = "(?P<start_time>\d{2}:\d{2})( *- *)?(?P<end_time>\d{2}:\d{2})?"
23+
24+
with open(sys.argv[1], newline='') as csvfile:
25+
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
26+
cal = Calendar()
27+
cal.add('prodid', '-//csv2ics.py////')
28+
cal.add('version', '2.0')
29+
cal.add('name', cal_name)
30+
cal.add('x-wr-name', cal_name)
31+
cal.add('timezone-id', timezone)
32+
cal.add('x-wr-timezone', timezone)
33+
tzinfo = pytz.timezone(timezone)
34+
day = None
35+
event_type = None
36+
locations = [] # aka Tracks for conferences
37+
last_time = None
38+
pending_events = []
39+
for row in reader:
40+
if len(row[0].strip()) == 0:
41+
locations = row[1:]
42+
# flush pending for previous day, assume 1h
43+
for e in pending_events:
44+
print("pending pending: %s" % e)
45+
end_time = start_time + timedelta(hours=1)
46+
e.add('dtend', datetime.combine(day, end_time.time(), tzinfo=tzinfo))
47+
cal.add_component(e)
48+
pending_events = []
49+
50+
m = re.match(day_filter, row[0])
51+
#if len(row[0]) and row[0][0].isdigit():
52+
if m is not None:
53+
event_type = m.group('event_type')
54+
day = datetime.strptime(m.group('day_str'), day_format)
55+
print(day)
56+
print(event_type)
57+
#m = re.match("(?P<start_time>\d{2}:\d{2})")
58+
m = re.match(time_filter, row[0])
59+
if m:
60+
start_time = m.group('start_time')
61+
end_time = m.group('end_time') or None
62+
start_time = datetime.strptime(start_time, "%H:%M")
63+
#start_time = timedelta(start_time)
64+
if end_time:
65+
end_time = datetime.strptime(end_time, "%H:%M")
66+
#end_time = timedelta(end_time)
67+
68+
# flush pending
69+
for e in pending_events:
70+
print("pending pending: %s" % e)
71+
e.add('dtend', datetime.combine(day, start_time.time(), tzinfo=tzinfo))
72+
cal.add_component(e)
73+
pending_events = []
74+
print("##%s" % str(m.groups()))
75+
print("## %s %s" % (start_time, end_time))
76+
for i, entry in enumerate(row[1:]):
77+
# skip fields with just a NO-BREAK SPACE or BOM
78+
if entry in ['\u00a0', '\uFEFF']:
79+
entry = ""
80+
entry = entry.strip()
81+
print("#### %d %s" % (len(entry), entry))
82+
if len(entry) < 1:
83+
continue
84+
print("## %d %s, %s" % (i, locations[i], entry))
85+
lines = entry.split('\n')
86+
e = Event()
87+
e.add('summary', lines[0])
88+
e.add('dtstart', datetime.combine(day, start_time.time(), tzinfo=tzinfo))
89+
if len(lines) > 1:
90+
desc = "(%s)\n" % event_type
91+
desc += "\n".join(lines[1:]).strip('\n')
92+
e.add('description', desc)
93+
e.add('location', locations[i])
94+
if end_time is None:
95+
# we don't know the end time yet
96+
print("pending += %s" % str(e))
97+
pending_events.append(e)
98+
else:
99+
e.add('dtend', datetime.combine(day, end_time.time(), tzinfo=tzinfo))
100+
cal.add_component(e)
101+
print('|\n'.join(row))
102+
for e in pending_events:
103+
# Assume last events are an hour long
104+
print("pending pending: %s" % e)
105+
end_time = start_time + timedelta(hours=1)
106+
e.add('dtend', datetime.combine(day, end_time.time(), tzinfo=tzinfo))
107+
cal.add_component(e)
108+
109+
with open(sys.argv[2], 'wb') as icsfile:
110+
icsfile.write(cal.to_ical())
111+

0 commit comments

Comments
 (0)