"""Generate ICS calendar containing only one user's duties (for subscription link).""" from datetime import datetime, timezone from icalendar import Calendar, Event from duty_teller.db.models import Duty # Summary labels by event_type (duty | unavailable | vacation) SUMMARY_BY_TYPE: dict[str, str] = { "duty": "Duty", "unavailable": "Unavailable", "vacation": "Vacation", } def _parse_utc_iso(iso_str: str) -> datetime: """Parse ISO 8601 UTC string (e.g. 2025-01-15T09:00:00Z) to timezone-aware datetime.""" s = iso_str.strip().rstrip("Z") if "Z" in s: s = s.replace("Z", "+00:00") else: s = s + "+00:00" return datetime.fromisoformat(s) def build_personal_ics(duties_with_name: list[tuple[Duty, str]]) -> bytes: """ Build a single VCALENDAR with one VEVENT per duty. duties_with_name: list of (Duty, full_name); full_name is unused for SUMMARY if we use event_type only; can be used later for DESCRIPTION. """ cal = Calendar() cal.add("prodid", "-//Duty Teller//Personal Calendar//EN") cal.add("version", "2.0") cal.add("calscale", "GREGORIAN") for duty, _full_name in duties_with_name: event = Event() start_dt = _parse_utc_iso(duty.start_at) end_dt = _parse_utc_iso(duty.end_at) # Ensure timezone-aware for icalendar if start_dt.tzinfo is None: start_dt = start_dt.replace(tzinfo=timezone.utc) if end_dt.tzinfo is None: end_dt = end_dt.replace(tzinfo=timezone.utc) event.add("dtstart", start_dt) event.add("dtend", end_dt) summary = SUMMARY_BY_TYPE.get( duty.event_type if duty.event_type else "duty", "Duty" ) event.add("summary", summary) event.add("uid", f"duty-{duty.id}@duty-teller") event.add("dtstamp", datetime.now(timezone.utc)) cal.add_component(event) return cal.to_ical()