import argparse import os import re from pathlib import Path import yaml from caldav import DAVClient from icalendar import Calendar def safe_filename(name): return re.sub(r"[^a-zA-Z0-9._-]+", "_", name) def export_calendar(url, username, password, output): client = DAVClient( url=url, username=username, password=password, ) principal = client.principal() calendars = principal.calendars() target = None for cal in calendars: if cal.name == output["calendar"]: target = cal break if target is None: raise RuntimeError( f"Calendar '{output['calendar']}' not found for {username}" ) print(f"Exporting {username}/{target.name}") result = Calendar() result.add("prodid", "-//CalDAV Export//") result.add("version", "2.0") objects = target.objects() print(f" {len(objects)} objects") for obj in objects: cal = Calendar.from_ical(obj.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/ics", ) 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", "-//CalDAV Export Combined//") combined.add("version", "2.0") for entry in config["calendars"]: username = entry["username"] password = entry["password"] cal = export_calendar( entry["url"], username, password, entry, ) 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-calendars.ics" with open(combined_file, "wb") as f: f.write(combined.to_ical()) print() print(f"Combined export: {combined_file}") if __name__ == "__main__": main()