13 toolsOAuth bearer
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
This pack takes a Google OAuth bearer token, andconfigure() is optional when $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? Your own account 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.
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 is not: it wantssingleEvents, maxResults, timeMin, orderBy.
Send single_events=true and Google does not complain. It returns 200 with a
correct-looking list of events — recurring ones unexpanded, because the parameter
that asked for expansion was a name it did not recognise and dropped. The agent
then reasons over a wrong answer that arrived with every sign of being right.
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