cherami 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ .mypy_cache/
3
+ __pycache__/
4
+ *.py[cod]
5
+ dist/
6
+ *.egg-info/
7
+ .generation.json
8
+ .env
9
+ .env.*
@@ -0,0 +1,24 @@
1
+ # Contributing
2
+
3
+ For a bug report, include a minimal reproduction, Python and SDK versions, the operation, and a request ID when available. Omit API keys, mail bodies and private attachments. Send security issues or private account questions to hello@cherami.to rather than a public issue.
4
+
5
+ Use Python 3.11+, uv and Bun. Applications consuming the package need only Python and its runtime dependencies.
6
+
7
+ ```sh
8
+ uv sync --locked
9
+ bun scripts/generate.mts
10
+ ```
11
+
12
+ The selected `openapi.json` and `operations.json` are the generation inputs. Discuss contract changes or new operations with a maintainer before implementing them; behavior must agree with the service's [HTTP reference](https://cherami.to/docs/api). Do not edit `models.py`, `_operations.py` or `_routes.py` directly. The generator produces static `TypedDict` models and method/pagination signatures; it does not add runtime response coercion. A narrow receipt-refinement normalization works around the model generator's conflicting TypedDict inheritance while leaving the public snapshot unchanged.
13
+
14
+ Keep transport, serialization, errors, pagination and send-recovery policy shared between sync and async clients. Only HTTPX I/O and iterator syntax differ. Preserve omitted versus null values, original file bytes, request IDs and accepted/rejected/unknown outcomes. No automatic retries or redirects. A broken response or cancellation cannot establish that a write was rolled back.
15
+
16
+ Use focused manual checks with a substituted HTTPX transport for fault paths. Real requests require an appropriately authorized account and workflow; never send mail or mutate an inbox merely to check an example. Do not commit credentials, mail, recovery records or private operation material. The runnable examples explain private file handling and explicit sending.
17
+
18
+ ## Distribution
19
+
20
+ `uv build` prepares a wheel and source archive in `dist/`. Inspect their contents and consume the wheel in an isolated environment before release. The wheel contains `cherami` source, `py.typed`, license and package metadata, not generation tools or workspace dependencies. The source archive includes everything needed for standalone generation, including `uv.lock`.
21
+
22
+ Maintainers publish releases manually. Verify that the source archive regenerates independently and that source and package metadata agree. Publish the reviewed artifact rather than rebuilding an unchecked tree; update installation guidance alongside package availability. No registry credential is required for local development.
23
+
24
+ In a pull request, explain the problem, proposed change and what you checked. Update examples or documentation when usage changes, and distinguish local fixture results from live service behavior.
cherami-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cherami
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.
cherami-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: cherami
3
+ Version: 0.1.0
4
+ Summary: Official Cherami HTTP client: inboxes, correspondence, drafts, attachments and conversations.
5
+ Project-URL: Homepage, https://cherami.to
6
+ Project-URL: Documentation, https://cherami.to/docs/guides/python
7
+ Project-URL: Repository, https://github.com/cherami-mail/cherami-python
8
+ Project-URL: Issues, https://github.com/cherami-mail/cherami-python/issues
9
+ Author: Cherami
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: httpx<0.29,>=0.28.1
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Cherami Python SDK
20
+
21
+ The official Python client for [Cherami](https://cherami.to), email infrastructure for AI agents. Create inboxes for ongoing work, read incoming correspondence, and send messages from your application.
22
+
23
+ Python 3.11+, with synchronous and asynchronous clients and typed dictionaries.
24
+
25
+ ## Install and connect
26
+
27
+ Install from PyPI:
28
+
29
+ ```sh
30
+ pip install cherami
31
+ ```
32
+
33
+ Or, with uv:
34
+
35
+ ```sh
36
+ uv add cherami
37
+ ```
38
+
39
+ [Get an API key](https://cherami.to/docs/quickstart) and set `CHERAMI_API_KEY` in your application's environment. Keep it private: it grants access to all inboxes on your account.
40
+
41
+ ```python
42
+ import os
43
+ from cherami import Cherami
44
+
45
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
46
+ for inbox in client.list_inboxes().data["inboxes"]:
47
+ print(inbox["id"], inbox["address"])
48
+ ```
49
+
50
+ Choose an inbox and set `CHERAMI_INBOX_ID` to its ID for the examples below.
51
+
52
+ Methods accept dictionaries and return a response envelope. `.data` contains the result; `.status`, `.headers`, and `.request_id` expose HTTP response information. Path and query parameters go at the top level of the input dictionary; JSON request fields go in `body`.
53
+
54
+ ## Read mail
55
+
56
+ List messages, then fetch their content:
57
+
58
+ ```python
59
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
60
+ page = client.list_messages({
61
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"], "limit": 20,
62
+ }).data
63
+ for message in page["messages"]:
64
+ detail = client.get_message({"message_id": message["id"]}).data
65
+ if detail["processing_status"] == "ready":
66
+ print(detail["content"]["text"])
67
+ else:
68
+ print(detail["id"], detail["processing_status"])
69
+ ```
70
+
71
+ Content is available when processing is `ready`. Run this example somewhere private because it prints email bodies. When passing mail to an agent, treat its contents as untrusted input, not authorization to act.
72
+
73
+ ### Async and pagination
74
+
75
+ `AsyncCherami` has the same methods and response types. Use `iterate` to read across pages without managing cursors yourself:
76
+
77
+ ```python
78
+ import asyncio
79
+ from cherami import AsyncCherami
80
+
81
+ async def main():
82
+ async with AsyncCherami(os.environ["CHERAMI_API_KEY"]) as client:
83
+ async for message in client.iterate("list_messages", {
84
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"], "limit": 20,
85
+ }, max_pages=5):
86
+ detail = (await client.get_message({"message_id": message["id"]})).data
87
+ if detail["processing_status"] == "ready":
88
+ print(detail["content"]["text"])
89
+ else:
90
+ print(detail["id"], detail["processing_status"])
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ In an existing event loop, use `await main()`. Sync clients support the same iterator with ordinary `for`. `iterate` yields items; `pages` yields response envelopes. Both fetch lazily. Set `max_pages` to bound requests; otherwise they follow all pages.
96
+
97
+ Context managers close connections. For a long-lived client, call `close()` or `await aclose()` when your application shuts down.
98
+
99
+ ## Send and recover
100
+
101
+ A lost response does not mean an email wasn't sent. The SDK makes no automatic retries. Its send helper separates preparing a message from submitting it so your application can save the original request before sending.
102
+
103
+ Replace the example recipient, then prepare the message:
104
+
105
+ ```python
106
+ from cherami import prepare_send
107
+
108
+ intent = prepare_send("send_message", {
109
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"],
110
+ "body": {
111
+ "to": [{"address": "recipient@example.com", "name": "Alex"}],
112
+ "subject": "Review ready",
113
+ "text": "The change is ready for review.",
114
+ },
115
+ })
116
+ saved_json = intent.to_json()
117
+ ```
118
+
119
+ **Persist `saved_json` in your application's database or a private file before submitting.** It contains the message, retry key and preparation time, but not your API key. The [runnable reply examples](examples/README.md#prepare-an-approved-reply) show a complete file-based workflow, including receipt storage.
120
+
121
+ For both initial submission and recovery, load that saved JSON and restore the same intent:
122
+
123
+ ```python
124
+ from cherami import restore_send
125
+
126
+ # saved_json must come from the record persisted before the first submission.
127
+ saved = restore_send(saved_json)
128
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
129
+ result = client.submit(saved)
130
+ receipt = {
131
+ "data": result.data,
132
+ "status": result.status,
133
+ "request_id": result.request_id,
134
+ }
135
+ # Persist this receipt without replacing earlier receipts for the intent.
136
+ ```
137
+
138
+ Read `result.data["message"]["status"]` to distinguish the outcome:
139
+
140
+ | Outcome | Meaning |
141
+ | --- | --- |
142
+ | `accepted` | The provider accepted submission. This is not proof of delivery. |
143
+ | `rejected` | The provider explicitly rejected submission. |
144
+ | `unknown` | Submission may have succeeded. Recover using the saved intent, not a new send. |
145
+
146
+ These outcomes are data, not exceptions. Preserve every returned receipt: when `outcome_persisted` is false, it may contain an outcome that later reads do not yet show. A failed local save does not undo sending.
147
+
148
+ The helper refuses submission after **23 hours and 59 minutes from preparation**. Restoring an intent does not extend that window. After expiry, inspect sent resources rather than preparing a replacement for an uncertain send. Recovery retrieves the outcome; it does not resume provider submission.
149
+
150
+ The helper also supports `reply_message`, `reply_all_message`, and `forward_message`. Direct send methods leave retry-key and recovery-window management to your application. Drafts use `send_draft` with separate same-draft protection. See the [sending guide](https://cherami.to/docs/guides/sending) and [draft guide](https://cherami.to/docs/guides/drafts) for those workflows.
151
+
152
+ ## Handle errors
153
+
154
+ ```python
155
+ from cherami import CheramiApiError, CheramiTransportError
156
+
157
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
158
+ try:
159
+ inboxes = client.list_inboxes().data
160
+ except CheramiApiError as error:
161
+ print(error.status, error.code, error.request_id)
162
+ except CheramiTransportError:
163
+ # No usable API response was received.
164
+ raise
165
+ ```
166
+
167
+ API errors also expose `.body`, `.headers`, and `.retry_after`. Transport failures raise `CheramiTransportError`; async cancellation propagates normally. For writes, neither a transport failure nor cancellation proves the operation was rolled back. Use the saved-intent workflow above for uncertain sends.
168
+
169
+ Clients use HTTPX with a default 60-second inactivity timeout per network operation. Pass `timeout` to a client or method to change it, or `None` to disable it. Requests do not automatically retry or follow redirects.
170
+
171
+ ## More workflows
172
+
173
+ The SDK supports inboxes, received and sent mail, replies, forwarding, drafts, labels, conversations, policy inspection and attachment downloads.
174
+
175
+ - [Runnable examples](examples/README.md): read mail, prepare a reply, and submit or recover it.
176
+ - [Python guide](https://cherami.to/docs/guides/python): client configuration, attachments and download handling.
177
+ - [HTTP reference](https://cherami.to/docs/api): input fields and response contracts. Named Python types are available from `cherami.models`, including `SendInput`, `SendReceipt`, and operation types such as `ListMessagesParams`.
178
+
179
+ Inputs and results are ordinary dictionaries with HTTP field names unchanged. Omit optional fields unless you intend to supply them; use `None` only where the API permits null. Types support static checking, not runtime validation.
180
+
181
+ ## Development
182
+
183
+ ```sh
184
+ uv sync --locked
185
+ bun scripts/generate.mts
186
+ uv build
187
+ ```
188
+
189
+ Consumers need neither Bun nor the model generator. See [CONTRIBUTING.md](CONTRIBUTING.md) for generation and distribution details. MIT licensed.
@@ -0,0 +1,171 @@
1
+ # Cherami Python SDK
2
+
3
+ The official Python client for [Cherami](https://cherami.to), email infrastructure for AI agents. Create inboxes for ongoing work, read incoming correspondence, and send messages from your application.
4
+
5
+ Python 3.11+, with synchronous and asynchronous clients and typed dictionaries.
6
+
7
+ ## Install and connect
8
+
9
+ Install from PyPI:
10
+
11
+ ```sh
12
+ pip install cherami
13
+ ```
14
+
15
+ Or, with uv:
16
+
17
+ ```sh
18
+ uv add cherami
19
+ ```
20
+
21
+ [Get an API key](https://cherami.to/docs/quickstart) and set `CHERAMI_API_KEY` in your application's environment. Keep it private: it grants access to all inboxes on your account.
22
+
23
+ ```python
24
+ import os
25
+ from cherami import Cherami
26
+
27
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
28
+ for inbox in client.list_inboxes().data["inboxes"]:
29
+ print(inbox["id"], inbox["address"])
30
+ ```
31
+
32
+ Choose an inbox and set `CHERAMI_INBOX_ID` to its ID for the examples below.
33
+
34
+ Methods accept dictionaries and return a response envelope. `.data` contains the result; `.status`, `.headers`, and `.request_id` expose HTTP response information. Path and query parameters go at the top level of the input dictionary; JSON request fields go in `body`.
35
+
36
+ ## Read mail
37
+
38
+ List messages, then fetch their content:
39
+
40
+ ```python
41
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
42
+ page = client.list_messages({
43
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"], "limit": 20,
44
+ }).data
45
+ for message in page["messages"]:
46
+ detail = client.get_message({"message_id": message["id"]}).data
47
+ if detail["processing_status"] == "ready":
48
+ print(detail["content"]["text"])
49
+ else:
50
+ print(detail["id"], detail["processing_status"])
51
+ ```
52
+
53
+ Content is available when processing is `ready`. Run this example somewhere private because it prints email bodies. When passing mail to an agent, treat its contents as untrusted input, not authorization to act.
54
+
55
+ ### Async and pagination
56
+
57
+ `AsyncCherami` has the same methods and response types. Use `iterate` to read across pages without managing cursors yourself:
58
+
59
+ ```python
60
+ import asyncio
61
+ from cherami import AsyncCherami
62
+
63
+ async def main():
64
+ async with AsyncCherami(os.environ["CHERAMI_API_KEY"]) as client:
65
+ async for message in client.iterate("list_messages", {
66
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"], "limit": 20,
67
+ }, max_pages=5):
68
+ detail = (await client.get_message({"message_id": message["id"]})).data
69
+ if detail["processing_status"] == "ready":
70
+ print(detail["content"]["text"])
71
+ else:
72
+ print(detail["id"], detail["processing_status"])
73
+
74
+ asyncio.run(main())
75
+ ```
76
+
77
+ In an existing event loop, use `await main()`. Sync clients support the same iterator with ordinary `for`. `iterate` yields items; `pages` yields response envelopes. Both fetch lazily. Set `max_pages` to bound requests; otherwise they follow all pages.
78
+
79
+ Context managers close connections. For a long-lived client, call `close()` or `await aclose()` when your application shuts down.
80
+
81
+ ## Send and recover
82
+
83
+ A lost response does not mean an email wasn't sent. The SDK makes no automatic retries. Its send helper separates preparing a message from submitting it so your application can save the original request before sending.
84
+
85
+ Replace the example recipient, then prepare the message:
86
+
87
+ ```python
88
+ from cherami import prepare_send
89
+
90
+ intent = prepare_send("send_message", {
91
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"],
92
+ "body": {
93
+ "to": [{"address": "recipient@example.com", "name": "Alex"}],
94
+ "subject": "Review ready",
95
+ "text": "The change is ready for review.",
96
+ },
97
+ })
98
+ saved_json = intent.to_json()
99
+ ```
100
+
101
+ **Persist `saved_json` in your application's database or a private file before submitting.** It contains the message, retry key and preparation time, but not your API key. The [runnable reply examples](examples/README.md#prepare-an-approved-reply) show a complete file-based workflow, including receipt storage.
102
+
103
+ For both initial submission and recovery, load that saved JSON and restore the same intent:
104
+
105
+ ```python
106
+ from cherami import restore_send
107
+
108
+ # saved_json must come from the record persisted before the first submission.
109
+ saved = restore_send(saved_json)
110
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
111
+ result = client.submit(saved)
112
+ receipt = {
113
+ "data": result.data,
114
+ "status": result.status,
115
+ "request_id": result.request_id,
116
+ }
117
+ # Persist this receipt without replacing earlier receipts for the intent.
118
+ ```
119
+
120
+ Read `result.data["message"]["status"]` to distinguish the outcome:
121
+
122
+ | Outcome | Meaning |
123
+ | --- | --- |
124
+ | `accepted` | The provider accepted submission. This is not proof of delivery. |
125
+ | `rejected` | The provider explicitly rejected submission. |
126
+ | `unknown` | Submission may have succeeded. Recover using the saved intent, not a new send. |
127
+
128
+ These outcomes are data, not exceptions. Preserve every returned receipt: when `outcome_persisted` is false, it may contain an outcome that later reads do not yet show. A failed local save does not undo sending.
129
+
130
+ The helper refuses submission after **23 hours and 59 minutes from preparation**. Restoring an intent does not extend that window. After expiry, inspect sent resources rather than preparing a replacement for an uncertain send. Recovery retrieves the outcome; it does not resume provider submission.
131
+
132
+ The helper also supports `reply_message`, `reply_all_message`, and `forward_message`. Direct send methods leave retry-key and recovery-window management to your application. Drafts use `send_draft` with separate same-draft protection. See the [sending guide](https://cherami.to/docs/guides/sending) and [draft guide](https://cherami.to/docs/guides/drafts) for those workflows.
133
+
134
+ ## Handle errors
135
+
136
+ ```python
137
+ from cherami import CheramiApiError, CheramiTransportError
138
+
139
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
140
+ try:
141
+ inboxes = client.list_inboxes().data
142
+ except CheramiApiError as error:
143
+ print(error.status, error.code, error.request_id)
144
+ except CheramiTransportError:
145
+ # No usable API response was received.
146
+ raise
147
+ ```
148
+
149
+ API errors also expose `.body`, `.headers`, and `.retry_after`. Transport failures raise `CheramiTransportError`; async cancellation propagates normally. For writes, neither a transport failure nor cancellation proves the operation was rolled back. Use the saved-intent workflow above for uncertain sends.
150
+
151
+ Clients use HTTPX with a default 60-second inactivity timeout per network operation. Pass `timeout` to a client or method to change it, or `None` to disable it. Requests do not automatically retry or follow redirects.
152
+
153
+ ## More workflows
154
+
155
+ The SDK supports inboxes, received and sent mail, replies, forwarding, drafts, labels, conversations, policy inspection and attachment downloads.
156
+
157
+ - [Runnable examples](examples/README.md): read mail, prepare a reply, and submit or recover it.
158
+ - [Python guide](https://cherami.to/docs/guides/python): client configuration, attachments and download handling.
159
+ - [HTTP reference](https://cherami.to/docs/api): input fields and response contracts. Named Python types are available from `cherami.models`, including `SendInput`, `SendReceipt`, and operation types such as `ListMessagesParams`.
160
+
161
+ Inputs and results are ordinary dictionaries with HTTP field names unchanged. Omit optional fields unless you intend to supply them; use `None` only where the API permits null. Types support static checking, not runtime validation.
162
+
163
+ ## Development
164
+
165
+ ```sh
166
+ uv sync --locked
167
+ bun scripts/generate.mts
168
+ uv build
169
+ ```
170
+
171
+ Consumers need neither Bun nor the model generator. See [CONTRIBUTING.md](CONTRIBUTING.md) for generation and distribution details. MIT licensed.
@@ -0,0 +1,49 @@
1
+ # Run the examples
2
+
3
+ Use Python 3.11+ and an existing [human-approved API key](https://cherami.to/docs/quickstart). No MCP connection is required. The scripts operate only when you run them; none starts a background poller.
4
+
5
+ From this SDK checkout, install it and its development environment with `uv sync --locked`. Run the commands below from this directory's parent with `uv run python examples/…`. Applications can install the published package with `pip install cherami` or `uv add cherami`.
6
+
7
+ ## Read correspondence
8
+
9
+ Supply `CHERAMI_API_KEY` privately through your environment. Do not put its value in commands, source, chat, or logs.
10
+
11
+ ```sh
12
+ uv run python examples/read_inbox.py
13
+ ```
14
+
15
+ Without `CHERAMI_INBOX_ID`, this lists available inbox IDs and addresses, then stops. Choose the inbox assigned to your application, set that ID, and run again. The example reads one page of 20 messages and prints prepared message text. Run in a private terminal: the text may contain confidential or hostile material. It does not label messages or treat reading as approval to act.
16
+
17
+ For async applications, the equivalent is:
18
+
19
+ ```sh
20
+ uv run python examples/read_inbox_async.py
21
+ ```
22
+
23
+ That version requires the inbox ID and prints original plaintext. Both leave unfinished message preparation visible rather than pretending the message is empty.
24
+
25
+ ## Prepare an approved reply
26
+
27
+ Inspect the chosen message's From and Reply-To, and confirm the recipient and reply content before continuing. `reply_message` derives recipients from the source; use an explicitly addressed `send_message` intent instead when you need to override them.
28
+
29
+ Set `CHERAMI_INBOX_ID`, `CHERAMI_MESSAGE_ID` (the Cherami message ID, not an RFC Message-ID), and `CHERAMI_REPLY_TEXT`. Set `CHERAMI_INTENT_PATH` to a new absolute filename in an existing private directory outside your repository.
30
+
31
+ ```sh
32
+ uv run python examples/prepare_reply.py
33
+ ```
34
+
35
+ This saves the payload, original key and preparation time with private permissions and exclusive creation. It sends nothing and requires no API key. Use a separate record for each intended reply. Keep the saved file unchanged; do not rerun this command to recover an uncertain send.
36
+
37
+ ## Submit or recover that reply
38
+
39
+ Set `CHERAMI_RECEIPT_DIR` to an existing absolute private directory and retain `CHERAMI_INTENT_PATH` and `CHERAMI_API_KEY`.
40
+
41
+ ```sh
42
+ uv run python examples/submit_reply.py
43
+ ```
44
+
45
+ Initial submission and recovery use this same command and original record. Each run makes exactly one request, saving `{data, status, request_id}` in a new private receipt file before printing the message ID, outcome and persistence flag. An empty/partial receipt file means recording failed, not that sending failed. Preserve every complete receipt.
46
+
47
+ `accepted` is provider acceptance, not delivery; `rejected` is explicit provider rejection; `unknown` leaves submission uncertain. If `outcome_persisted` is false, preserve the immediate result even if later reads lag. The helper refuses submission after 23 hours and 59 minutes from preparation. Do not replace the key or reprepare an uncertain send after expiry; inspect sent resources. Recovery never resumes provider submission.
48
+
49
+ See the [sending guide](https://cherami.to/docs/guides/sending) for the complete contract.
@@ -0,0 +1,19 @@
1
+ """Save an approved reply once. No network request or credential is needed."""
2
+ import os
3
+ from pathlib import Path
4
+ from cherami import prepare_send
5
+
6
+ path = Path(os.environ["CHERAMI_INTENT_PATH"])
7
+ if not path.is_absolute():
8
+ raise ValueError("CHERAMI_INTENT_PATH must be an absolute private filename.")
9
+ intent = prepare_send("reply_message", {
10
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"],
11
+ "body": {"message_id": os.environ["CHERAMI_MESSAGE_ID"], "text": os.environ["CHERAMI_REPLY_TEXT"]},
12
+ })
13
+ # Exclusive creation avoids replacing a prior intent. A partial write is not a
14
+ # usable record: do not submit until this command has completed successfully.
15
+ with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w", encoding="utf-8") as file:
16
+ file.write(intent.to_json())
17
+ file.flush()
18
+ os.fsync(file.fileno())
19
+ print("Saved reply. Use submit_reply.py for initial submission or recovery.")
@@ -0,0 +1,18 @@
1
+ """Read recent mail. Prints message text: run privately, not in shared logs."""
2
+ import os
3
+ from cherami import Cherami
4
+
5
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
6
+ inbox_id = os.environ.get("CHERAMI_INBOX_ID")
7
+ if not inbox_id:
8
+ for inbox in client.list_inboxes().data["inboxes"]:
9
+ print(inbox["id"], inbox["address"])
10
+ raise SystemExit("Choose an assigned inbox and set CHERAMI_INBOX_ID.")
11
+ for message in client.iterate("list_messages", {"inbox_id": inbox_id, "limit": 20}, max_pages=1):
12
+ detail = client.get_message({"message_id": message["id"]}).data
13
+ print(detail["id"], detail["processing_status"])
14
+ if detail["processing_status"] == "ready":
15
+ content = detail["content"]
16
+ # Empty extracted text is meaningful; only None falls back to original.
17
+ text = content["reply_text"] if content["reply_text"] is not None else content["text"]
18
+ print(text if text is not None else "No plain text; inspect original content privately.")
@@ -0,0 +1,19 @@
1
+ """Async reading uses the same models and lazy pagination, without blocking I/O."""
2
+ import asyncio
3
+ import os
4
+ from cherami import AsyncCherami
5
+
6
+
7
+ async def main() -> None:
8
+ async with AsyncCherami(os.environ["CHERAMI_API_KEY"]) as client:
9
+ async for message in client.iterate("list_messages", {
10
+ "inbox_id": os.environ["CHERAMI_INBOX_ID"], "limit": 20,
11
+ }, max_pages=1):
12
+ detail = (await client.get_message({"message_id": message["id"]})).data
13
+ print(detail["id"], detail["processing_status"])
14
+ if detail["processing_status"] == "ready":
15
+ print(detail["content"]["text"])
16
+
17
+
18
+ if __name__ == "__main__":
19
+ asyncio.run(main())
@@ -0,0 +1,23 @@
1
+ """Submit/recover the unchanged saved intent. Makes one request, never retries."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from uuid import uuid4
6
+ from cherami import Cherami, restore_send
7
+
8
+ intent_path = Path(os.environ["CHERAMI_INTENT_PATH"])
9
+ receipt_dir = Path(os.environ["CHERAMI_RECEIPT_DIR"])
10
+ if not intent_path.is_absolute() or not receipt_dir.is_absolute() or not receipt_dir.is_dir():
11
+ raise ValueError("Use an absolute intent filename and existing private receipt directory.")
12
+ intent = restore_send(intent_path.read_text(encoding="utf-8"))
13
+ with Cherami(os.environ["CHERAMI_API_KEY"]) as client:
14
+ # Open the destination before any send. Every response gets its own receipt;
15
+ # a later unknown replay must not overwrite earlier provider acceptance.
16
+ path = receipt_dir / f"receipt-{uuid4()}.json"
17
+ with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w", encoding="utf-8") as file:
18
+ result = client.submit(intent)
19
+ json.dump({"data": result.data, "status": result.status, "request_id": result.request_id}, file)
20
+ file.flush()
21
+ os.fsync(file.fileno())
22
+ print(result.data["message"]["id"], result.data["message"]["status"], result.data["outcome_persisted"])
23
+ # File-write failure does not undo a send. Empty/partial files are not receipts.