from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Any @dataclass(frozen=True) class AvailabilityResult: start: str end: str available: bool busy_slots: list[dict[str, str]] checked_calendars: list[str] class CalendarAvailabilityAgent: def __init__(self, calendar_service: Any) -> None: self.calendar_service = calendar_service def get_availability( self, start: str, end: str, calendar_ids: list[str] | None = None ) -> AvailabilityResult: start_dt = _parse_iso_datetime(start) end_dt = _parse_iso_datetime(end) if end_dt <= start_dt: raise ValueError("end must be after start.") calendars = calendar_ids or ["primary"] query_body = { "timeMin": start_dt.isoformat(), "timeMax": end_dt.isoformat(), "items": [{"id": calendar_id} for calendar_id in calendars], } freebusy = self.calendar_service.freebusy().query(body=query_body).execute() calendars_payload = freebusy.get("calendars", {}) busy_slots: list[dict[str, str]] = [] for calendar_id, data in calendars_payload.items(): for busy_slot in data.get("busy", []): busy_slots.append( { "calendar_id": calendar_id, "start": busy_slot["start"], "end": busy_slot["end"], } ) return AvailabilityResult( start=start_dt.isoformat(), end=end_dt.isoformat(), available=len(busy_slots) == 0, busy_slots=busy_slots, checked_calendars=calendars, ) def _parse_iso_datetime(value: str) -> datetime: normalized = value.strip() if normalized.endswith("Z"): normalized = normalized[:-1] + "+00:00" parsed = datetime.fromisoformat(normalized) if parsed.tzinfo is None: raise ValueError("datetime must include a timezone offset, for example +01:00.") return parsed