commune-mail 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.
- commune_mail-0.1.0/.gitignore +6 -0
- commune_mail-0.1.0/PKG-INFO +577 -0
- commune_mail-0.1.0/README.md +552 -0
- commune_mail-0.1.0/commune/__init__.py +49 -0
- commune_mail-0.1.0/commune/_http.py +102 -0
- commune_mail-0.1.0/commune/client.py +595 -0
- commune_mail-0.1.0/commune/exceptions.py +49 -0
- commune_mail-0.1.0/commune/py.typed +0 -0
- commune_mail-0.1.0/commune/types.py +203 -0
- commune_mail-0.1.0/pyproject.toml +37 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commune-mail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Commune – email infrastructure for agents. Threads, inboxes, domains, attachments, and sending.
|
|
5
|
+
Project-URL: Homepage, https://github.com/commune-ai/commune
|
|
6
|
+
Project-URL: Documentation, https://docs.commune.sh
|
|
7
|
+
Project-URL: Repository, https://github.com/commune-ai/commune
|
|
8
|
+
Author-email: Commune <hello@commune.sh>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,ai,api,email,inbox,sdk,threads
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Email
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Commune Python SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for [Commune](https://commune.sh) — email infrastructure for AI agents.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install commune-ai
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
From zero to a working email agent in 4 lines — no domain setup, no DNS:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from commune import CommuneClient
|
|
42
|
+
|
|
43
|
+
client = CommuneClient(api_key="comm_...")
|
|
44
|
+
|
|
45
|
+
# Create an inbox — domain is auto-assigned
|
|
46
|
+
inbox = client.inboxes.create(local_part="support")
|
|
47
|
+
print(f"Inbox ready: {inbox.address}") # → "support@agents.postking.io"
|
|
48
|
+
|
|
49
|
+
# List email threads
|
|
50
|
+
threads = client.threads.list(inbox_id=inbox.id, limit=5)
|
|
51
|
+
for t in threads.data:
|
|
52
|
+
print(f" [{t.message_count} msgs] {t.subject}")
|
|
53
|
+
|
|
54
|
+
# Send an email
|
|
55
|
+
client.messages.send(
|
|
56
|
+
to="user@example.com",
|
|
57
|
+
subject="Hello from my agent",
|
|
58
|
+
text="Hi there!",
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it. No domain verification, no DNS records. Just create an inbox and start sending/receiving.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Concepts
|
|
67
|
+
|
|
68
|
+
Commune organizes email around four layers:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Domain → Inbox → Thread → Message
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Domain** — A custom email domain you own (e.g. `example.com`). You verify it by adding DNS records.
|
|
75
|
+
- **Inbox** — A mailbox under a domain (e.g. `support@example.com`). Each inbox can have webhooks for real-time notifications.
|
|
76
|
+
- **Thread** — A conversation: a group of related messages sharing a subject/reply chain. Called `conversation_id` internally, exposed as `thread_id` in the SDK.
|
|
77
|
+
- **Message** — A single email (inbound or outbound) within a thread.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Client
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from commune import CommuneClient
|
|
85
|
+
|
|
86
|
+
client = CommuneClient(
|
|
87
|
+
api_key="comm_...", # Required. Your API key.
|
|
88
|
+
base_url=None, # Optional. Override API URL.
|
|
89
|
+
timeout=30.0, # Optional. Request timeout in seconds.
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Supports context manager:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
with CommuneClient(api_key="comm_...") as client:
|
|
97
|
+
domains = client.domains.list()
|
|
98
|
+
# Connection closed automatically
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Domains
|
|
104
|
+
|
|
105
|
+
Domains are the foundation. You register a domain, add DNS records, verify it, then create inboxes under it.
|
|
106
|
+
|
|
107
|
+
### `client.domains.list()`
|
|
108
|
+
|
|
109
|
+
List all domains in your organization.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
domains = client.domains.list()
|
|
113
|
+
# → [Domain(id="d_abc123", name="example.com", status="verified", ...)]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Returns:** `list[Domain]`
|
|
117
|
+
|
|
118
|
+
| Field | Type | Description |
|
|
119
|
+
|-------|------|-------------|
|
|
120
|
+
| `id` | `str` | Domain ID |
|
|
121
|
+
| `name` | `str` | Domain name |
|
|
122
|
+
| `status` | `str` | `"not_started"`, `"pending"`, `"verified"`, `"failed"` |
|
|
123
|
+
| `region` | `str` | AWS region |
|
|
124
|
+
| `records` | `list` | DNS records (MX, TXT, CNAME) |
|
|
125
|
+
| `inboxes` | `list[Inbox]` | Inboxes under this domain |
|
|
126
|
+
|
|
127
|
+
### `client.domains.create(name, region=None)`
|
|
128
|
+
|
|
129
|
+
Register a new domain. After creating, you'll need to verify it.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
domain = client.domains.create(name="example.com")
|
|
133
|
+
print(domain.id) # → "d_abc123"
|
|
134
|
+
print(domain.status) # → "not_started"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Parameter | Type | Required | Description |
|
|
138
|
+
|-----------|------|----------|-------------|
|
|
139
|
+
| `name` | `str` | Yes | Domain name (e.g. `"example.com"`) |
|
|
140
|
+
| `region` | `str` | No | AWS region (e.g. `"us-east-1"`) |
|
|
141
|
+
|
|
142
|
+
### `client.domains.get(domain_id)`
|
|
143
|
+
|
|
144
|
+
Get full details for a single domain.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
domain = client.domains.get("d_abc123")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `client.domains.records(domain_id)`
|
|
151
|
+
|
|
152
|
+
Get the DNS records you need to add at your registrar.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
records = client.domains.records("d_abc123")
|
|
156
|
+
for r in records:
|
|
157
|
+
print(f" {r['type']} {r['name']} → {r['value']}")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Returns:** `list[dict]` — each record has `type`, `name`, `value`, `status`, `ttl`.
|
|
161
|
+
|
|
162
|
+
### `client.domains.verify(domain_id)`
|
|
163
|
+
|
|
164
|
+
Trigger verification after you've added the DNS records.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
result = client.domains.verify("d_abc123")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Typical flow
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# 1. Create the domain
|
|
174
|
+
domain = client.domains.create(name="example.com")
|
|
175
|
+
|
|
176
|
+
# 2. Get DNS records to configure
|
|
177
|
+
records = client.domains.records(domain.id)
|
|
178
|
+
print("Add these DNS records at your registrar:")
|
|
179
|
+
for r in records:
|
|
180
|
+
print(f" {r['type']} {r['name']} → {r['value']}")
|
|
181
|
+
|
|
182
|
+
# 3. After adding records, verify
|
|
183
|
+
result = client.domains.verify(domain.id)
|
|
184
|
+
|
|
185
|
+
# 4. Check status
|
|
186
|
+
domain = client.domains.get(domain.id)
|
|
187
|
+
print(f"Status: {domain.status}") # → "verified"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Inboxes
|
|
193
|
+
|
|
194
|
+
Inboxes are mailboxes that receive and send email. Create one with just a `local_part` — the domain is auto-assigned.
|
|
195
|
+
|
|
196
|
+
### `client.inboxes.create(local_part, *, domain_id=None, name=None, webhook=None)`
|
|
197
|
+
|
|
198
|
+
Create a new inbox. Domain is **auto-resolved** if not provided — no DNS setup needed.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# Simplest — domain auto-assigned
|
|
202
|
+
inbox = client.inboxes.create(local_part="support")
|
|
203
|
+
print(inbox.address) # → "support@agents.postking.io"
|
|
204
|
+
|
|
205
|
+
# Explicit domain (if you have a custom domain)
|
|
206
|
+
inbox = client.inboxes.create(local_part="billing", domain_id="d_abc123")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Parameter | Type | Required | Description |
|
|
210
|
+
|-----------|------|----------|-------------|
|
|
211
|
+
| `local_part` | `str` | Yes | Part before `@` (e.g. `"support"`, `"billing"`) |
|
|
212
|
+
| `domain_id` | `str` | No | Domain to create under. Auto-resolved if omitted. |
|
|
213
|
+
| `name` | `str` | No | Display name |
|
|
214
|
+
| `webhook` | `dict` | No | `{"endpoint": "https://...", "events": ["inbound"]}` |
|
|
215
|
+
|
|
216
|
+
**Returns:** `Inbox`
|
|
217
|
+
|
|
218
|
+
| Field | Type | Description |
|
|
219
|
+
|-------|------|-------------|
|
|
220
|
+
| `id` | `str` | Inbox ID |
|
|
221
|
+
| `local_part` | `str` | Part before `@` |
|
|
222
|
+
| `address` | `str` | Full email address |
|
|
223
|
+
| `webhook` | `InboxWebhook \| str \| None` | Webhook configuration |
|
|
224
|
+
| `status` | `str \| None` | Inbox status |
|
|
225
|
+
| `created_at` | `str \| None` | ISO timestamp |
|
|
226
|
+
|
|
227
|
+
### `client.inboxes.list(domain_id=None)`
|
|
228
|
+
|
|
229
|
+
List inboxes. Without `domain_id`, lists all inboxes across all domains.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# All inboxes
|
|
233
|
+
inboxes = client.inboxes.list()
|
|
234
|
+
|
|
235
|
+
# Inboxes for a specific domain
|
|
236
|
+
inboxes = client.inboxes.list(domain_id="d_abc123")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `client.inboxes.get(domain_id, inbox_id)`
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
inbox = client.inboxes.get("d_abc123", "i_xyz")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `client.inboxes.update(domain_id, inbox_id, **fields)`
|
|
246
|
+
|
|
247
|
+
Update one or more fields. Only provided fields are changed.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
inbox = client.inboxes.update("d_abc123", "i_xyz", local_part="help")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `client.inboxes.set_webhook(domain_id, inbox_id, *, endpoint, events=None)`
|
|
254
|
+
|
|
255
|
+
Shortcut to set a webhook. You'll receive a POST when emails arrive.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
client.inboxes.set_webhook(
|
|
259
|
+
"d_abc123", "i_xyz",
|
|
260
|
+
endpoint="https://your-app.com/webhook",
|
|
261
|
+
events=["inbound"],
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `client.inboxes.remove(domain_id, inbox_id)`
|
|
266
|
+
|
|
267
|
+
Delete an inbox permanently.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
client.inboxes.remove("d_abc123", "i_xyz") # → True
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Threads
|
|
276
|
+
|
|
277
|
+
A thread is a conversation — a group of related email messages. Threads are listed with **cursor-based pagination** for efficient browsing of large mailboxes.
|
|
278
|
+
|
|
279
|
+
### `client.threads.list(*, inbox_id=None, domain_id=None, limit=20, cursor=None, order="desc")`
|
|
280
|
+
|
|
281
|
+
List threads for an inbox or domain. Returns newest first by default.
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
result = client.threads.list(inbox_id="i_xyz", limit=10)
|
|
285
|
+
|
|
286
|
+
for thread in result.data:
|
|
287
|
+
print(f"[{thread.message_count} msgs] {thread.subject}")
|
|
288
|
+
print(f" Last activity: {thread.last_message_at}")
|
|
289
|
+
print(f" Preview: {thread.snippet}")
|
|
290
|
+
|
|
291
|
+
# Paginate
|
|
292
|
+
if result.has_more:
|
|
293
|
+
page2 = client.threads.list(inbox_id="i_xyz", cursor=result.next_cursor)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Parameter | Type | Required | Description |
|
|
297
|
+
|-----------|------|----------|-------------|
|
|
298
|
+
| `inbox_id` | `str` | One of these | Filter by inbox |
|
|
299
|
+
| `domain_id` | `str` | required | Filter by domain |
|
|
300
|
+
| `limit` | `int` | No | 1–100, default 20 |
|
|
301
|
+
| `cursor` | `str` | No | Cursor from previous `next_cursor` |
|
|
302
|
+
| `order` | `str` | No | `"desc"` (newest first) or `"asc"` |
|
|
303
|
+
|
|
304
|
+
**Returns:** `ThreadList`
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
ThreadList(
|
|
308
|
+
data=[Thread(...)], # List of thread summaries
|
|
309
|
+
next_cursor="abc...", # Pass to next call for next page (None if no more)
|
|
310
|
+
has_more=True, # Whether more pages exist
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Thread object:**
|
|
315
|
+
|
|
316
|
+
| Field | Type | Description |
|
|
317
|
+
|-------|------|-------------|
|
|
318
|
+
| `thread_id` | `str` | Thread identifier |
|
|
319
|
+
| `subject` | `str \| None` | Email subject |
|
|
320
|
+
| `last_message_at` | `str` | ISO timestamp of last message |
|
|
321
|
+
| `first_message_at` | `str \| None` | ISO timestamp of first message |
|
|
322
|
+
| `message_count` | `int` | Total messages in thread |
|
|
323
|
+
| `snippet` | `str \| None` | Preview of last message (up to 200 chars) |
|
|
324
|
+
| `last_direction` | `str \| None` | `"inbound"` or `"outbound"` |
|
|
325
|
+
| `inbox_id` | `str \| None` | Inbox this thread belongs to |
|
|
326
|
+
| `domain_id` | `str \| None` | Domain this thread belongs to |
|
|
327
|
+
| `has_attachments` | `bool` | Whether any message has attachments |
|
|
328
|
+
|
|
329
|
+
### `client.threads.messages(thread_id, *, limit=50, order="asc")`
|
|
330
|
+
|
|
331
|
+
Get all messages in a thread. Returns oldest first by default (chronological reading order).
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
messages = client.threads.messages("conv_abc123")
|
|
335
|
+
|
|
336
|
+
for msg in messages:
|
|
337
|
+
sender = next((p.identity for p in msg.participants if p.role == "sender"), "unknown")
|
|
338
|
+
print(f" [{msg.direction}] From: {sender}")
|
|
339
|
+
print(f" Subject: {msg.metadata.subject}")
|
|
340
|
+
print(f" {msg.content[:200]}")
|
|
341
|
+
print()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| Parameter | Type | Required | Description |
|
|
345
|
+
|-----------|------|----------|-------------|
|
|
346
|
+
| `thread_id` | `str` | Yes | Thread ID |
|
|
347
|
+
| `limit` | `int` | No | 1–1000, default 50 |
|
|
348
|
+
| `order` | `str` | No | `"asc"` (chronological) or `"desc"` |
|
|
349
|
+
|
|
350
|
+
**Returns:** `list[Message]`
|
|
351
|
+
|
|
352
|
+
**Message object:**
|
|
353
|
+
|
|
354
|
+
| Field | Type | Description |
|
|
355
|
+
|-------|------|-------------|
|
|
356
|
+
| `message_id` | `str` | Unique message identifier |
|
|
357
|
+
| `conversation_id` | `str` | Thread ID this message belongs to |
|
|
358
|
+
| `direction` | `str` | `"inbound"` or `"outbound"` |
|
|
359
|
+
| `participants` | `list[Participant]` | `[{role: "sender", identity: "user@..."}, ...]` |
|
|
360
|
+
| `content` | `str` | Plain text body |
|
|
361
|
+
| `content_html` | `str \| None` | HTML body |
|
|
362
|
+
| `attachments` | `list[str]` | Attachment IDs |
|
|
363
|
+
| `created_at` | `str` | ISO timestamp |
|
|
364
|
+
| `metadata.subject` | `str` | Subject line |
|
|
365
|
+
| `metadata.inbox_id` | `str` | Inbox ID |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Messages
|
|
370
|
+
|
|
371
|
+
### `client.messages.send(**kwargs)`
|
|
372
|
+
|
|
373
|
+
Send an email. Returns the sent message data.
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
result = client.messages.send(
|
|
377
|
+
to="user@example.com",
|
|
378
|
+
subject="Order Confirmation",
|
|
379
|
+
html="<h1>Thanks for your order!</h1><p>Your order #1234 is confirmed.</p>",
|
|
380
|
+
)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Parameter | Type | Required | Description |
|
|
384
|
+
|-----------|------|----------|-------------|
|
|
385
|
+
| `to` | `str \| list[str]` | Yes | Recipient(s) |
|
|
386
|
+
| `subject` | `str` | Yes | Subject line |
|
|
387
|
+
| `html` | `str` | No* | HTML body |
|
|
388
|
+
| `text` | `str` | No* | Plain text body |
|
|
389
|
+
| `from_address` | `str` | No | Sender (uses domain default) |
|
|
390
|
+
| `cc` | `list[str]` | No | CC recipients |
|
|
391
|
+
| `bcc` | `list[str]` | No | BCC recipients |
|
|
392
|
+
| `reply_to` | `str` | No | Reply-to address |
|
|
393
|
+
| `thread_id` | `str` | No | Reply in existing thread |
|
|
394
|
+
| `domain_id` | `str` | No | Send from specific domain |
|
|
395
|
+
| `inbox_id` | `str` | No | Send from specific inbox |
|
|
396
|
+
| `attachments` | `list[str]` | No | Attachment IDs |
|
|
397
|
+
| `headers` | `dict[str, str]` | No | Custom headers |
|
|
398
|
+
|
|
399
|
+
*At least one of `html` or `text` is required.
|
|
400
|
+
|
|
401
|
+
**Reply to a thread:**
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
client.messages.send(
|
|
405
|
+
to="customer@gmail.com",
|
|
406
|
+
subject="Re: Order Issue",
|
|
407
|
+
html="<p>We're looking into this for you.</p>",
|
|
408
|
+
thread_id="conv_abc123", # continues the thread
|
|
409
|
+
inbox_id="i_xyz",
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### `client.messages.list(**kwargs)`
|
|
414
|
+
|
|
415
|
+
List messages with filters. Provide at least one of `inbox_id`, `domain_id`, or `sender`.
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
messages = client.messages.list(
|
|
419
|
+
inbox_id="i_xyz",
|
|
420
|
+
limit=20,
|
|
421
|
+
order="desc",
|
|
422
|
+
after="2025-01-01T00:00:00Z",
|
|
423
|
+
)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
| Parameter | Type | Required | Description |
|
|
427
|
+
|-----------|------|----------|-------------|
|
|
428
|
+
| `inbox_id` | `str` | One of | Filter by inbox |
|
|
429
|
+
| `domain_id` | `str` | these | Filter by domain |
|
|
430
|
+
| `sender` | `str` | required | Filter by sender email |
|
|
431
|
+
| `limit` | `int` | No | 1–1000, default 50 |
|
|
432
|
+
| `order` | `str` | No | `"asc"` or `"desc"` (default) |
|
|
433
|
+
| `before` | `str` | No | ISO date — messages before this time |
|
|
434
|
+
| `after` | `str` | No | ISO date — messages after this time |
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Attachments
|
|
439
|
+
|
|
440
|
+
Upload files, then reference them when sending emails.
|
|
441
|
+
|
|
442
|
+
### `client.attachments.upload(content, filename, mime_type)`
|
|
443
|
+
|
|
444
|
+
Upload a file. Returns an `attachment_id` you pass to `messages.send()`.
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
import base64
|
|
448
|
+
|
|
449
|
+
with open("invoice.pdf", "rb") as f:
|
|
450
|
+
content = base64.b64encode(f.read()).decode()
|
|
451
|
+
|
|
452
|
+
upload = client.attachments.upload(
|
|
453
|
+
content=content,
|
|
454
|
+
filename="invoice.pdf",
|
|
455
|
+
mime_type="application/pdf",
|
|
456
|
+
)
|
|
457
|
+
print(upload.attachment_id) # → "att_abc123"
|
|
458
|
+
print(upload.size) # → 45230
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
| Parameter | Type | Required | Description |
|
|
462
|
+
|-----------|------|----------|-------------|
|
|
463
|
+
| `content` | `str` | Yes | Base64-encoded file data |
|
|
464
|
+
| `filename` | `str` | Yes | Original filename |
|
|
465
|
+
| `mime_type` | `str` | Yes | MIME type |
|
|
466
|
+
|
|
467
|
+
**Returns:** `AttachmentUpload`
|
|
468
|
+
|
|
469
|
+
| Field | Type | Description |
|
|
470
|
+
|-------|------|-------------|
|
|
471
|
+
| `attachment_id` | `str` | ID to use in `messages.send()` |
|
|
472
|
+
| `filename` | `str` | Filename |
|
|
473
|
+
| `mime_type` | `str` | MIME type |
|
|
474
|
+
| `size` | `int` | Size in bytes |
|
|
475
|
+
|
|
476
|
+
### `client.attachments.get(attachment_id)`
|
|
477
|
+
|
|
478
|
+
Get metadata for an uploaded attachment.
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
att = client.attachments.get("att_abc123")
|
|
482
|
+
print(att.filename, att.mime_type, att.size)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### `client.attachments.url(attachment_id, *, expires_in=3600)`
|
|
486
|
+
|
|
487
|
+
Get a temporary download URL.
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
url_info = client.attachments.url("att_abc123", expires_in=7200)
|
|
491
|
+
print(url_info.url) # → "https://..."
|
|
492
|
+
print(url_info.expires_in) # → 7200
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Returns:** `AttachmentUrl`
|
|
496
|
+
|
|
497
|
+
| Field | Type | Description |
|
|
498
|
+
|-------|------|-------------|
|
|
499
|
+
| `url` | `str` | Temporary download URL |
|
|
500
|
+
| `expires_in` | `int` | Seconds until URL expires |
|
|
501
|
+
| `filename` | `str` | Filename |
|
|
502
|
+
| `mime_type` | `str` | MIME type |
|
|
503
|
+
| `size` | `int` | Size in bytes |
|
|
504
|
+
|
|
505
|
+
### Full attachment flow
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
import base64
|
|
509
|
+
|
|
510
|
+
# 1. Upload the file
|
|
511
|
+
with open("report.pdf", "rb") as f:
|
|
512
|
+
content = base64.b64encode(f.read()).decode()
|
|
513
|
+
|
|
514
|
+
upload = client.attachments.upload(content, "report.pdf", "application/pdf")
|
|
515
|
+
|
|
516
|
+
# 2. Send email with attachment
|
|
517
|
+
client.messages.send(
|
|
518
|
+
to="user@example.com",
|
|
519
|
+
subject="Monthly Report",
|
|
520
|
+
html="<p>Please find the report attached.</p>",
|
|
521
|
+
attachments=[upload.attachment_id],
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# 3. Later, get a download URL for that attachment
|
|
525
|
+
url_info = client.attachments.url(upload.attachment_id)
|
|
526
|
+
print(f"Download: {url_info.url}")
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Error Handling
|
|
532
|
+
|
|
533
|
+
All errors inherit from `CommuneError`. Catch specific types or the base class.
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from commune import (
|
|
537
|
+
CommuneClient,
|
|
538
|
+
CommuneError,
|
|
539
|
+
AuthenticationError,
|
|
540
|
+
NotFoundError,
|
|
541
|
+
ValidationError,
|
|
542
|
+
RateLimitError,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
client = CommuneClient(api_key="comm_...")
|
|
547
|
+
domain = client.domains.get("nonexistent")
|
|
548
|
+
except AuthenticationError:
|
|
549
|
+
# 401 — invalid or expired API key
|
|
550
|
+
print("Check your API key")
|
|
551
|
+
except NotFoundError:
|
|
552
|
+
# 404 — resource doesn't exist
|
|
553
|
+
print("Domain not found")
|
|
554
|
+
except ValidationError as e:
|
|
555
|
+
# 400 — bad request parameters
|
|
556
|
+
print(f"Invalid request: {e.message}")
|
|
557
|
+
except RateLimitError:
|
|
558
|
+
# 429 — too many requests
|
|
559
|
+
print("Slow down, try again in a moment")
|
|
560
|
+
except CommuneError as e:
|
|
561
|
+
# Catch-all for any API error
|
|
562
|
+
print(f"Error ({e.status_code}): {e.message}")
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
| Exception | HTTP Status | When |
|
|
566
|
+
|-----------|-------------|------|
|
|
567
|
+
| `AuthenticationError` | 401 | Invalid/expired API key |
|
|
568
|
+
| `ValidationError` | 400 | Bad request parameters |
|
|
569
|
+
| `NotFoundError` | 404 | Resource doesn't exist |
|
|
570
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
571
|
+
| `CommuneError` | Any | Base class for all errors |
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## License
|
|
576
|
+
|
|
577
|
+
MIT
|