www/tools/gen_upcoming.py
Hauke Mehrtens 64ff74e5d2 Behebe Terminübersicht bei Wiederholungen mit UNTIL
gen_upcoming.py normalisiert dtstart auf naive lokale Zeit, reichte die
laut RFC 5545 in UTC anzugebende UNTIL-Zeit der RRULE aber tz-aware an
dateutil weiter. Die Mischung aus naivem dtstart und tz-awarer UNTIL
lässt dateutil.rrule mit "RRULE UNTIL values must be specified in UTC
when DTSTART is timezone-aware" abbrechen -- der Build schlägt fehl,
sobald ein wiederkehrender Termin ein UNTIL enthält.

Mit ignoretz=True wird die UNTIL-Zeit ebenfalls naiv behandelt,
konsistent zur restlichen Verarbeitung. Für Termine ohne UNTIL ist das
ein No-Op.

Fixes: 2e3a02af0c ("refacture all the things!")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
2026-07-04 23:35:50 +02:00

108 lines
3.3 KiB
Python
Executable file

#!/usr/bin/env python3
import sys
import logging
import locale
from dateutil.parser import parse as _parse
from datetime import datetime, timedelta
from dateutil.rrule import rruleset, rrulestr
import icalendar
def parse(value):
# Some templates emit DTSTART with a trailing "Z" (e.g. 20260507T190000Z)
# which produces tz-aware datetimes, while others emit naive local times.
# Normalize to naive so comparisons and sorting don't mix the two.
dt = _parse(value)
if dt.tzinfo is not None:
dt = dt.replace(tzinfo=None)
return dt
def vevent_to_event(event, rrstart=None):
if rrstart == None:
begin = parse(event["DTSTART"].to_ical())
else:
begin = rrstart
return {
"name": event["SUMMARY"].to_ical(),
"url": event["URL"].to_ical(),
"begin": begin
}
def parse_single_event(event, start, end):
logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
dtstart = parse(event["DTSTART"].to_ical())
if dtstart >= start and dtstart < end:
return vevent_to_event(event)
def parse_recurring_event(event, start, end):
logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
dtstart = parse(event["DTSTART"].to_ical())
rs = rruleset()
# dtstart is normalized to naive local time above; pass ignoretz so a
# tz-aware UNTIL (RFC 5545 requires UTC for it) is treated as naive too,
# otherwise dateutil rejects the naive/aware mismatch.
rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart, ignoretz=True))
if "EXDATE" in event.keys():
for exdate in event["EXDATE"]:
rs.exdate(parse(exdate.to_ical()))
events = []
for date in list(rs):
if date >= start and date < end:
events.append(vevent_to_event(event, date))
return events
def find_events(icsfilestr, start, end, num):
with open(icsfilestr, "r") as icsfile:
cal = icalendar.Calendar.from_ical(icsfile.read())
events = []
for event in cal.subcomponents:
if event.name == "VEVENT":
if "RRULE" in event.keys():
events.extend(parse_recurring_event(event, start, end))
else:
ev = parse_single_event(event, start, end)
if ev is not None:
events.append(ev)
events = sorted(events, key=lambda k: k["begin"])
events = events[0:num]
return events
def format_events(events):
print("<table class=\"table table-condensed\">")
for event in events:
print(
"<tr>"
f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>"
f"<td><a href=\"{event['url'].decode('utf-8')}\">{event['name'].decode('utf-8')}</a></td>"
"</tr>"
)
print("</table><!--/.table .table-condensed-->")
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} calendar max_days max_items")
sys.exit(-1)
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
calendar = sys.argv[1]
max_days = int(sys.argv[2])
max_items = int(sys.argv[3])
now = datetime.now()
events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
format_events(events)