import calendar from dataclasses import dataclass from datetime import date, datetime from typing import Generator import pytz import streamlit as st from googleapiclient.discovery import build def build_calendar_api(creds): return build("calendar", "v3", credentials=creds) @dataclass class Event: id: str title: str description: str | None start: datetime @st.cache_data(ttl=60, show_spinner="Fetching calendars...") def get_calendars(_service): calendars = [] page_token = None while True: calendar_list = _service.calendarList().list(pageToken=page_token).execute() for entry in calendar_list["items"]: cal_id = entry["id"] calendars.append(cal_id) page_token = calendar_list.get("nextPageToken") if not page_token: break return calendars def localize(date_input: datetime | date) -> datetime: local_timezone = pytz.timezone("America/Los_Angeles") timezone_aware_datetime = local_timezone.localize( datetime(date_input.year, date_input.month, date_input.day) ) utc_datetime = timezone_aware_datetime.astimezone(pytz.utc) return utc_datetime @st.cache_data(ttl=60, show_spinner="Fetching calendar events...") def get_events( _service, start: datetime | date, end: datetime | date, calendar_ids: list[str], ) -> list[Event]: all_events: list[Event] = [] for cal_id in calendar_ids: all_events.extend( _gen_events( _service, calendarId=cal_id, timeMin=localize(start).isoformat(), timeMax=localize(end).isoformat(), singleEvents=True, orderBy="startTime", ) ) all_events.sort(key=lambda ev: ev.start) return all_events def _gen_events(_service, **kwargs) -> Generator[Event, None, None]: page_token = None while True: events = ( _service.events() .list( **kwargs, pageToken=page_token, ) .execute() ) for event in events["items"]: start = _parse_date(event["start"]) ev = Event( id=event["id"], title=event["summary"], description=event.get("description"), start=start, ) yield ev page_token = events.get("nextPageToken") if not page_token: break def _parse_date(date_obj: dict) -> datetime: if "dateTime" in date_obj: dt = datetime.fromisoformat(date_obj["dateTime"].rstrip("Z")) elif "date" in date_obj: dt = datetime.fromisoformat(date_obj["date"]) else: raise ValueError("Event has no start date") is_naive = dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None if is_naive: if "timeZone" in date_obj: timezone = pytz.timezone(date_obj["timeZone"]) dt = timezone.localize(dt) else: dt = pytz.utc.localize(dt) return dt def get_start_and_end(month_date: date) -> tuple[date, date]: start_of_month = date(month_date.year, month_date.month, 1) last_day_of_month = calendar.monthrange(month_date.year, month_date.month)[1] end_of_month = date(month_date.year, month_date.month, last_day_of_month) return start_of_month, end_of_month