Serve events in ics with UTZ timestamps.

This commit is contained in:
Edward Betts 2025-11-14 15:14:17 +00:00
parent 77ea6a8afa
commit 5eab5361b2

View file

@ -4,7 +4,7 @@ import decimal
import hashlib
import os
import typing
from datetime import date, datetime, timedelta, timezone, tzinfo as datetime_tzinfo
from datetime import date, datetime, timedelta, timezone
import flask
import pycountry
@ -548,43 +548,16 @@ def _append_ical_property(lines: list[str], name: str, value: str) -> None:
lines.append(line)
def _append_datetime_property(lines: list[str], name: str, dt_value: datetime) -> None:
"""Append a datetime property preserving timezone."""
formatted, tzid = _format_ical_datetime(dt_value)
prop_name = f"{name};TZID={tzid}" if tzid else name
_append_ical_property(lines, prop_name, formatted)
def _ensure_timezone(dt_value: datetime) -> datetime:
"""Ensure datetimes are timezone-aware."""
if dt_value.tzinfo is None or dt_value.tzinfo.utcoffset(dt_value) is None:
def _ensure_utc(dt_value: datetime) -> datetime:
"""Ensure datetimes are timezone-aware in UTC."""
if dt_value.tzinfo is None:
return dt_value.replace(tzinfo=timezone.utc)
return dt_value
return dt_value.astimezone(timezone.utc)
def _tzid_from_tzinfo(tzinfo_obj: datetime_tzinfo, dt_value: datetime) -> str | None:
"""Return TZID string for timezone-aware datetime."""
for attr in ("zone", "key"):
tzid = getattr(tzinfo_obj, attr, None)
if isinstance(tzid, str):
return tzid
tzname = tzinfo_obj.tzname(dt_value)
if isinstance(tzname, str) and tzname.upper() not in {"UTC", "GMT"}:
return tzname
return None
def _format_ical_datetime(dt_value: datetime) -> tuple[str, str | None]:
"""Format datetime objects, preserving timezone when available."""
dt_value = _ensure_timezone(dt_value)
tzinfo = dt_value.tzinfo
assert tzinfo
tzid = _tzid_from_tzinfo(tzinfo, dt_value)
if tzid:
local_dt = dt_value.astimezone(tzinfo).replace(tzinfo=None)
return local_dt.strftime("%Y%m%dT%H%M%S"), tzid
utc_dt = dt_value.astimezone(timezone.utc)
return utc_dt.strftime("%Y%m%dT%H%M%SZ"), None
def _format_ical_datetime(dt_value: datetime) -> str:
"""Format datetime objects using UTC and RFC5545 format."""
return _ensure_utc(dt_value).strftime("%Y%m%dT%H%M%SZ")
def _format_ical_date(date_value: date) -> str:
@ -598,7 +571,7 @@ def _event_datetimes(element: TripElement) -> tuple[datetime, datetime]:
end_dt = as_datetime(element.end_time) if element.end_time else None
if end_dt is None or end_dt <= start_dt:
end_dt = start_dt + DEFAULT_EVENT_DURATION
return _ensure_timezone(start_dt), _ensure_timezone(end_dt)
return _ensure_utc(start_dt), _ensure_utc(end_dt)
def _event_all_day_dates(element: TripElement) -> tuple[date, date]:
@ -784,7 +757,7 @@ def build_trip_ical(trips: list[Trip]) -> bytes:
for index, element in enumerate(trip.elements()):
lines.append("BEGIN:VEVENT")
_append_ical_property(lines, "UID", _trip_element_uid(trip, element, index))
_append_datetime_property(lines, "DTSTAMP", generated)
_append_ical_property(lines, "DTSTAMP", _format_ical_datetime(generated))
if element.all_day:
start_date, end_date = _event_all_day_dates(element)
_append_ical_property(
@ -795,8 +768,8 @@ def build_trip_ical(trips: list[Trip]) -> bytes:
)
else:
start_dt, end_dt = _event_datetimes(element)
_append_datetime_property(lines, "DTSTART", start_dt)
_append_datetime_property(lines, "DTEND", end_dt)
_append_ical_property(lines, "DTSTART", _format_ical_datetime(start_dt))
_append_ical_property(lines, "DTEND", _format_ical_datetime(end_dt))
summary = _escape_ical_text(_trip_element_summary(trip, element))
_append_ical_property(lines, "SUMMARY", summary)