heinzel-google 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. heinzel_google-0.9.1/.gitignore +15 -0
  2. heinzel_google-0.9.1/.hypothesis/.gitignore +9 -0
  3. heinzel_google-0.9.1/.hypothesis/constants/037c2f619c5f5ee0 +4 -0
  4. heinzel_google-0.9.1/.hypothesis/constants/32d6ed43e73e9c4d +4 -0
  5. heinzel_google-0.9.1/.hypothesis/constants/4ed33d3d987f61a4 +4 -0
  6. heinzel_google-0.9.1/.hypothesis/constants/67b0a8ccf18bf5d2 +4 -0
  7. heinzel_google-0.9.1/.hypothesis/constants/68ca2fef151a93d8 +4 -0
  8. heinzel_google-0.9.1/.hypothesis/constants/6da411fe19b8f4f4 +4 -0
  9. heinzel_google-0.9.1/.hypothesis/constants/6dcaadd2e5dfed7e +4 -0
  10. heinzel_google-0.9.1/.hypothesis/constants/b17d0304d74d3014 +4 -0
  11. heinzel_google-0.9.1/.hypothesis/constants/b4107e19f0c382e0 +4 -0
  12. heinzel_google-0.9.1/.hypothesis/constants/bbfc298edfa6d48e +4 -0
  13. heinzel_google-0.9.1/.hypothesis/constants/be6ec7ab5c779a4a +4 -0
  14. heinzel_google-0.9.1/.hypothesis/constants/bf69a526b8fa5d9c +4 -0
  15. heinzel_google-0.9.1/.hypothesis/constants/ca673ccd04b58ff7 +4 -0
  16. heinzel_google-0.9.1/.hypothesis/unicode_data/15.0.0/charmap.json.gz +0 -0
  17. heinzel_google-0.9.1/.hypothesis/unicode_data/15.0.0/codec-v2-utf-8.json.gz +0 -0
  18. heinzel_google-0.9.1/CHANGELOG.md +8 -0
  19. heinzel_google-0.9.1/LICENSE +21 -0
  20. heinzel_google-0.9.1/PKG-INFO +182 -0
  21. heinzel_google-0.9.1/README.md +169 -0
  22. heinzel_google-0.9.1/plugin-lock.json +41 -0
  23. heinzel_google-0.9.1/pyproject.toml +131 -0
  24. heinzel_google-0.9.1/requirements.lock +1241 -0
  25. heinzel_google-0.9.1/src/heinzel_google/__init__.py +133 -0
  26. heinzel_google-0.9.1/src/heinzel_google/_calendar.py +422 -0
  27. heinzel_google-0.9.1/src/heinzel_google/_docs.py +237 -0
  28. heinzel_google-0.9.1/src/heinzel_google/_documents.py +193 -0
  29. heinzel_google-0.9.1/src/heinzel_google/_gws.py +542 -0
  30. heinzel_google-0.9.1/src/heinzel_google/_mail.py +649 -0
  31. heinzel_google-0.9.1/src/heinzel_google/_models.py +72 -0
  32. heinzel_google-0.9.1/src/heinzel_google/_module.py +7 -0
  33. heinzel_google-0.9.1/src/heinzel_google/_service.py +141 -0
  34. heinzel_google-0.9.1/src/heinzel_google/_setup.py +120 -0
  35. heinzel_google-0.9.1/src/heinzel_google/_sheets.py +102 -0
  36. heinzel_google-0.9.1/src/heinzel_google/py.typed +0 -0
  37. heinzel_google-0.9.1/tests/conftest.py +210 -0
  38. heinzel_google-0.9.1/tests/integration/capability_test.py +226 -0
  39. heinzel_google-0.9.1/tests/integration/cli_test.py +63 -0
  40. heinzel_google-0.9.1/tests/integration/connector_test.py +75 -0
  41. heinzel_google-0.9.1/tests/integration/runner_test.py +125 -0
  42. heinzel_google-0.9.1/tests/unit/calendar_test.py +503 -0
  43. heinzel_google-0.9.1/tests/unit/contract_test.py +50 -0
  44. heinzel_google-0.9.1/tests/unit/docs_test.py +310 -0
  45. heinzel_google-0.9.1/tests/unit/documents_test.py +184 -0
  46. heinzel_google-0.9.1/tests/unit/gws_test.py +366 -0
  47. heinzel_google-0.9.1/tests/unit/mail_test.py +590 -0
  48. heinzel_google-0.9.1/tests/unit/service_test.py +136 -0
  49. heinzel_google-0.9.1/tests/unit/setup_test.py +160 -0
  50. heinzel_google-0.9.1/tests/unit/sheets_test.py +51 -0
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ .tools/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .pyright/
7
+ .coverage*
8
+ htmlcov/
9
+ node_modules/
10
+ dist/
11
+ release-dist/
12
+ *.egg-info/
13
+ .acceptance/
14
+ .preview/
15
+ .candidates/
@@ -0,0 +1,9 @@
1
+ # This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
2
+ # .hypothesis by default, because we generally recommend that .hypothesis not be checked
3
+ # into version control.
4
+ #
5
+ # If you *would* like to check .hypothesis into version control, you should delete this
6
+ # file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
7
+ # if it does, that's a bug - please report it!)
8
+
9
+ *
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_calendar.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [250, 300, 100000, '#', '(no title)', '+00:00', '.webex.com', '.zoom.us', '?', '@', 'Google Calendar', 'Json', 'Z', '\\', '^[a-z0-9.-]+$', 'accepted', 'all', 'attendees', 'attendeesOmitted', 'busy', 'calendar', 'calendarId', 'calendars', 'cancelled', 'conferenceData', 'conferenceId', 'confirmed', 'createRequest', 'date', 'dateTime', 'declined', 'delete', 'description', 'displayName', 'email', 'end', 'entryPointType', 'entryPoints', 'errors', 'eventId', 'events', 'free and busy times', 'freebusy', 'get', 'guestsCanModify', 'hangoutLink', 'hangoutsMeet', 'htmlLink', 'https', 'id', 'insert', 'items', 'list', 'list[object]', 'location', 'maxResults', 'meet.google.com', 'needsAction', 'none', 'organizer', 'originalStartTime', 'patch', 'pending', 'primary', 'query', 'recurrence', 'requestId', 'resource', 'responseStatus', 'self', 'sendUpdates', 'showDeleted', 'singleEvents', 'start', 'status', 'summary', 'teams.live.com', 'teams.microsoft.com', 'tentative', 'timeMax', 'timeMin', 'timeZone', 'type', 'uri', 'video', 'zoom.us']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_docs.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [1000000, 20000000, '\n---\n', ' --- |', ' |', ' | ', '#', '(', ')', '-', 'Json', 'NONE', 'TITLE', 'Untitled tab', '[', '[ \\t]+', '\\n{3,}', '\\|', ']', 'body', 'bold', 'bullet', 'childTabs', 'content', 'documentTab', 'elements', 'glyphSymbol', 'glyphType', 'horizontalRule', 'http://', 'https://', 'italic', 'link', 'listId', 'listProperties', 'list[object]', 'lists', 'name', 'namedStyleType', 'nestingLevel', 'nestingLevels', 'paragraph', 'paragraphStyle', 'person', 'personProperties', 'richLink', 'richLinkProperties', 'someone', 'tabProperties', 'table', 'tableCells', 'tableRows', 'tabs', 'textRun', 'textStyle', 'title', 'uri', 'url', '|', '| ']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/.venv/bin/coverage
2
+ # hypothesis_version: 6.170.13
3
+
4
+ ['-script.pyw', '.exe', '__main__']
@@ -0,0 +1,4 @@
1
+ # file: /usr/lib/python3.12/sitecustomize.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ []
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_module.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ ['google']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/__init__.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [366, 'CALENDAR_HOSTS', 'CALENDAR_WRITE_SCOPE', 'DOCUMENT_HOSTS', 'Drive', 'DriveFile', 'Gmail', 'GoogleAbout', 'GoogleCalendar', 'GoogleDocuments', 'GoogleService', 'GoogleSession', 'GoogleSettings', 'Gws', 'LOGIN', 'LOGIN_WITH_MAIL', 'MAIL_HOSTS', 'MAIL_SCOPE', 'MAIL_WRITE_SCOPE', 'Process', 'SCOPES', 'calendar', 'docs.googleapis.com', 'documents', 'gmail.googleapis.com', 'login_command', 'mail', 'module', 'read', 'write', 'www.googleapis.com']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_sheets.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [1000000, ' --- |', ' |', ' | ', '(a chart, not read)', '(empty)', 'Json', 'OBJECT', 'Untitled sheet', '\\|', 'data', 'formattedValue', 'list[object]', 'properties', 'rowData', 'sheetType', 'sheets', 'title', 'values', '|', '| ']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_service.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [15.0, '0.0.0', 'Google Drive', 'about', 'bindings', 'calendar', 'dict[str, Any]', 'documents', 'drive', 'drive about get', 'emailAddress', 'fields', 'get', 'google', 'google.com', 'gws', 'heinzel-google', 'mail', 'user', 'your account']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_models.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [60.0, 600, 'Google Workspace CLI', 'Time limit', 'format', 'gws_path', 'path', 'seconds', 'timeout', 'x-expert', 'x-unit']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_mail.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [b'-_', 300.0, 100, 200, 500, 1000, 10000, 2000000, '"', ', ', '-', '-->', '/', '<', '<!--', '=', '>', '@', 'CATEGORY_', 'Cc', 'Content-Type', 'DRAFT', 'From', 'Gmail', 'IMPORTANT', 'INBOX', 'In-Reply-To', 'Json', 'Message-ID', 'References', 'Reply-To', 'STARRED', 'Subject', 'To', 'UNREAD', '\\', '\\n{3,}', '_', '_is_text_encoding', 'addLabelIds', 'ascii', 'blockquote', 'body', 'br', 'cc', 'content-type', 'conversation', 'create', 'data', 'div', 'draft', 'drafts', 'emailAddress', 'filename', 'format', 'from', 'full', 'get', 'getProfile', 'gmail', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'headers', 'hr', 'id', 'internalDate', 'labelIds', 'labels', 'li', 'list', 'list[object]', 'maxResults', 'me', 'message', 'message-id', 'messages', 'metadata', 'metadataHeaders', 'mimeType', 'modify', 'name', 'p', 'parts', 'payload', 'q', 'raw', 'raw_unicode_escape', 're:', 'references', 'removeLabelIds', 'replace', 'reply-to', 'script', 'send', 'spam', 'style', 'subject', 'table', 'template', 'text/html', 'text/plain', 'the labels', 'the new draft', 'threadId', 'threads', 'title', 'to', 'tr', 'trash', 'unicode_escape', 'userId', 'users', 'utf-8', 'value', 'your address']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_gws.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [2.0, 10.0, 100, 300, 400, 401, 403, 404, 409, 429, 500, 65536, 10000000, 25000000, ' google.timeout.', '***', ',', '--format', '--json', '--params', '--version', '.', '.config', '/', ':', 'Gmail', 'HOME', 'HTTPS_PROXY', 'HTTP_PROXY', 'IO[bytes]', 'LANG', 'LC_', 'LOGNAME', 'NODE_EXTRA_CA_CERTS', 'NO_PROXY', 'PATH', 'RATE_LIMIT_EXCEEDED', 'SERVICE_DISABLED', 'SSL_CERT_DIR', 'SSL_CERT_FILE', 'SYSTEMROOT', 'TERM', 'TMPDIR', 'USER', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_RUNTIME_DIR', 'accessNotConfigured', 'calendar', 'client_secret.json', 'code', 'credentials.enc', 'credentials.json', 'details', 'dict[str, Any]', 'error', 'errors', 'gws', 'http_proxy', 'https_proxy', 'hz setup google', 'json', 'list[object]', 'mail', 'message', 'nextPageToken', 'no details', 'no_proxy', 'object', 'pageToken', 'quotaExceeded', 'rateLimitExceeded', 'reason', 'replace', 'retry, or raise']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_documents.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [100, 200, " and name contains '", ' or ', "'", '/open', 'Google Docs', 'Google Drive', 'Google Sheets', '\\', "\\'", '\\\\', 'dict[str, Any]', 'docs', 'docs.google.com', 'document', 'documentId', 'documents', 'drive', 'drive.google.com', 'fields', 'fileId', 'files', 'get', 'https', 'id', 'includeGridData', 'includeTabsContent', 'list', 'mimeType', 'modifiedTime', 'modifiedTime desc', 'name', 'orderBy', 'pageSize', 'properties', 'q', 'revisionId', 'sheets', 'shortcutDetails', 'spreadsheet', 'spreadsheetId', 'spreadsheets', 'supportsAllDrives', 'targetId', 'title', 'version']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/heinzel-plugins/heinzel-plugins/plugins/heinzel-google/src/heinzel_google/_setup.py
2
+ # hypothesis_version: 6.170.13
3
+
4
+ [900.0, ' only', '?', 'OAuth client', 'automatic', 'calendar', 'configured', 'default', 'documents', 'google', 'gws is signed in.', 'gws_path', 'mail', 'read-only access']
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.9.1
4
+
5
+ Move this plugin to its independent repository project for the Heinzel 0.9.1 plugin contract.
6
+ Own its dependencies, tests and artifacts; consume public contracts without concrete provider dependencies.
7
+
8
+ Earlier history is preserved in the source repository named in README.md.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Helmut Hoffer von Ankershoffen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.5
2
+ Name: heinzel-google
3
+ Version: 0.9.1
4
+ Summary: Google Docs, Sheets, Drive, Calendar and Gmail: the documents, calendar and mail capabilities.
5
+ Project-URL: Source, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/tree/main/plugins/heinzel-google
6
+ Project-URL: Changelog, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/blob/main/plugins/heinzel-google/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: heinzel<0.10,>=0.9.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # heinzel-google
15
+
16
+ Google Docs, Sheets, Drive, Calendar and Gmail: the `documents` (read), `calendar` and `mail` (read and write)
17
+ capabilities ([ADR 0028](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0028-integrations-are-capability-adapters.md)): a first-party
18
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin. It is independently released and installed explicitly:
19
+
20
+ ```bash
21
+ hz system plugin add google --confirm # a compatible plugin release; run it again to upgrade
22
+ hz system plugin remove heinzel-google
23
+ ```
24
+
25
+ It is the **default provider** for Google Workspace, not the only possible one: workflows ask for `documents`,
26
+ `calendar` and `mail`, and any other plugin that provides those capabilities can be bound instead, in configuration.
27
+
28
+ ## How it reaches Google
29
+
30
+ Through the open-source [Google Workspace CLI](https://github.com/googleworkspace/cli), `gws` (0.22 or newer): it
31
+ signs in with Google, keeps its own credentials (encrypted, with the key in the OS keychain, by default) and calls
32
+ Google's APIs. This plugin never sees a token. It runs `gws <service> <resource> <method> --params <JSON> --format
33
+ json` as an argument list (never through a shell), with the executable resolved to an absolute path, from the root
34
+ directory (so no project's `.env` reaches `gws`), with an allowlisted environment (never Heinzelmännchen's own
35
+ credentials or other secrets; of gws's own variables only `GOOGLE_WORKSPACE_CLI_CONFIG_DIR`, `_KEYRING_BACKEND`,
36
+ `_CLIENT_ID`, `_CLIENT_SECRET` and `GOOGLE_WORKSPACE_PROJECT_ID`, not its token, credentials-file, sanitizing or log
37
+ variables), a
38
+ timeout that stops `gws` and anything it started, and at most 10 MB of output read. Why `gws`:
39
+ [ADR 0042](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0042-google-workspace-through-the-gws-cli.md).
40
+
41
+ What leaves this machine, and where to: file ids, the Drive query (the modified-time window and the `--name` text)
42
+ the calendar window and the mail search, to Google's API hosts (`www.googleapis.com` for Drive and Calendar,
43
+ `docs.googleapis.com`, `sheets.googleapis.com`, `gmail.googleapis.com`); `gws` also fetches API descriptions from Google and refreshes its sign-in at
44
+ `oauth2.googleapis.com`. Nothing goes anywhere else: gws 0.22 documents no telemetry or update check, and none was
45
+ found in it.
46
+
47
+ ## Set up
48
+
49
+ Install `gws` (it needs Node.js), give it an OAuth client once (`gws auth setup`, which needs `gcloud`, or a Desktop
50
+ OAuth client as its README describes), then let setup do the rest:
51
+
52
+ ```bash
53
+ npm install -g @googleworkspace/cli
54
+ hz setup google # finds gws, signs it in, binds documents, calendar and (if you say so) mail
55
+ hz system doctor # finds gws, checks its version and its stored sign-in, from files; Google isn't contacted
56
+ ```
57
+
58
+ By hand, setup is:
59
+
60
+ ```bash
61
+ 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
62
+ hz system config set capabilities.documents google
63
+ hz system config set capabilities.calendar google
64
+ ```
65
+
66
+ Mail is opt-in: setup asks, defaulting to no, and then signs in again with Gmail's read-only scope added
67
+ (`,https://www.googleapis.com/auth/gmail.readonly`), which lets the plugin read all of your mail; then
68
+ `hz system config set capabilities.mail google`.
69
+
70
+ Writes are opt-in too. Setup asks whether hz may change your calendar and your mail, defaulting to no; only then does
71
+ the sign-in ask for `https://www.googleapis.com/auth/calendar.events` (create, move, cancel and answer events) and
72
+ `https://www.googleapis.com/auth/gmail.modify` (keep drafts, send them, file conversations; it reads too, so it
73
+ replaces `gmail.readonly`, and it can't delete mail for good). Heinzelmännchen writes only from publish operations
74
+ that show you what they will do and run after you confirm; a chat's agent only proposes them
75
+ ([ADR 0059](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0059-the-calendar-and-the-mail-write-through-hz-after-a-confirmation.md)). Without the
76
+ scope, a write fails with the `gws auth login` that adds it.
77
+
78
+ These are the narrowest read-only scopes for what the adapters do; `drive.metadata.readonly` lists and identifies files
79
+ and never reads their contents (Google classes it as restricted, which matters only for published OAuth clients).
80
+ Bindings go in your user configuration, never in a project `hz.toml`, which Heinzelmännchen ignores for them.
81
+
82
+ Optional settings, in `[google]` or `HZ_GOOGLE__<FIELD>`: `gws_path`, the absolute path of `gws` when it isn't on
83
+ `PATH` (only absolute `PATH` entries are searched; both are ignored in a project `hz.toml`: a repository someone else
84
+ controls must not choose the program that reads your data), and `timeout`, the seconds each `gws` run may take
85
+ (default 60, also ignored in a project `hz.toml`).
86
+
87
+ ### Is it connected?
88
+
89
+ Bound automatically to documents and calendar while it is the only installed adapter of them, [ADR
90
+ 0055](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0055-connectors-show-their-sign-in-test-live-on-demand-and-bind-themselves-when-alone.md); mail
91
+ never is, so Gmail stays opt-in: `hz setup google` asks. `hz system connector
92
+ list` shows whether gws has a sign-in, from its files alone; `hz system connector
93
+ test google` asks Drive who is signed in (`gws drive about get`, 15 s at most), and only when you run it.
94
+
95
+ ## Use
96
+
97
+ Workflows ask for `documents` and `calendar`, never for Google:
98
+
99
+ ```python
100
+ from pydantic import AwareDatetime
101
+
102
+ from heinzel import Calendar, Document, Documents, Event, Module, Window
103
+
104
+ module = Module("prep", "One-on-one preparation.", requires=["documents:read", "calendar:read"])
105
+
106
+
107
+ @module.operation
108
+ def week(calendar: Calendar, since: AwareDatetime, until: AwareDatetime) -> list[Event]:
109
+ """This week's events."""
110
+ return calendar.events(Window(since=since, until=until))
111
+
112
+
113
+ @module.operation
114
+ def notes(documents: Documents) -> Document:
115
+ """The running notes, as Markdown."""
116
+ return documents.read("https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz/edit")
117
+ ```
118
+
119
+ | Method | Capability | `gws` command |
120
+ | --- | --- | --- |
121
+ | `read(ref)` | `documents:read` | `drive files get` (metadata), then `docs documents get` with `includeTabsContent` or `sheets spreadsheets get` with `includeGridData` |
122
+ | `events(window)` | `calendar:read` | `calendar events list` on `primary`, `singleEvents`, `showDeleted`, paged by `pageToken` |
123
+ | `messages(query, window)` | `mail:read` | `gmail users messages list` with Gmail's search, then `messages get` (metadata) for each |
124
+ | `read(message)` | `mail:read` | `gmail users messages get` (full) |
125
+ | `free_busy(people, window)` | `calendar:read` | `calendar freebusy query` for `primary` and each address |
126
+ | `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 |
127
+ | `update_event(event, change)` | `calendar:write` | `calendar events get`, then `patch` (only your events, or ones whose guests may change them) |
128
+ | `cancel_event(event, notify)` | `calendar:write` | `calendar events get`, then `delete` (only your events; decline others') |
129
+ | `respond(event, response)` | `calendar:write` | `calendar events get`, then `patch` of your own attendee entry |
130
+ | `thread(thread)` | `mail:read` | `gmail users threads get` (full), without drafts, Spam and Trash |
131
+ | `draft(draft)` | `mail:read` | `gmail users drafts get` (full) |
132
+ | `create_draft(draft)` | `mail:write` | `gmail users drafts create` with a plain-text MIME message; a reply with `threadId`, `In-Reply-To` and `References` |
133
+ | `send_draft(draft, seen)` | `mail:write` | `gmail users drafts get`, then `send` if its message is still the one you saw |
134
+ | `relabel(thread, add, remove)` | `mail:write` | `gmail users threads modify` with label ids; not Spam, Trash, Drafts or Sent |
135
+
136
+ - **Documents** are a Docs, Sheets or Drive link (`https://docs.google.com/document/d/<id>/...`,
137
+ `.../spreadsheets/d/<id>/...`, `https://drive.google.com/file/d/<id>/...`, `.../open?id=<id>`) or a bare file id;
138
+ links to any other host are refused before `gws` runs, and a Drive shortcut is followed once. A Doc comes back as
139
+ Markdown-flavoured text, every tab under its title: headings, lists, bold and italic, links, tables; person chips by
140
+ name, never their email, and mail links by their text; titles and link targets can't start a line of their own;
141
+ images and drawings are left out. A Sheet comes back as one `## <sheet>` table per sheet
142
+ with the values as people see them. `revision` is the Doc's revision id or the Sheet's Drive version. Uploaded Word
143
+ or Excel files are refused: open them as a Google Doc or Sheet first.
144
+ - **Events** are your primary calendar's, every occurrence of a recurring event, cancelled ones included, by start.
145
+ Attendees are display names and emails, without meeting rooms; the workflow packet builder drops the emails before
146
+ anything reaches an agent's prompt. `conference_id` is the Zoom meeting number when the event has a Zoom link (in
147
+ its conferencing data, location or description), else the Meet code, so the `meetings` capability can find the
148
+ event's meeting; conference links are kept only on the conferencing services' own hosts (Meet, Zoom, Teams, Webex),
149
+ without their query, where passcodes live. A display name that is an address is dropped.
150
+ - **Mail** is your Gmail: `messages` takes the shared query syntax (`from:<address or domain> subject:<text>
151
+ label:<name>`) and a window. The window and the sender go into Gmail's search and the label as its id; the subject
152
+ is matched by the plugin, because Gmail's `subject:` matches whole words only. Results come oldest first, at most
153
+ 200 messages a window (one `gws` run each). `read` gives the text part, or the HTML part as text, in the part's own
154
+ charset, without attachments, and only the headers that carry no addresses. Senders and recipients are names and
155
+ emails, parsed before any encoded name is decoded, so a display name can't pose as an address; the workflow packet
156
+ builder drops the addresses before anything reaches an agent's prompt. A message longer than 2 million characters,
157
+ or with more than 500 MIME parts or 2 million characters of HTML, is refused, not cut; Spam and Trash are not
158
+ read; reading one window's messages may take at most five minutes.
159
+ - **Writes** change only what the person confirmed, through publish operations. Ids are checked before they reach
160
+ `gws`; a write that times out or fails without Google refusing it is not retried and says to look in Google first;
161
+ a rate-limited one is retried. A recurring series' own id is refused: one occurrence changes at a time. Drafts are built with
162
+ Python's `email` package from plain addresses and one-line subjects, so nothing can add a header.
163
+ - **Text is data.** Every string returned passes `heinzel.clean_text` (no control, invisible or bidirectional
164
+ characters).
165
+ - **Bounds.** One `gws` run prints at most 10 MB, one call reads at most 100 pages or 25 MB, a Doc's text at most four
166
+ times its JSON, tables and tabs nest at most 8 deep, a Sheet at most a million cells; past that, an error says to
167
+ narrow the window or that the file is too large. Rate limits are honoured: a rate-limited call is retried after 1, 2
168
+ and 4 seconds, then fails saying when to retry.
169
+
170
+ ```bash
171
+ hz google info # this plugin, its version, the gws it uses, the scopes and APIs
172
+ hz google files --days 3 # Docs and Sheets you can open that changed in the last three days
173
+ hz system capability list # who reads what, through which adapter and hosts
174
+ ```
175
+
176
+ Errors say what to do: no `gws` names the install; not signed in or an expired sign-in names the exact
177
+ `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.
178
+ Token-shaped strings in anything `gws` says are redacted before it is shortened.
179
+
180
+ Development and release commands: [repository guide](../../README.md).
181
+
182
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,169 @@
1
+ # heinzel-google
2
+
3
+ Google Docs, Sheets, Drive, Calendar and Gmail: the `documents` (read), `calendar` and `mail` (read and write)
4
+ capabilities ([ADR 0028](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0028-integrations-are-capability-adapters.md)): a first-party
5
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin. It is independently released and installed explicitly:
6
+
7
+ ```bash
8
+ hz system plugin add google --confirm # a compatible plugin release; run it again to upgrade
9
+ hz system plugin remove heinzel-google
10
+ ```
11
+
12
+ It is the **default provider** for Google Workspace, not the only possible one: workflows ask for `documents`,
13
+ `calendar` and `mail`, and any other plugin that provides those capabilities can be bound instead, in configuration.
14
+
15
+ ## How it reaches Google
16
+
17
+ Through the open-source [Google Workspace CLI](https://github.com/googleworkspace/cli), `gws` (0.22 or newer): it
18
+ signs in with Google, keeps its own credentials (encrypted, with the key in the OS keychain, by default) and calls
19
+ Google's APIs. This plugin never sees a token. It runs `gws <service> <resource> <method> --params <JSON> --format
20
+ json` as an argument list (never through a shell), with the executable resolved to an absolute path, from the root
21
+ directory (so no project's `.env` reaches `gws`), with an allowlisted environment (never Heinzelmännchen's own
22
+ credentials or other secrets; of gws's own variables only `GOOGLE_WORKSPACE_CLI_CONFIG_DIR`, `_KEYRING_BACKEND`,
23
+ `_CLIENT_ID`, `_CLIENT_SECRET` and `GOOGLE_WORKSPACE_PROJECT_ID`, not its token, credentials-file, sanitizing or log
24
+ variables), a
25
+ timeout that stops `gws` and anything it started, and at most 10 MB of output read. Why `gws`:
26
+ [ADR 0042](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0042-google-workspace-through-the-gws-cli.md).
27
+
28
+ What leaves this machine, and where to: file ids, the Drive query (the modified-time window and the `--name` text)
29
+ the calendar window and the mail search, to Google's API hosts (`www.googleapis.com` for Drive and Calendar,
30
+ `docs.googleapis.com`, `sheets.googleapis.com`, `gmail.googleapis.com`); `gws` also fetches API descriptions from Google and refreshes its sign-in at
31
+ `oauth2.googleapis.com`. Nothing goes anywhere else: gws 0.22 documents no telemetry or update check, and none was
32
+ found in it.
33
+
34
+ ## Set up
35
+
36
+ Install `gws` (it needs Node.js), give it an OAuth client once (`gws auth setup`, which needs `gcloud`, or a Desktop
37
+ OAuth client as its README describes), then let setup do the rest:
38
+
39
+ ```bash
40
+ npm install -g @googleworkspace/cli
41
+ hz setup google # finds gws, signs it in, binds documents, calendar and (if you say so) mail
42
+ hz system doctor # finds gws, checks its version and its stored sign-in, from files; Google isn't contacted
43
+ ```
44
+
45
+ By hand, setup is:
46
+
47
+ ```bash
48
+ 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
49
+ hz system config set capabilities.documents google
50
+ hz system config set capabilities.calendar google
51
+ ```
52
+
53
+ Mail is opt-in: setup asks, defaulting to no, and then signs in again with Gmail's read-only scope added
54
+ (`,https://www.googleapis.com/auth/gmail.readonly`), which lets the plugin read all of your mail; then
55
+ `hz system config set capabilities.mail google`.
56
+
57
+ Writes are opt-in too. Setup asks whether hz may change your calendar and your mail, defaulting to no; only then does
58
+ the sign-in ask for `https://www.googleapis.com/auth/calendar.events` (create, move, cancel and answer events) and
59
+ `https://www.googleapis.com/auth/gmail.modify` (keep drafts, send them, file conversations; it reads too, so it
60
+ replaces `gmail.readonly`, and it can't delete mail for good). Heinzelmännchen writes only from publish operations
61
+ that show you what they will do and run after you confirm; a chat's agent only proposes them
62
+ ([ADR 0059](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0059-the-calendar-and-the-mail-write-through-hz-after-a-confirmation.md)). Without the
63
+ scope, a write fails with the `gws auth login` that adds it.
64
+
65
+ These are the narrowest read-only scopes for what the adapters do; `drive.metadata.readonly` lists and identifies files
66
+ and never reads their contents (Google classes it as restricted, which matters only for published OAuth clients).
67
+ Bindings go in your user configuration, never in a project `hz.toml`, which Heinzelmännchen ignores for them.
68
+
69
+ Optional settings, in `[google]` or `HZ_GOOGLE__<FIELD>`: `gws_path`, the absolute path of `gws` when it isn't on
70
+ `PATH` (only absolute `PATH` entries are searched; both are ignored in a project `hz.toml`: a repository someone else
71
+ controls must not choose the program that reads your data), and `timeout`, the seconds each `gws` run may take
72
+ (default 60, also ignored in a project `hz.toml`).
73
+
74
+ ### Is it connected?
75
+
76
+ Bound automatically to documents and calendar while it is the only installed adapter of them, [ADR
77
+ 0055](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0055-connectors-show-their-sign-in-test-live-on-demand-and-bind-themselves-when-alone.md); mail
78
+ never is, so Gmail stays opt-in: `hz setup google` asks. `hz system connector
79
+ list` shows whether gws has a sign-in, from its files alone; `hz system connector
80
+ test google` asks Drive who is signed in (`gws drive about get`, 15 s at most), and only when you run it.
81
+
82
+ ## Use
83
+
84
+ Workflows ask for `documents` and `calendar`, never for Google:
85
+
86
+ ```python
87
+ from pydantic import AwareDatetime
88
+
89
+ from heinzel import Calendar, Document, Documents, Event, Module, Window
90
+
91
+ module = Module("prep", "One-on-one preparation.", requires=["documents:read", "calendar:read"])
92
+
93
+
94
+ @module.operation
95
+ def week(calendar: Calendar, since: AwareDatetime, until: AwareDatetime) -> list[Event]:
96
+ """This week's events."""
97
+ return calendar.events(Window(since=since, until=until))
98
+
99
+
100
+ @module.operation
101
+ def notes(documents: Documents) -> Document:
102
+ """The running notes, as Markdown."""
103
+ return documents.read("https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz/edit")
104
+ ```
105
+
106
+ | Method | Capability | `gws` command |
107
+ | --- | --- | --- |
108
+ | `read(ref)` | `documents:read` | `drive files get` (metadata), then `docs documents get` with `includeTabsContent` or `sheets spreadsheets get` with `includeGridData` |
109
+ | `events(window)` | `calendar:read` | `calendar events list` on `primary`, `singleEvents`, `showDeleted`, paged by `pageToken` |
110
+ | `messages(query, window)` | `mail:read` | `gmail users messages list` with Gmail's search, then `messages get` (metadata) for each |
111
+ | `read(message)` | `mail:read` | `gmail users messages get` (full) |
112
+ | `free_busy(people, window)` | `calendar:read` | `calendar freebusy query` for `primary` and each address |
113
+ | `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 |
114
+ | `update_event(event, change)` | `calendar:write` | `calendar events get`, then `patch` (only your events, or ones whose guests may change them) |
115
+ | `cancel_event(event, notify)` | `calendar:write` | `calendar events get`, then `delete` (only your events; decline others') |
116
+ | `respond(event, response)` | `calendar:write` | `calendar events get`, then `patch` of your own attendee entry |
117
+ | `thread(thread)` | `mail:read` | `gmail users threads get` (full), without drafts, Spam and Trash |
118
+ | `draft(draft)` | `mail:read` | `gmail users drafts get` (full) |
119
+ | `create_draft(draft)` | `mail:write` | `gmail users drafts create` with a plain-text MIME message; a reply with `threadId`, `In-Reply-To` and `References` |
120
+ | `send_draft(draft, seen)` | `mail:write` | `gmail users drafts get`, then `send` if its message is still the one you saw |
121
+ | `relabel(thread, add, remove)` | `mail:write` | `gmail users threads modify` with label ids; not Spam, Trash, Drafts or Sent |
122
+
123
+ - **Documents** are a Docs, Sheets or Drive link (`https://docs.google.com/document/d/<id>/...`,
124
+ `.../spreadsheets/d/<id>/...`, `https://drive.google.com/file/d/<id>/...`, `.../open?id=<id>`) or a bare file id;
125
+ links to any other host are refused before `gws` runs, and a Drive shortcut is followed once. A Doc comes back as
126
+ Markdown-flavoured text, every tab under its title: headings, lists, bold and italic, links, tables; person chips by
127
+ name, never their email, and mail links by their text; titles and link targets can't start a line of their own;
128
+ images and drawings are left out. A Sheet comes back as one `## <sheet>` table per sheet
129
+ with the values as people see them. `revision` is the Doc's revision id or the Sheet's Drive version. Uploaded Word
130
+ or Excel files are refused: open them as a Google Doc or Sheet first.
131
+ - **Events** are your primary calendar's, every occurrence of a recurring event, cancelled ones included, by start.
132
+ Attendees are display names and emails, without meeting rooms; the workflow packet builder drops the emails before
133
+ anything reaches an agent's prompt. `conference_id` is the Zoom meeting number when the event has a Zoom link (in
134
+ its conferencing data, location or description), else the Meet code, so the `meetings` capability can find the
135
+ event's meeting; conference links are kept only on the conferencing services' own hosts (Meet, Zoom, Teams, Webex),
136
+ without their query, where passcodes live. A display name that is an address is dropped.
137
+ - **Mail** is your Gmail: `messages` takes the shared query syntax (`from:<address or domain> subject:<text>
138
+ label:<name>`) and a window. The window and the sender go into Gmail's search and the label as its id; the subject
139
+ is matched by the plugin, because Gmail's `subject:` matches whole words only. Results come oldest first, at most
140
+ 200 messages a window (one `gws` run each). `read` gives the text part, or the HTML part as text, in the part's own
141
+ charset, without attachments, and only the headers that carry no addresses. Senders and recipients are names and
142
+ emails, parsed before any encoded name is decoded, so a display name can't pose as an address; the workflow packet
143
+ builder drops the addresses before anything reaches an agent's prompt. A message longer than 2 million characters,
144
+ or with more than 500 MIME parts or 2 million characters of HTML, is refused, not cut; Spam and Trash are not
145
+ read; reading one window's messages may take at most five minutes.
146
+ - **Writes** change only what the person confirmed, through publish operations. Ids are checked before they reach
147
+ `gws`; a write that times out or fails without Google refusing it is not retried and says to look in Google first;
148
+ a rate-limited one is retried. A recurring series' own id is refused: one occurrence changes at a time. Drafts are built with
149
+ Python's `email` package from plain addresses and one-line subjects, so nothing can add a header.
150
+ - **Text is data.** Every string returned passes `heinzel.clean_text` (no control, invisible or bidirectional
151
+ characters).
152
+ - **Bounds.** One `gws` run prints at most 10 MB, one call reads at most 100 pages or 25 MB, a Doc's text at most four
153
+ times its JSON, tables and tabs nest at most 8 deep, a Sheet at most a million cells; past that, an error says to
154
+ narrow the window or that the file is too large. Rate limits are honoured: a rate-limited call is retried after 1, 2
155
+ and 4 seconds, then fails saying when to retry.
156
+
157
+ ```bash
158
+ hz google info # this plugin, its version, the gws it uses, the scopes and APIs
159
+ hz google files --days 3 # Docs and Sheets you can open that changed in the last three days
160
+ hz system capability list # who reads what, through which adapter and hosts
161
+ ```
162
+
163
+ Errors say what to do: no `gws` names the install; not signed in or an expired sign-in names the exact
164
+ `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.
165
+ Token-shaped strings in anything `gws` says are redacted before it is shortened.
166
+
167
+ Development and release commands: [repository guide](../../README.md).
168
+
169
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,41 @@
1
+ {
2
+ "format": 1,
3
+ "sdk": 1,
4
+ "inputs": [
5
+ "coverage>=7.12",
6
+ "editables>=0.5",
7
+ "hatchling>=1.29.0",
8
+ "heinzel>=0.9.1,<0.10",
9
+ "hypothesis>=6.150,<7",
10
+ "mypy>=1.19.0,<3",
11
+ "pyright[nodejs]==1.1.414",
12
+ "pytest-timeout>=2.4.0,<3",
13
+ "pytest-xdist>=3.8",
14
+ "pytest>=9.0.3,<10",
15
+ "ruff==0.17.2"
16
+ ],
17
+ "core": {
18
+ "version": "0.9.1",
19
+ "dependencies": [
20
+ "croniter>=6.2.4",
21
+ "fastapi>=0.161",
22
+ "keyring>=25.6",
23
+ "mcp>=2.3",
24
+ "packaging>=25.0",
25
+ "platformdirs>=4.5",
26
+ "pluggy<2,>=1.6.0",
27
+ "psutil==7.2.2",
28
+ "pydantic-ai-slim==2.61.0",
29
+ "pydantic-ai-slim[anthropic]==2.61.0; extra == \"anthropic\"",
30
+ "pydantic-ai-slim[openai]==2.61.0; extra == \"openai\"",
31
+ "pydantic-settings<3,>=2.11.0",
32
+ "pydantic<3,>=2.12.0",
33
+ "pyyaml<7,>=6.0.2",
34
+ "rich>=14.2.0",
35
+ "tomlkit>=0.13",
36
+ "typer>=0.23.0",
37
+ "uvicorn>=0.54",
38
+ "wsproto>=1.3"
39
+ ]
40
+ }
41
+ }