t-r.de/utils/refresh-events.py

176 lines
6.3 KiB
Python
Executable File

import caldav
import vobject
from datetime import datetime, date, time, timedelta
import yaml
import os
import locale
import threading
from contextlib import contextmanager
"""CalDAV server url"""
server_url = "https://***REMOVED***
"""CalDAV calendar url"""
cal_url = "https://***REMOVED***
"""CalDAV username"""
cal_user = "tobias"
"""Category to filter events for. Only events that belong to this category will be processed."""
cal_category = "Veranstaltung"
"""Name of the file the YAML data will be writte to."""
result_file = "../content/pages/termine.md"
"""List of icalendar properties that will be passed on to Pelican. At this time, all properties are used except those regarding alarms."""
cal_properties = [ # taken from ical specification
# descriptive
"attach", "categories", "class", "comment", "description", "geo", "location", "percent-complete", "priority", "resources", "status", "summary",
# date and time
"completed", "dtend", "due", "dtstart", "duration", "freebusy", "transp",
# timezone components
"tzid", "tzname", "tzoffsetfrom", "tzoffsetto", "tzurl",
# relationship
"attendee", "contact", "organizer", "recurrence-id", "related-to", "url", "uid",
# recurrence
"exdate", "exrule", "rdate", "rrule",
# alarm
# "action", "repeat", "trigger",
# change management
"created", "dtstamp", "last-modified", "sequence"
]
"""List of icalendar properties that the vobject passes as either datetime.datetime or datetime.date objects. To be able to compare any two objects, the script makes each one aware and/or converts into datetime.datetime if necessary. Default timezone is the system timezone; default time is 23:59:59."""
cal_prop_datetimes = ["dtend", "due", "dtstart", "duration", "dtstamp", "last-modified"]
"""Current time as an aware datetime.datetime object."""
now = datetime.now().astimezone()
"""Returns an aware datetime.datetime object based on the given datetime or date. Conversion happens only if necessary, i.e. if d is of type datetime.date or a naive datetime.datetime object."""
def fixDatetime(d, t=None, tz=None):
if not tz:
tz = now.tzinfo
# datetime object is made aware if necessary
if type(d) == datetime:
if d.tzinfo:
return d
else:
return datetime(d.date(), d.time(), tz)
# raise error if d is neither datetime nor date
elif type(d) != date:
raise TypeError("parameter must be a datetime.date or datetime.datetime object")
# d is a date object
if not t:
t = time(23, 59, 59, tzinfo=tz) # if no time parameter was passed, use 23:59:59
if not t.tzinfo:
t.replace(tzinfo=tz)
return datetime.combine(d, t)
"""Takes a list of events in vobject representation and converts each event into a dict of Python datatypes."""
def extractEventData(events):
e_tmp = []
# iterate over events
for e in events:
d = {}
# iterate over event properties
for k, v in e.contents.items():
# we're only interested in a subset of properties
if not k in cal_properties:
continue
# Usually, v is a list with a single item. We only need the item's value attribute
if len(v) == 1:
d[k] = v[0].value
# but sometimes the list has more than one item; this only ever happens for the property "categories"
# and in this case, each list item's value is itself a one item list (which is stupid but that's how vobject handles categories)
else:
d[k] = [v[i].value[0] for i in range(len(v))]
e_tmp.append(d)
return e_tmp
# get password
with open(".caldav_pass", "r") as f:
cal_pass = f.read().strip()
# create vobject calendar
vcal = vobject.newFromBehavior("vcalendar")
# establish connection to caldav server
with caldav.DAVClient(url=server_url, username=cal_user, password=cal_pass) as dav_client:
# establish connection to calendar
dav_cal = dav_client.calendar(url=cal_url)
# put all events from caldav calendar into vobject calendar
for e in dav_cal.events():
for ev in e.instance.contents["vevent"]:
vcal.add(ev)
# we only want events belonging to a specific category
events = [e for e in vcal.getChildren() if "categories" in e.contents.keys() and cal_category in map(lambda x: x.value[0], e.contents["categories"])]
# extract event data
events = extractEventData(events)
# fix dates and datetimes
for e in events:
for k in e.keys():
if k in cal_prop_datetimes:
# fix a CalDAV/vobject/icalendar (?) bug where a dtend value which is a date only (no time) automatically moves one day into the future
if k == "dtend" and type(e[k]) == date:
e[k] = e[k] - timedelta(days=1)
# fix all datetimes so they can be compared and sorted
e[k] = fixDatetime(e[k])
# keep only future events
events = [e for e in events if e["dtstart"] >= now]
# sort by start date
events.sort(key=lambda e: e["dtstart"])
# read default metadata if present
default_metadata = None
if os.path.isfile(result_file + ".metadata"):
with open(result_file + ".metadata", "r") as f:
default_metadata = yaml.safe_load(f)
# set up a context manager in order to threadsafely format dates and times with the German locale
# source: https://stackoverflow.com/a/24070673
LOCALE_LOCK = threading.Lock()
@contextmanager
def setlocale(name):
with LOCALE_LOCK:
saved = locale.setlocale(locale.LC_TIME)
try:
yield locale.setlocale(locale.LC_TIME, name)
finally:
locale.setlocale(locale.LC_TIME, saved)
# temporary set locale to de_DE (category "time" only) and format start date as well as start time or end date
with setlocale('de_DE.UTF-8'):
for e in events:
# start date
e["startdate"] = e["dtstart"].strftime("%a, %d.%m.")
# start time (if not the default time)
if e["dtstart"].timetz() != time(23, 59, 59, tzinfo=now.tzinfo):
e["starttime"] = e["dtstart"].strftime("%H:%M Uhr")
# end date (if different from start date)
if e["dtstart"].date() < e["dtend"].date():
e["enddate"] = e["dtend"].date().strftime("%a, %d.%m.")
# add current date to default metadata while we're at it
if default_metadata:
default_metadata["date"] = now.strftime("%Y-%m-%d %H:%M")
# write data as YAML
with open(result_file, 'w') as f:
f.write("---\n")
if default_metadata:
yaml.dump(default_metadata, f)
yaml.dump({"termine": events}, f)
f.write("---\n")
f.write("written at " + datetime.today().isoformat(" "))