gcalendar_example.py
"primary" as calendar_id
for the authenticated account’s own calendar, or the calendar’s address for a
shared one. calendar_list_list is where an address comes from when you do not
already hold one.
Four tools is the whole pack, and the interesting part is not the surface. It is
that events_list takes seventeen filter parameters beside the calendar id, and
every one of them fails silently if it goes out under the wrong name.
Authenticating
Before the first call, Google Calendar needs a Google OAuth client. Google issues no API key for it: credentials come only from an app you register in the Cloud console. Set up Google walks through every screen — about ten minutes, done once, and the same client serves all six Google packs. This pack takes a Google OAuth bearer token, andconfigure() is optional when a renewable grant or $GOOGLE_ACCESS_TOKEN is set. Which credential provider you hand it depends on whose account the calls run as.
Refer to Google’s provider page for the “GOOGLE” constant the snippets below name, the scopes these 13 tools ask for, and this server’s refresh behaviour.
A token you hold
For a script, or a notebook.EnvTokenProvider re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; StaticTokenProvider takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.
gcalendar_script.py
One account, refreshed
For an agent or a server acting as you.OAuth2Client turns a client registration and a stored refresh token into an access token, and renews it before it lapses.
gcalendar_agent.py
No refresh token yet? Set up Google is the one-time consent flow that hands you one.
Many end users
For a product whose users each connect their own account.SubjectProvider builds one credential per user through a factory you write, and use_subject names the user a call acts for. Your users’ accounts is the consent route inside your app; serving many users is the per-subject cache and its eviction.
The client is handed a function that reads the stored grant, not a copy of the refresh token. A server runs several workers, and a copy goes stale the first time another worker refreshes: More than one process.
gcalendar_server.py
calendar.events covers events on calendars the account can already reach. It
does not cover reading which calendars those are, so calendar_list_list asks
for calendar.readonly instead and consent has to name both.
The client
oauth_tool_factory is the whole client: a thin wrapper over httpx that attaches your token and these endpoint constants to each request. google-api-python-client and google-auth do not enter your dependency tree.credentials is whichever of the three you built in Authenticating. A pack takes it through configure(); a client you build takes the same object as credential_provider, and has no configure() of its own.
Paging through a list
A cursor belongs to the tool that returns it, so it is declared on that tool’s builder call:gcalendar_pagination.py
Pagination is declared on
events_list, events_instances and calendar_list_list. The other 10 take no cursor.Tools
Each is aTool, called with
ainvoke as in the snippet above. The name
links to its parameters, its response and what it costs.
Eventsevents_getGETGet one event by its ID.events_listGETList events matching a given search filter.events_instancesGETList the individual occurrences of a recurring event, each with its own event ID that can be updated or deleted on its own.events_insertPOSTCreate a calendar event; returns details of the event.events_updatePUTReplace an event in full.events_patchPATCHChange part of an event.events_movePOSTMove an event to another calendar, changing its organizer.events_quick_addPOSTCreate an event from a plain-text description such as ‘Appointment at Somewhere on June 3rd 10am-10:25am’.events_importPOSTImport an event that already exists elsewhere, keeping its iCalUID so the two copies stay identifiable as the same event.events_deleteDELETEDelete an event from the calendar.CalendarListcalendar_list_listGETList the calendars on the user’s calendar list.calendar_list_getGETGet one calendar from the user’s calendar list, with their access role for it.Calendarcalendars_getGETGet a calendar’s own metadata, including the time zone its events are read against.
Why query casing is the load-bearing line
Charter’s default query casing is snake, because most REST APIs are snake. Google documents camelCase:singleEvents, maxResults, timeMin, iCalUID.
Calendar answers 200 to a query parameter it does not recognise, and ignores it.
It does recognise some snake_case spellings, the ones that match its own internal
field names: single_events and max_results both took effect when tested on
2026-10-24. i_cal_uid did not. Filtering by i_cal_uid= an id that matches
nothing returned all 94 of the test calendar’s events, where iCalUID= returned
none. The agent then reasons over a wrong answer that arrived with every sign of
being right, and which spellings happen to work is nowhere documented.
So the factory declares query_case="camel", and the
key case cascade converts every parameter on its way
out. The schema stays snake, which is what a Python caller expects; the wire gets
camel, which is what Google requires. The
conformance suite checks this for every Google pack,
because it is the kind of mistake that never surfaces as an error.
Gotchas
The event body is one field, named event
The event body is one field, named event
events_insert takes the event as a single event argument, with the query
parameters (send_updates, conference_data_version, max_attendees)
beside it rather than inside it.Times are RFC 3339, and the offset is not optional
Times are RFC 3339, and the offset is not optional
time_min, time_max and an event’s start.dateTime are RFC 3339
timestamps with a UTC offset — 2026-09-02T10:00:00+02:00 or
...Z. Google rejects a naive timestamp. An all-day event uses date
instead of dateTime, and the two are mutually exclusive.Recurring events need single_events
Recurring events need single_events
Without
single_events=true, a weekly standup is one event with a
recurrence rule, not the instances an agent is usually asking about. With it,
Google expands the series and order_by="startTime" becomes legal — it is
rejected otherwise.events_delete has no counterpart here
events_delete has no counterpart here
There is no restore tool in this pack: once an agent calls
events_delete,
undoing it is a job for the Calendar UI. send_updates decides whether
attendees are notified of the cancellation, and it is a query parameter on
the delete call rather than a property of the event.Related
- Google — consent screen, scopes, refresh
- Key case cascade — where
single_eventsbecomessingleEvents