Files
2026-06-29 10:08:59 -07:00

339 lines
13 KiB
Python

"""Event tool implementations for the Google Calendar mock."""
from datetime import UTC, datetime
from pydantic import EmailStr, ValidationError
from google_calendar.models import (
EVENT_TYPE_PROPERTY_FIELDS,
AttendeeResponseStatus,
BirthdayProperties,
CalendarId,
CalendarPerson,
Event,
EventAttendee,
EventClearField,
EventDateTime,
EventId,
EventReminders,
EventSource,
EventSummary,
EventTransparency,
EventType,
ExtendedProperties,
FocusTimeProperties,
OutOfOfficeProperties,
RecurrenceRuleString,
WorkingLocationProperties,
dump_model,
event_attendee_payload,
)
from google_calendar.state import (
delete_calendar_event,
generate_event_id,
get_calendar_events,
load_data,
save_data_or_error,
set_calendar_event,
)
def create_event(
summary: EventSummary,
start: EventDateTime,
end: EventDateTime,
description: str | None = None,
location: str | None = None,
calendar_id: CalendarId = "primary",
recurrence: list[RecurrenceRuleString] | None = None,
reminders: EventReminders | None = None,
attendees: list[EventAttendee] | None = None,
creator: CalendarPerson | None = None,
organizer: CalendarPerson | None = None,
extendedProperties: ExtendedProperties | None = None,
source: EventSource | None = None,
transparency: EventTransparency | None = None,
eventType: EventType = EventType.DEFAULT,
outOfOfficeProperties: OutOfOfficeProperties | None = None,
focusTimeProperties: FocusTimeProperties | None = None,
workingLocationProperties: WorkingLocationProperties | None = None,
birthdayProperties: BirthdayProperties | None = None,
) -> dict:
"""
Creates a new event in Google Calendar.
Args:
summary: Event title
start: Start time object with either 'dateTime' (ISO format, e.g. '2025-12-10T09:00:00-05:00')
for timed events, or 'date' (YYYY-MM-DD, e.g. '2025-12-10') for all-day events.
Optionally include 'timeZone' (e.g. 'America/New_York').
end: End time object with either 'dateTime' or 'date' (same format as start).
For all-day events, 'date' is exclusive (e.g. end '2025-12-11' means the event ends on 2025-12-10).
description: Event description
location: Event location
calendar_id: Calendar to create the event in (default 'primary')
recurrence: RRULE strings for recurring events (e.g., ['RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=10'])
reminders: Reminder overrides, e.g. {"useDefault": false, "overrides": [{"method": "popup", "minutes": 15}]}
attendees: List of attendees, e.g. [{"email": "alice@co.com", "displayName": "Alice"}]. responseStatus defaults to 'needsAction'.
transparency: Whether the event blocks availability. 'opaque' is busy; 'transparent' is free.
Returns:
The created event object with its ID
"""
data = load_data()
event_id = generate_event_id()
now = datetime.now(UTC).isoformat().replace("+00:00", "Z")
event: dict = {
"id": event_id,
"summary": summary,
"start": dump_model(start, EventDateTime),
"end": dump_model(end, EventDateTime),
"created": now,
"updated": now,
"eventType": EventType(eventType).value,
}
if description:
event["description"] = description
if location:
event["location"] = location
if recurrence:
event["recurrence"] = recurrence
if reminders:
event["reminders"] = dump_model(reminders, EventReminders)
if attendees:
event["attendees"] = [event_attendee_payload(attendee) for attendee in attendees]
if creator is not None:
event["creator"] = dump_model(creator, CalendarPerson)
if organizer is not None:
event["organizer"] = dump_model(organizer, CalendarPerson)
if extendedProperties is not None:
event["extendedProperties"] = dump_model(extendedProperties, ExtendedProperties)
if source is not None:
event["source"] = dump_model(source, EventSource)
if transparency is not None:
event["transparency"] = EventTransparency(transparency).value
if outOfOfficeProperties is not None:
event["outOfOfficeProperties"] = dump_model(outOfOfficeProperties, OutOfOfficeProperties)
if focusTimeProperties is not None:
event["focusTimeProperties"] = dump_model(focusTimeProperties, FocusTimeProperties)
if workingLocationProperties is not None:
event["workingLocationProperties"] = dump_model(workingLocationProperties, WorkingLocationProperties)
if birthdayProperties is not None:
event["birthdayProperties"] = dump_model(birthdayProperties, BirthdayProperties)
try:
event = Event.model_validate(event).model_dump(exclude_none=True)
except ValidationError as e:
return {"status": "error", "message": str(e)}
if not set_calendar_event(data, calendar_id, event_id, event):
return {"status": "error", "message": f"Calendar '{calendar_id}' not found"}
if error := save_data_or_error(data):
return error
return {"status": "success", "event": event}
def get_event(eventId: EventId, calendar_id: CalendarId = "primary") -> dict:
"""
Retrieves details of a specific event.
Args:
eventId: ID of the event to retrieve
calendar_id: Calendar to look in (default 'primary')
Returns:
The event object if found
"""
data = load_data()
events = get_calendar_events(data, calendar_id)
if eventId not in events:
return {"status": "error", "message": f"Event with ID '{eventId}' not found in calendar '{calendar_id}'"}
return {"status": "success", "event": events[eventId]}
def update_event(
eventId: EventId,
summary: EventSummary | None = None,
start: EventDateTime | None = None,
end: EventDateTime | None = None,
description: str | None = None,
location: str | None = None,
calendar_id: CalendarId = "primary",
recurrence: list[RecurrenceRuleString] | None = None,
reminders: EventReminders | None = None,
attendees: list[EventAttendee] | None = None,
creator: CalendarPerson | None = None,
organizer: CalendarPerson | None = None,
extendedProperties: ExtendedProperties | None = None,
source: EventSource | None = None,
transparency: EventTransparency | None = None,
eventType: EventType | None = None,
outOfOfficeProperties: OutOfOfficeProperties | None = None,
focusTimeProperties: FocusTimeProperties | None = None,
workingLocationProperties: WorkingLocationProperties | None = None,
birthdayProperties: BirthdayProperties | None = None,
clear_fields: list[EventClearField] | None = None,
) -> dict:
"""
Updates an existing event.
Args:
eventId: ID of the event to update
summary: New event title
start: New start time object with either 'dateTime' (ISO format) for timed events,
or 'date' (YYYY-MM-DD) for all-day events. Optionally include 'timeZone'.
end: New end time object with either 'dateTime' or 'date' (same format as start).
description: New event description
location: New event location
calendar_id: Calendar containing the event (default 'primary')
recurrence: RRULE strings for recurring events (pass empty list to remove recurrence)
reminders: Reminder overrides (pass {"useDefault": true} to reset to defaults)
attendees: Replace attendee list (pass empty list to remove all attendees)
transparency: Whether the event blocks availability. 'opaque' is busy; 'transparent' is free.
eventType: Event type to set. Switching types clears properties that only belong to the old type.
clear_fields: Optional event fields to remove, e.g. ['source', 'transparency'].
Returns:
The updated event object
"""
data = load_data()
events = get_calendar_events(data, calendar_id)
if eventId not in events:
return {"status": "error", "message": f"Event with ID '{eventId}' not found in calendar '{calendar_id}'"}
event = dict(events[eventId])
if clear_fields:
fields_to_clear = {field.value for field in clear_fields}
for field in fields_to_clear:
event.pop(field, None)
if summary is not None:
event["summary"] = summary
if start is not None:
event["start"] = dump_model(start, EventDateTime)
if end is not None:
event["end"] = dump_model(end, EventDateTime)
if description is not None:
event["description"] = description
if location is not None:
event["location"] = location
if recurrence is not None:
if recurrence:
event["recurrence"] = recurrence
else:
event.pop("recurrence", None)
if reminders is not None:
event["reminders"] = dump_model(reminders, EventReminders)
if attendees is not None:
event["attendees"] = [event_attendee_payload(attendee) for attendee in attendees]
if creator is not None:
event["creator"] = dump_model(creator, CalendarPerson)
if organizer is not None:
event["organizer"] = dump_model(organizer, CalendarPerson)
if extendedProperties is not None:
event["extendedProperties"] = dump_model(extendedProperties, ExtendedProperties)
if source is not None:
event["source"] = dump_model(source, EventSource)
if transparency is not None:
event["transparency"] = EventTransparency(transparency).value
if eventType is not None:
event_type = EventType(eventType)
event["eventType"] = event_type.value
for property_event_type, property_field in EVENT_TYPE_PROPERTY_FIELDS.items():
if property_event_type != event_type:
event.pop(property_field, None)
if outOfOfficeProperties is not None:
event["outOfOfficeProperties"] = dump_model(outOfOfficeProperties, OutOfOfficeProperties)
if focusTimeProperties is not None:
event["focusTimeProperties"] = dump_model(focusTimeProperties, FocusTimeProperties)
if workingLocationProperties is not None:
event["workingLocationProperties"] = dump_model(workingLocationProperties, WorkingLocationProperties)
if birthdayProperties is not None:
event["birthdayProperties"] = dump_model(birthdayProperties, BirthdayProperties)
event["updated"] = datetime.now(UTC).isoformat().replace("+00:00", "Z")
try:
event = Event.model_validate(event).model_dump(exclude_none=True)
except ValidationError as e:
return {"status": "error", "message": str(e)}
events[eventId] = event
if error := save_data_or_error(data):
return error
return {"status": "success", "event": event}
def delete_event(eventId: EventId, calendar_id: CalendarId = "primary") -> dict:
"""
Deletes an event from the calendar.
Args:
eventId: ID of the event to delete
calendar_id: Calendar containing the event (default 'primary')
Returns:
Confirmation of deletion
"""
data = load_data()
deleted_event = delete_calendar_event(data, calendar_id, eventId)
if deleted_event is None:
return {"status": "error", "message": f"Event with ID '{eventId}' not found in calendar '{calendar_id}'"}
if error := save_data_or_error(data):
return error
return {"status": "success", "message": f"Event '{deleted_event['summary']}' deleted"}
def respond_to_event(
eventId: EventId,
email: EmailStr,
response: AttendeeResponseStatus,
calendar_id: CalendarId = "primary",
) -> dict:
"""
Update an attendee's RSVP response on an event.
Args:
eventId: ID of the event
email: Email address of the attendee responding
response: Response status — 'accepted', 'declined', 'tentative', or 'needsAction'
calendar_id: Calendar containing the event (default 'primary')
Returns:
The updated event with attendee responses
"""
data = load_data()
events = get_calendar_events(data, calendar_id)
if eventId not in events:
return {"status": "error", "message": f"Event with ID '{eventId}' not found in calendar '{calendar_id}'"}
event = events[eventId]
attendees = event.get("attendees", [])
found = False
for attendee in attendees:
if attendee.get("email", "").lower() == email.lower():
attendee["responseStatus"] = response.value
found = True
break
if not found:
return {"status": "error", "message": f"Attendee '{email}' not found on this event"}
event["updated"] = datetime.now(UTC).isoformat().replace("+00:00", "Z")
if error := save_data_or_error(data):
return error
return {"status": "success", "event": event}