Skip to content

Commit 94f055b

Browse files
author
Shanjai
committed
Initial release — Commune Python SDK v0.2.0
- Sync + async clients (CommuneClient, AsyncCommuneClient) - Typed Pydantic v2 models with forward-compatible extras - Webhook signature verification (HMAC-SHA256) - COMMUNE_API_KEY env var support - Exception hierarchy (PermissionDeniedError, AuthenticationError, etc.) - 35 unit tests - MIT license
0 parents  commit 94f055b

23 files changed

Lines changed: 3549 additions & 0 deletions

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
__pycache__/
2+
*.pyc
3+
*.pyo
4+
*.egg-info/
5+
dist/
6+
build/
7+
.venv/
8+
.env
9+
.pytest_cache/
10+
*.so
11+
.mypy_cache/
12+
.ruff_cache/

API_REFERENCE.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# Commune Python SDK API Reference
2+
3+
Package on PyPI: `commune-mail`
4+
Import path: `commune`
5+
6+
This reference describes typed request/response contracts for the public SDK.
7+
8+
## Contract Policy
9+
10+
- Stable core fields are strongly typed in `commune/types.py`.
11+
- Forward-compatible API expansion is allowed through extensible metadata (`extra` fields).
12+
- Backward compatibility target: non-breaking additions for response fields.
13+
14+
## Client
15+
16+
```python
17+
from commune import CommuneClient
18+
19+
client = CommuneClient(api_key="comm_...")
20+
21+
# Or use COMMUNE_API_KEY env var:
22+
client = CommuneClient()
23+
```
24+
25+
| Parameter | Type | Required | Default |
26+
|---|---|---|---|
27+
| `api_key` | `str \| None` | No | `COMMUNE_API_KEY` env var |
28+
| `base_url` | `str \| None` | No | `https://api.commune.sh` |
29+
| `timeout` | `float` | No | `30.0` |
30+
31+
## Async Client
32+
33+
```python
34+
from commune import AsyncCommuneClient
35+
36+
async with AsyncCommuneClient(api_key="comm_...") as client:
37+
...
38+
```
39+
40+
Same parameters as `CommuneClient`. All methods are `async`.
41+
42+
## Domains
43+
44+
### `client.domains.list()`
45+
Permission: `domains:read`
46+
47+
Request params: none
48+
Response type: `list[Domain]`
49+
50+
### `client.domains.create(name, region=None)`
51+
Permission: `domains:write`
52+
53+
| Parameter | Type | Required |
54+
|---|---|---|
55+
| `name` | `str` | Yes |
56+
| `region` | `str | None` | No |
57+
58+
Response type: `Domain`
59+
60+
### `client.domains.get(domain_id)`
61+
Permission: `domains:read`
62+
63+
| Parameter | Type | Required |
64+
|---|---|---|
65+
| `domain_id` | `str` | Yes |
66+
67+
Response type: `Domain`
68+
69+
### `client.domains.verify(domain_id)`
70+
Permission: `domains:write`
71+
72+
| Parameter | Type | Required |
73+
|---|---|---|
74+
| `domain_id` | `str` | Yes |
75+
76+
Response type: `DomainVerificationResult`
77+
78+
### `client.domains.records(domain_id)`
79+
Permission: `domains:read`
80+
81+
| Parameter | Type | Required |
82+
|---|---|---|
83+
| `domain_id` | `str` | Yes |
84+
85+
Response type: `list[DomainDnsRecord]`
86+
87+
## Inboxes
88+
89+
### `client.inboxes.list(domain_id=None)`
90+
Permission: `inboxes:read`
91+
92+
| Parameter | Type | Required |
93+
|---|---|---|
94+
| `domain_id` | `str | None` | No |
95+
96+
Response type: `list[Inbox]`
97+
98+
### `client.inboxes.create(local_part, *, domain_id=None, name=None, webhook=None)`
99+
Permission: `inboxes:write`
100+
101+
| Parameter | Type | Required |
102+
|---|---|---|
103+
| `local_part` | `str` | Yes |
104+
| `domain_id` | `str | None` | No |
105+
| `name` | `str | None` | No |
106+
| `webhook` | `dict[str, Any] | None` | No |
107+
108+
Response type: `Inbox`
109+
110+
### `client.inboxes.get(domain_id, inbox_id)`
111+
Permission: `inboxes:read`
112+
113+
| Parameter | Type | Required |
114+
|---|---|---|
115+
| `domain_id` | `str` | Yes |
116+
| `inbox_id` | `str` | Yes |
117+
118+
Response type: `Inbox`
119+
120+
### `client.inboxes.update(domain_id, inbox_id, *, local_part=None, webhook=None, status=None)`
121+
Permission: `inboxes:write`
122+
123+
| Parameter | Type | Required |
124+
|---|---|---|
125+
| `domain_id` | `str` | Yes |
126+
| `inbox_id` | `str` | Yes |
127+
| `local_part` | `str | None` | No |
128+
| `webhook` | `dict[str, Any] | None` | No |
129+
| `status` | `str | None` | No |
130+
131+
Response type: `Inbox`
132+
133+
### `client.inboxes.set_webhook(domain_id, inbox_id, *, endpoint, events=None)`
134+
Permission: `inboxes:write`
135+
136+
| Parameter | Type | Required |
137+
|---|---|---|
138+
| `domain_id` | `str` | Yes |
139+
| `inbox_id` | `str` | Yes |
140+
| `endpoint` | `str` | Yes |
141+
| `events` | `list[str] | None` | No |
142+
143+
Response type: `Inbox`
144+
145+
### `client.inboxes.remove(domain_id, inbox_id)`
146+
Permission: `inboxes:write`
147+
148+
| Parameter | Type | Required |
149+
|---|---|---|
150+
| `domain_id` | `str` | Yes |
151+
| `inbox_id` | `str` | Yes |
152+
153+
Response type: `bool`
154+
155+
## Threads
156+
157+
### `client.threads.list(*, inbox_id=None, domain_id=None, limit=20, cursor=None, order="desc")`
158+
Permission: `threads:read`
159+
160+
| Parameter | Type | Required |
161+
|---|---|---|
162+
| `inbox_id` | `str | None` | One of `inbox_id` or `domain_id` |
163+
| `domain_id` | `str | None` | One of `inbox_id` or `domain_id` |
164+
| `limit` | `int` | No |
165+
| `cursor` | `str | None` | No |
166+
| `order` | `str` | No |
167+
168+
Response type: `ThreadList` (`data`, `next_cursor`, `has_more`)
169+
170+
### `client.threads.messages(thread_id, *, limit=50, order="asc")`
171+
Permission: `threads:read`
172+
173+
| Parameter | Type | Required |
174+
|---|---|---|
175+
| `thread_id` | `str` | Yes |
176+
| `limit` | `int` | No |
177+
| `order` | `str` | No |
178+
179+
Response type: `list[Message]`
180+
181+
## Messages
182+
183+
### `client.messages.send(...)`
184+
Permission: `messages:write`
185+
186+
| Parameter | Type | Required |
187+
|---|---|---|
188+
| `to` | `str | list[str]` | Yes |
189+
| `subject` | `str` | Yes |
190+
| `html` | `str | None` | No |
191+
| `text` | `str | None` | No |
192+
| `from_address` | `str | None` | No |
193+
| `cc` | `list[str] | None` | No |
194+
| `bcc` | `list[str] | None` | No |
195+
| `reply_to` | `str | None` | No |
196+
| `thread_id` | `str | None` | No |
197+
| `domain_id` | `str | None` | No |
198+
| `inbox_id` | `str | None` | No |
199+
| `attachments` | `list[str] | None` | No |
200+
| `headers` | `dict[str, str] | None` | No |
201+
202+
Response type: `SendMessageResult`
203+
204+
### `client.messages.list(*, inbox_id=None, domain_id=None, sender=None, limit=50, order="desc", before=None, after=None)`
205+
Permission: `messages:read`
206+
207+
| Parameter | Type | Required |
208+
|---|---|---|
209+
| `inbox_id` | `str | None` | One of `inbox_id`, `domain_id`, `sender` |
210+
| `domain_id` | `str | None` | One of `inbox_id`, `domain_id`, `sender` |
211+
| `sender` | `str | None` | One of `inbox_id`, `domain_id`, `sender` |
212+
| `limit` | `int` | No |
213+
| `order` | `str` | No |
214+
| `before` | `str | None` | No |
215+
| `after` | `str | None` | No |
216+
217+
Response type: `list[Message]`
218+
219+
## Attachments
220+
221+
### `client.attachments.upload(content, filename, mime_type)`
222+
Permission: `attachments:write`
223+
224+
| Parameter | Type | Required |
225+
|---|---|---|
226+
| `content` | `str` (base64) | Yes |
227+
| `filename` | `str` | Yes |
228+
| `mime_type` | `str` | Yes |
229+
230+
Response type: `AttachmentUpload`
231+
232+
### `client.attachments.get(attachment_id)`
233+
Permission: `attachments:read`
234+
235+
| Parameter | Type | Required |
236+
|---|---|---|
237+
| `attachment_id` | `str` | Yes |
238+
239+
Response type: `Attachment`
240+
241+
### `client.attachments.url(attachment_id, *, expires_in=3600)`
242+
Permission: `attachments:read`
243+
244+
| Parameter | Type | Required |
245+
|---|---|---|
246+
| `attachment_id` | `str` | Yes |
247+
| `expires_in` | `int` | No |
248+
249+
Response type: `AttachmentUrl`
250+
251+
## Webhook Verification
252+
253+
```python
254+
from commune import verify_signature, WebhookVerificationError
255+
256+
verify_signature(payload, signature, secret, timestamp=None, tolerance_seconds=300)
257+
```
258+
259+
| Parameter | Type | Required | Default |
260+
|---|---|---|---|
261+
| `payload` | `bytes \| str` | Yes | - |
262+
| `signature` | `str` | Yes | - |
263+
| `secret` | `str` | Yes | - |
264+
| `timestamp` | `str \| None` | No | `None` |
265+
| `tolerance_seconds` | `int` | No | `300` |
266+
267+
Returns `True` on success. Raises `WebhookVerificationError` on failure.
268+
269+
## Exception Types
270+
271+
- `AuthenticationError` (401)
272+
- `PermissionDeniedError` (403) — recommended name
273+
- `PermissionError` (403) — backward-compatible alias (shadows builtin)
274+
- `ValidationError` (400)
275+
- `NotFoundError` (404)
276+
- `RateLimitError` (429)
277+
- `WebhookVerificationError` — webhook signature verification failure
278+
- `CommuneError` — base class / fallback for other HTTP errors

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Changelog
2+
3+
All notable changes to the Commune Python SDK will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.2.0] — 2026-02-12
9+
10+
### Added
11+
- **`AsyncCommuneClient`** — Full async/await support via `httpx.AsyncClient`. Identical API surface to the sync client.
12+
- **`verify_signature()`** — Webhook signature verification helper with HMAC-SHA256 and timestamp freshness checks.
13+
- **`WebhookVerificationError`** exception for webhook verification failures.
14+
- **`PermissionDeniedError`** — Renamed from `PermissionError` to avoid shadowing the Python builtin. The old name is kept as a backward-compatible alias.
15+
- **`COMMUNE_API_KEY` env var**`CommuneClient()` and `AsyncCommuneClient()` now auto-read the API key from the environment if not passed explicitly.
16+
- **`CONTRIBUTING.md`** — Contributor guide.
17+
- **`LICENSE`** file (MIT).
18+
- This **`CHANGELOG.md`**.
19+
20+
### Changed
21+
- `api_key` parameter is now optional (falls back to `COMMUNE_API_KEY` env var).
22+
- `CommuneError` now has a `__repr__` for better debugging.
23+
24+
### Fixed
25+
- `PermissionError` no longer shadows `builtins.PermissionError`.
26+
27+
## [0.1.1] — 2026-02-06
28+
29+
### Added
30+
- Inbox `display_name` field support.
31+
- `MessageMetadata` fields: `spam_score`, `spam_action`, `spam_flagged`, `delivery_status`, prompt injection fields.
32+
- Comprehensive security documentation in README.
33+
34+
### Changed
35+
- Package renamed from `commune-ai` to `commune-mail` on PyPI.
36+
- `Message.conversation_id` renamed to `Message.thread_id`.
37+
- Updated all examples from `conv_` prefixes to `thread_` prefixes.
38+
39+
## [0.1.0] — 2026-01-28
40+
41+
### Added
42+
- Initial release.
43+
- Sync client with domains, inboxes, threads, messages, and attachments.
44+
- Pydantic v2 typed models with `extra="allow"` for forward compatibility.
45+
- Exception hierarchy mapping HTTP status codes.
46+
- `py.typed` marker for PEP 561 type checking support.
47+
- 9 unit tests covering contracts, error mapping, and pagination.

0 commit comments

Comments
 (0)