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.
- cherami-0.1.0/.gitignore +9 -0
- cherami-0.1.0/CONTRIBUTING.md +24 -0
- cherami-0.1.0/LICENSE +21 -0
- cherami-0.1.0/PKG-INFO +189 -0
- cherami-0.1.0/README.md +171 -0
- cherami-0.1.0/examples/README.md +49 -0
- cherami-0.1.0/examples/prepare_reply.py +19 -0
- cherami-0.1.0/examples/read_inbox.py +18 -0
- cherami-0.1.0/examples/read_inbox_async.py +19 -0
- cherami-0.1.0/examples/submit_reply.py +23 -0
- cherami-0.1.0/openapi.json +10335 -0
- cherami-0.1.0/operations.json +38 -0
- cherami-0.1.0/pyproject.toml +34 -0
- cherami-0.1.0/scripts/generate.mts +88 -0
- cherami-0.1.0/src/cherami/__init__.py +16 -0
- cherami-0.1.0/src/cherami/_client.py +187 -0
- cherami-0.1.0/src/cherami/_operations.py +383 -0
- cherami-0.1.0/src/cherami/_routes.py +43 -0
- cherami-0.1.0/src/cherami/_transport.py +158 -0
- cherami-0.1.0/src/cherami/attachments.py +25 -0
- cherami-0.1.0/src/cherami/models.py +1109 -0
- cherami-0.1.0/src/cherami/py.typed +0 -0
- cherami-0.1.0/src/cherami/recovery.py +108 -0
- cherami-0.1.0/uv.lock +633 -0
cherami-0.1.0/.gitignore
ADDED
|
@@ -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.
|
cherami-0.1.0/README.md
ADDED
|
@@ -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.
|