import argparse import os import re from pathlib import Path import yaml from caldav import DAVClient from icalendar import Calendar def safe_filename(name: str) -> str: return re.sub(r"[^a-zA-Z0-9._-]+", "_", name) def export_calendar(url: str, username: str, password: str, calendar: str) -> Calendar: client = DAVClient( url=url, username=username, password=password, ) principal = client.principal() calendars = principal.calendars() target = None for cal in calendars: if cal.name == calendar: target = cal break if target is None: raise RuntimeError( f"Calendar '{calendar}' not found for {username}" ) print(f"Exporting {username}/{target.name}") result = Calendar() result.add("prodid", f"-//{calendar} calendar export//") result.add("version", "2.0") objects = target.objects() print(f" {len(objects)} objects") for obj in objects: data = obj.data if data is None: print(f" fetching {obj.url}") obj.load() data = obj.data if not data: print(f" skipping empty object {obj.url}") continue cal = Calendar.from_ical(data) for component in cal.walk(): if component.name != "VCALENDAR": result.add_component(component) return result def main(): parser = argparse.ArgumentParser() parser.add_argument("config") parser.add_argument( "-o", "--output", default="/srv/http/calendars", ) args = parser.parse_args() with open(args.config) as f: config = yaml.safe_load(f) output_dir = Path(args.output) output_dir.mkdir(parents=True, exist_ok=True) combined = Calendar() combined.add("prodid", "-//Combined calendar//") combined.add("version", "2.0") username = config["username"] password = config["password"] for entry in config["calendars"]: cal = export_calendar( entry["url"], username, password, entry["calendar"], ) filename = output_dir / f"{safe_filename(entry['calendar'])}.ics" with open(filename, "wb") as f: f.write(cal.to_ical()) print(f" -> {filename}") for component in cal.walk(): if component.name != "VCALENDAR": combined.add_component(component) combined_file = output_dir / "all.ics" with open(combined_file, "wb") as f: f.write(combined.to_ical()) print() print(f"Combined export: {combined_file}") if __name__ == "__main__": main()