heinzel-google
Google Docs, Sheets, Drive, Calendar and Gmail: the documents (read), calendar and mail (read and write)
capabilities (ADR 0028): a first-party
Heinzelmännchen plugin. It is independently released and installed explicitly:
hz system plugin add google --confirm # a compatible plugin release; run it again to upgrade
hz system plugin remove heinzel-google
It is the default provider for Google Workspace, not the only possible one: workflows ask for documents,
calendar and mail, and any other plugin that provides those capabilities can be bound instead, in configuration.
How it reaches Google
Through the open-source Google Workspace CLI, gws (0.22 or newer): it
signs in with Google, keeps its own credentials (encrypted, with the key in the OS keychain, by default) and calls
Google's APIs. This plugin never sees a token. It runs gws <service> <resource> <method> --params <JSON> --format json as an argument list (never through a shell), with the executable resolved to an absolute path, from the root
directory (so no project's .env reaches gws), with an allowlisted environment (never Heinzelmännchen's own
credentials or other secrets; of gws's own variables only GOOGLE_WORKSPACE_CLI_CONFIG_DIR, _KEYRING_BACKEND,
_CLIENT_ID, _CLIENT_SECRET and GOOGLE_WORKSPACE_PROJECT_ID, not its token, credentials-file, sanitizing or log
variables), a
timeout that stops gws and anything it started, and at most 10 MB of output read. Why gws:
ADR 0042.
What leaves this machine, and where to: file ids, the Drive query (the modified-time window and the --name text)
the calendar window and the mail search, to Google's API hosts (www.googleapis.com for Drive and Calendar,
docs.googleapis.com, sheets.googleapis.com, gmail.googleapis.com); gws also fetches API descriptions from Google and refreshes its sign-in at
oauth2.googleapis.com. Nothing goes anywhere else: gws 0.22 documents no telemetry or update check, and none was
found in it.
Set up
Install gws (it needs Node.js), give it an OAuth client once (gws auth setup, which needs gcloud, or a Desktop
OAuth client as its README describes), then let setup do the rest:
npm install -g @googleworkspace/cli
hz setup google # finds gws, signs it in, binds documents, calendar and (if you say so) mail
hz system doctor # finds gws, checks its version and its stored sign-in, from files; Google isn't contacted
By hand, setup is:
gws auth login --scopes https://www.googleapis.com/auth/documents.readonly,https://www.googleapis.com/auth/spreadsheets.readonly,https://www.googleapis.com/auth/drive.metadata.readonly,https://www.googleapis.com/auth/calendar.readonly
hz system config set capabilities.documents google
hz system config set capabilities.calendar google
Mail is opt-in: setup asks, defaulting to no, and then signs in again with Gmail's read-only scope added
(,https://www.googleapis.com/auth/gmail.readonly), which lets the plugin read all of your mail; then
hz system config set capabilities.mail google.
Writes are opt-in too. Setup asks whether hz may change your calendar and your mail, defaulting to no; only then does
the sign-in ask for https://www.googleapis.com/auth/calendar.events (create, move, cancel and answer events) and
https://www.googleapis.com/auth/gmail.modify (keep drafts, send them, file conversations; it reads too, so it
replaces gmail.readonly, and it can't delete mail for good). Heinzelmännchen writes only from publish operations
that show you what they will do and run after you confirm; a chat's agent only proposes them
(ADR 0059). Without the
scope, a write fails with the gws auth login that adds it.
These are the narrowest read-only scopes for what the adapters do; drive.metadata.readonly lists and identifies files
and never reads their contents (Google classes it as restricted, which matters only for published OAuth clients).
Bindings go in your user configuration, never in a project hz.toml, which Heinzelmännchen ignores for them.
Optional settings, in [google] or HZ_GOOGLE__<FIELD>: gws_path, the absolute path of gws when it isn't on
PATH (only absolute PATH entries are searched; both are ignored in a project hz.toml: a repository someone else
controls must not choose the program that reads your data), and timeout, the seconds each gws run may take
(default 60, also ignored in a project hz.toml).
Is it connected?
Bound automatically to documents and calendar while it is the only installed adapter of them, ADR
0055; mail
never is, so Gmail stays opt-in: hz setup google asks. hz system connector list shows whether gws has a sign-in, from its files alone; hz system connector test google asks Drive who is signed in (gws drive about get, 15 s at most), and only when you run it.
Use
Workflows ask for documents and calendar, never for Google:
from pydantic import AwareDatetime
from heinzel import Calendar, Document, Documents, Event, Module, Window
module = Module("prep", "One-on-one preparation.", requires=["documents:read", "calendar:read"])
@module.operation
def week(calendar: Calendar, since: AwareDatetime, until: AwareDatetime) -> list[Event]:
"""This week's events."""
return calendar.events(Window(since=since, until=until))
@module.operation
def notes(documents: Documents) -> Document:
"""The running notes, as Markdown."""
return documents.read("https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz/edit")
| Method | Capability | gws command |
|---|---|---|
read(ref) |
documents:read |
drive files get (metadata), then docs documents get with includeTabsContent or sheets spreadsheets get with includeGridData |
events(window) |
calendar:read |
calendar events list on primary, singleEvents, showDeleted, paged by pageToken |
messages(query, window) |
mail:read |
gmail users messages list with Gmail's search, then messages get (metadata) for each |
read(message) |
mail:read |
gmail users messages get (full) |
free_busy(people, window) |
calendar:read |
calendar freebusy query for primary and each address |
create_event(event) |
calendar:write |
calendar events insert on primary with an id of hz's own (no duplicate on a retry), sendUpdates as the draft says, a Meet link when asked |
update_event(event, change) |
calendar:write |
calendar events get, then patch (only your events, or ones whose guests may change them) |
cancel_event(event, notify) |
calendar:write |
calendar events get, then delete (only your events; decline others') |
respond(event, response) |
calendar:write |
calendar events get, then patch of your own attendee entry |
thread(thread) |
mail:read |
gmail users threads get (full), without drafts, Spam and Trash |
draft(draft) |
mail:read |
gmail users drafts get (full) |
create_draft(draft) |
mail:write |
gmail users drafts create with a plain-text MIME message; a reply with threadId, In-Reply-To and References |
send_draft(draft, seen) |
mail:write |
gmail users drafts get, then send if its message is still the one you saw |
relabel(thread, add, remove) |
mail:write |
gmail users threads modify with label ids; not Spam, Trash, Drafts or Sent |
- Documents are a Docs, Sheets or Drive link (
https://docs.google.com/document/d/<id>/...,.../spreadsheets/d/<id>/...,https://drive.google.com/file/d/<id>/...,.../open?id=<id>) or a bare file id; links to any other host are refused beforegwsruns, and a Drive shortcut is followed once. A Doc comes back as Markdown-flavoured text, every tab under its title: headings, lists, bold and italic, links, tables; person chips by name, never their email, and mail links by their text; titles and link targets can't start a line of their own; images and drawings are left out. A Sheet comes back as one## <sheet>table per sheet with the values as people see them.revisionis the Doc's revision id or the Sheet's Drive version. Uploaded Word or Excel files are refused: open them as a Google Doc or Sheet first. - Events are your primary calendar's, every occurrence of a recurring event, cancelled ones included, by start.
Attendees are display names and emails, without meeting rooms; the workflow packet builder drops the emails before
anything reaches an agent's prompt.
conference_idis the Zoom meeting number when the event has a Zoom link (in its conferencing data, location or description), else the Meet code, so themeetingscapability can find the event's meeting; conference links are kept only on the conferencing services' own hosts (Meet, Zoom, Teams, Webex), without their query, where passcodes live. A display name that is an address is dropped. - Mail is your Gmail:
messagestakes the shared query syntax (from:<address or domain> subject:<text> label:<name>) and a window. The window and the sender go into Gmail's search and the label as its id; the subject is matched by the plugin, because Gmail'ssubject:matches whole words only. Results come oldest first, at most 200 messages a window (onegwsrun each).readgives the text part, or the HTML part as text, in the part's own charset, without attachments, and only the headers that carry no addresses. Senders and recipients are names and emails, parsed before any encoded name is decoded, so a display name can't pose as an address; the workflow packet builder drops the addresses before anything reaches an agent's prompt. A message longer than 2 million characters, or with more than 500 MIME parts or 2 million characters of HTML, is refused, not cut; Spam and Trash are not read; reading one window's messages may take at most five minutes. - Writes change only what the person confirmed, through publish operations. Ids are checked before they reach
gws; a write that times out or fails without Google refusing it is not retried and says to look in Google first; a rate-limited one is retried. A recurring series' own id is refused: one occurrence changes at a time. Drafts are built with Python'semailpackage from plain addresses and one-line subjects, so nothing can add a header. - Text is data. Every string returned passes
heinzel.clean_text(no control, invisible or bidirectional characters). - Bounds. One
gwsrun prints at most 10 MB, one call reads at most 100 pages or 25 MB, a Doc's text at most four times its JSON, tables and tabs nest at most 8 deep, a Sheet at most a million cells; past that, an error says to narrow the window or that the file is too large. Rate limits are honoured: a rate-limited call is retried after 1, 2 and 4 seconds, then fails saying when to retry.
hz google info # this plugin, its version, the gws it uses, the scopes and APIs
hz google files --days 3 # Docs and Sheets you can open that changed in the last three days
hz system capability list # who reads what, through which adapter and hosts
Errors say what to do: no gws names the install; not signed in or an expired sign-in names the exact
gws auth login command above (doctor names hz setup google); a missing scope or a disabled API says which; a file the account can't see says so.
Token-shaped strings in anything gws says are redacted before it is shortened.
Development and release commands: repository guide.
Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
Metadata
Release files for heinzel-google 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| heinzel_google-0.9.1.tar.gz | 151.7 kB | Without provenance | Details for heinzel_google-0.9.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| heinzel_google-0.9.1-py3-none-any.whl | Python 3 | none | any | Details for heinzel_google-0.9.1-py3-none-any.whl |
Total release size: 201.5 kB
Release files / heinzel_google-0.9.1.tar.gz
| Download URL | heinzel_google-0.9.1.tar.gz |
|---|---|
| Size | 151.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d21862ba257c16e6c50ebed1bacd646458d7b5f436cb58592e40d56ccc544f43
|
|
BLAKE2b-256 checksum How to use checksums |
04f10f2c49c664d70d03653f1c9365ef0215727969ddc9eb92d6d7add649155d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / heinzel_google-0.9.1-py3-none-any.whl
| Download URL | heinzel_google-0.9.1-py3-none-any.whl |
|---|---|
| Size | 49.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef7a9090e9f3c8803f3b6546d6629614e637f25346f766f697827a0da132e81a
|
|
BLAKE2b-256 checksum How to use checksums |
51e4c0e4073c53bdbd4139969b431a1d506a9b335c84ad10460c1e2b7253bcf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for heinzel-google 0.9.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.