keelson-sdk 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.
- keelson_sdk-0.1.0/LICENSE +21 -0
- keelson_sdk-0.1.0/PKG-INFO +385 -0
- keelson_sdk-0.1.0/README.md +366 -0
- keelson_sdk-0.1.0/keelson/__init__.py +10 -0
- keelson_sdk-0.1.0/keelson_email/__init__.py +39 -0
- keelson_sdk-0.1.0/keelson_email/client.py +943 -0
- keelson_sdk-0.1.0/keelson_files/__init__.py +31 -0
- keelson_sdk-0.1.0/keelson_files/client.py +723 -0
- keelson_sdk-0.1.0/keelson_identity/__init__.py +35 -0
- keelson_sdk-0.1.0/keelson_identity/client.py +551 -0
- keelson_sdk-0.1.0/keelson_identity/local.py +242 -0
- keelson_sdk-0.1.0/keelson_media/__init__.py +23 -0
- keelson_sdk-0.1.0/keelson_media/client.py +343 -0
- keelson_sdk-0.1.0/keelson_sdk.egg-info/PKG-INFO +385 -0
- keelson_sdk-0.1.0/keelson_sdk.egg-info/SOURCES.txt +17 -0
- keelson_sdk-0.1.0/keelson_sdk.egg-info/dependency_links.txt +1 -0
- keelson_sdk-0.1.0/keelson_sdk.egg-info/top_level.txt +5 -0
- keelson_sdk-0.1.0/pyproject.toml +45 -0
- keelson_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 STRICTUS LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keelson-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keelson Python SDK
|
|
5
|
+
Author: Keelson
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://keelson.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/keelsonhq/python-sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Keelson Python SDK
|
|
21
|
+
|
|
22
|
+
Python SDK for building apps on the Keelson platform. Provides four modules:
|
|
23
|
+
|
|
24
|
+
> **Note**: This repository is a read-only release mirror. Development happens in the private Keelson monorepo; issues are welcome here, but pull requests are not accepted — changes land through the next release.
|
|
25
|
+
|
|
26
|
+
| Module | Import | Description |
|
|
27
|
+
|--------|--------|-------------|
|
|
28
|
+
| `keelson_media` | `from keelson import media` | Media storage (upload, serve by ID) |
|
|
29
|
+
| `keelson_files` | `from keelson import files` | Data files (key-addressed, overwrite, private) |
|
|
30
|
+
| `keelson_identity` | `from keelson import identity` | User identity and directory |
|
|
31
|
+
| `keelson_email` | `from keelson import email` | Inbound/outbound email |
|
|
32
|
+
|
|
33
|
+
Cross-language parity across Node, Python, and Go is defined in
|
|
34
|
+
[`./PARITY.md`](./PARITY.md). APIs below are labelled as
|
|
35
|
+
**guaranteed** (same capability in all 3 languages) or
|
|
36
|
+
**Python-specific** (convenience helpers unique to this SDK).
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install keelson-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The PyPI distribution is named `keelson-sdk`; Python imports continue to use
|
|
45
|
+
`keelson` and the domain modules shown below.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Media SDK (`keelson_media`)
|
|
50
|
+
|
|
51
|
+
Upload immutable media (images, PDFs, generated assets) and serve it by ID. On
|
|
52
|
+
Keelson it uses the managed media storage (internal media API); for local
|
|
53
|
+
development it uses the local filesystem. The runtime-mode contract is
|
|
54
|
+
**fail-closed**: it never silently writes to ephemeral local storage when platform
|
|
55
|
+
Media configuration is missing or incomplete (see Modes below).
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from keelson import media
|
|
59
|
+
|
|
60
|
+
# Store a file — returns a generated ULID file ID
|
|
61
|
+
file_id = media.put(b"Hello, world!", content_type="text/plain")
|
|
62
|
+
|
|
63
|
+
# Store with filename (MIME type auto-detected)
|
|
64
|
+
with open("report.pdf", "rb") as f:
|
|
65
|
+
pdf_id = media.put(f.read(), filename="report.pdf")
|
|
66
|
+
|
|
67
|
+
# Retrieve
|
|
68
|
+
data = media.get(file_id)
|
|
69
|
+
|
|
70
|
+
# Check existence and metadata
|
|
71
|
+
if media.exists(file_id):
|
|
72
|
+
info = media.stat(file_id)
|
|
73
|
+
print(info.content_type, info.content_length)
|
|
74
|
+
|
|
75
|
+
# Public URL (for embedding in HTML)
|
|
76
|
+
src = media.url(file_id) # "/media/01HXYZ..."
|
|
77
|
+
|
|
78
|
+
# Delete
|
|
79
|
+
media.delete(file_id)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Cross-language guaranteed API
|
|
83
|
+
|
|
84
|
+
| Function | Signature | Description |
|
|
85
|
+
|----------|-----------|-------------|
|
|
86
|
+
| `put` | `(data, *, content_type=None, filename=None) -> str` | Store file, returns ULID `file_id` |
|
|
87
|
+
| `get` | `(file_id) -> bytes` | Download file content |
|
|
88
|
+
| `delete` | `(file_id) -> None` | Delete file |
|
|
89
|
+
| `exists` | `(file_id) -> bool` | Check if file exists |
|
|
90
|
+
| `stat` | `(file_id) -> MediaStat` | Get metadata (content_type, content_length, status) |
|
|
91
|
+
| `url` | `(file_id) -> str` | Generate public URL path |
|
|
92
|
+
|
|
93
|
+
### Python-specific helpers
|
|
94
|
+
|
|
95
|
+
| Function | Signature | Description |
|
|
96
|
+
|----------|-----------|-------------|
|
|
97
|
+
| `open` | `(file_id) -> BinaryIO` | Get file as binary stream (wraps `get()` in `BytesIO`) |
|
|
98
|
+
|
|
99
|
+
**Data class**: `MediaStat` (frozen dataclass with `content_type: str`, `content_length: int`, `status: int`).
|
|
100
|
+
|
|
101
|
+
**Exception**: `MediaError` (subclass of `RuntimeError`).
|
|
102
|
+
|
|
103
|
+
### Modes (fail-closed runtime-mode contract)
|
|
104
|
+
|
|
105
|
+
| `KEELSON_MODE` | Condition | Behaviour |
|
|
106
|
+
|------|-----------|-----------|
|
|
107
|
+
| `keelson` | Both Media env set | Remote (the Keelson media service) |
|
|
108
|
+
| `keelson` | Media env missing | **`MediaError`** — capability unavailable (covers `files_enabled=false`); never local |
|
|
109
|
+
| any | Exactly one of base URL / token set | **`MediaError`** — incomplete remote config |
|
|
110
|
+
| `local` | — | Local filesystem (`MEDIA_DIR`, default `./media`) |
|
|
111
|
+
| unset | Both Media env set | Remote (backward compatibility) |
|
|
112
|
+
| unset | No Media env, platform core env visible (`KEELSON_APP_ID` / `KEELSON_TENANT_ID` / `KEELSON_DEPLOY_ID`) | **`MediaError`** — refuses silent local fallback |
|
|
113
|
+
| unset | No Media env, no platform env | Local filesystem (local development) |
|
|
114
|
+
|
|
115
|
+
The SDK never silently falls back to ephemeral local storage on Keelson: set
|
|
116
|
+
`KEELSON_MODE=local` explicitly for local development.
|
|
117
|
+
|
|
118
|
+
### Environment variables
|
|
119
|
+
|
|
120
|
+
| Variable | Description |
|
|
121
|
+
|----------|-------------|
|
|
122
|
+
| `KEELSON_MODE` | `keelson` (remote, fail-closed) / `local` (local FS) / unset (local development). Platform injects `keelson`. |
|
|
123
|
+
| `KEELSON_INTERNAL_MEDIA_BASE_URL` | Internal endpoint for the Keelson media service (Keelson mode; required with the token). |
|
|
124
|
+
| `KEELSON_APP_MEDIA_TOKEN` | App-scoped bearer token for the Keelson media service; validated by the platform. |
|
|
125
|
+
| `KEELSON_MEDIA_URL_PREFIX` | Public URL prefix (default: `/media/`). |
|
|
126
|
+
| `MEDIA_DIR` | Local storage directory (default: `./media`). Used whenever the SDK resolves to local mode — either explicit `KEELSON_MODE=local`, or zero-config local development (`KEELSON_MODE` unset with no Media env and no platform core env). |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Files (data) SDK (`keelson_files`)
|
|
131
|
+
|
|
132
|
+
Durable file storage for your app's own files — state, settings, caches. Reads
|
|
133
|
+
and writes are always whole-file, and `write()` is write-through: once it
|
|
134
|
+
returns, the data is persisted. There is no background sync and nothing is
|
|
135
|
+
stored on ephemeral local disk. Overwriting an existing key is the normal case;
|
|
136
|
+
updates to the same key are limited to about once per second. For user-uploaded
|
|
137
|
+
or generated media referenced by ID and served over HTTP, use `keelson_media`;
|
|
138
|
+
for data read/written on every request, use the database.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from keelson import files
|
|
142
|
+
|
|
143
|
+
files.write("seen_urls.json", json.dumps(seen))
|
|
144
|
+
seen = json.loads(files.read("seen_urls.json") or "[]") # read() -> bytes | None
|
|
145
|
+
keys = files.list() # sorted list[str]
|
|
146
|
+
files.delete("seen_urls.json") # idempotent
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Cross-language guaranteed API
|
|
150
|
+
|
|
151
|
+
| Function | Description |
|
|
152
|
+
|----------|-------------|
|
|
153
|
+
| `write(key, data)` | Overwrite `key` with bytes/str (str stored UTF-8); write-through |
|
|
154
|
+
| `read(key)` | `bytes`, or `None` when the key is absent (only a 404 is missing) |
|
|
155
|
+
| `delete(key)` | Idempotent delete |
|
|
156
|
+
| `list(prefix="")` | Full, lexicographically-sorted key list; paging absorbed |
|
|
157
|
+
|
|
158
|
+
Key grammar: `/`-separated relative path, well-formed UTF-8 ≤ 512 bytes total and
|
|
159
|
+
≤ 255 bytes per segment, no leading/trailing `/`, no empty / `.` / `..` segments,
|
|
160
|
+
no control characters. One-object soft limit 10 MiB. There is no `exists()` —
|
|
161
|
+
`read()` returning `None` covers it.
|
|
162
|
+
|
|
163
|
+
### Environment variables
|
|
164
|
+
|
|
165
|
+
| Variable | Description |
|
|
166
|
+
|----------|-------------|
|
|
167
|
+
| `KEELSON_MODE` | `keelson` (remote) or `local`; the single mode signal |
|
|
168
|
+
| `KEELSON_FILES_BUCKET` / `KEELSON_FILES_PREFIX` | Platform-injected in `keelson` mode (managed object storage) |
|
|
169
|
+
| `KEELSON_FILES_DIR` | Local-mode directory (default `./.keelson/files`) |
|
|
170
|
+
|
|
171
|
+
Fail-closed: `KEELSON_MODE=keelson` requires bucket + prefix + platform identity;
|
|
172
|
+
missing config raises `FilesError`. See [`./PARITY.md`](./PARITY.md) for the
|
|
173
|
+
full contract.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Identity SDK (`keelson_identity`)
|
|
178
|
+
|
|
179
|
+
User identity and tenant directory lookup. In production, the Keelson auth
|
|
180
|
+
gateway injects trusted `X-Keelson-User-*` headers before requests reach the app.
|
|
181
|
+
Use `get_current_user` when the basic user profile is enough; use
|
|
182
|
+
`get_current_identity` when the app needs tenant role, app permissions, app
|
|
183
|
+
roles, or group attributes.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
import os
|
|
187
|
+
|
|
188
|
+
from keelson_identity import (
|
|
189
|
+
get_current_user,
|
|
190
|
+
get_current_identity,
|
|
191
|
+
list_members,
|
|
192
|
+
get_user,
|
|
193
|
+
list_groups,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
user = get_current_user(headers=request.headers)
|
|
197
|
+
print(user.email)
|
|
198
|
+
|
|
199
|
+
identity = get_current_identity(
|
|
200
|
+
headers=request.headers,
|
|
201
|
+
app_token=os.environ["KEELSON_DIRECTORY_TOKEN"],
|
|
202
|
+
)
|
|
203
|
+
print(identity.tenant.role)
|
|
204
|
+
print(identity.app.permissions) # ["manage", "view"]
|
|
205
|
+
if identity.attributes:
|
|
206
|
+
print(identity.attributes.groups) # ["developers", "everyone"]
|
|
207
|
+
|
|
208
|
+
# Directory lookup as the app actor
|
|
209
|
+
page = list_members(
|
|
210
|
+
app_token=os.environ["KEELSON_DIRECTORY_TOKEN"],
|
|
211
|
+
limit=25, offset=0, q="alice",
|
|
212
|
+
)
|
|
213
|
+
for member in page.items:
|
|
214
|
+
print(member.id, member.email, member.name)
|
|
215
|
+
|
|
216
|
+
member = get_user("user-id-here", app_token=os.environ["KEELSON_DIRECTORY_TOKEN"])
|
|
217
|
+
groups = list_groups(app_token=os.environ["KEELSON_DIRECTORY_TOKEN"])
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Cross-language guaranteed API
|
|
221
|
+
|
|
222
|
+
| Function | Signature | Description |
|
|
223
|
+
|----------|-----------|-------------|
|
|
224
|
+
| `get_current_user` | `(headers=...) -> UserIdentity` | Parse the current user's basic profile from trusted `X-Keelson-User-*` headers; no network call |
|
|
225
|
+
| `get_current_identity` | `(headers=..., app_token=...) -> CurrentIdentity` | Fetch the current user's full identity as the app actor |
|
|
226
|
+
| `list_members` | `(*, base_url=None, cookie=None, authorization=None, app_token=None, **filters) -> PaginatedMembers` | List tenant members |
|
|
227
|
+
| `get_user` | `(user_id, *, base_url=None, cookie=None, authorization=None, app_token=None) -> MemberItem` | Get user by ID |
|
|
228
|
+
| `list_groups` | `(*, base_url=None, cookie=None, authorization=None, app_token=None) -> list[GroupItem]` | List tenant groups |
|
|
229
|
+
|
|
230
|
+
`get_current_user` and `get_current_identity` accept common request header
|
|
231
|
+
mappings. The required header is `x-keelson-user-id`; `x-keelson-user-email`
|
|
232
|
+
and `x-keelson-user-name` are optional.
|
|
233
|
+
|
|
234
|
+
Directory functions also support app-as-actor access with `app_token` or the
|
|
235
|
+
`KEELSON_DIRECTORY_TOKEN` env fallback. Keep app tokens on the server.
|
|
236
|
+
|
|
237
|
+
### Python-specific helpers
|
|
238
|
+
|
|
239
|
+
| Function | Signature | Description |
|
|
240
|
+
|----------|-----------|-------------|
|
|
241
|
+
| `is_local_mode` | `() -> bool` | Check if running in local mode |
|
|
242
|
+
|
|
243
|
+
**Data classes**: `CurrentIdentity`, `UserIdentity`, `TenantIdentity`, `AppIdentity`, `AttributesIdentity`, `MemberItem`, `PaginatedMembers`, `GroupItem`.
|
|
244
|
+
|
|
245
|
+
**Exception**: `IdentityError`.
|
|
246
|
+
|
|
247
|
+
### Filtering members by group
|
|
248
|
+
|
|
249
|
+
`list_members` narrows results to a single group via either `group_id` or
|
|
250
|
+
`group_key`:
|
|
251
|
+
|
|
252
|
+
- `group_key` — the group's code-facing identifier. Always present, stable, and
|
|
253
|
+
immutable. **Prefer this for code references.** Keys may be non-ASCII (e.g. a
|
|
254
|
+
Japanese `経理`).
|
|
255
|
+
- `group_id` — a stable UUID for machine integration / internal wiring.
|
|
256
|
+
|
|
257
|
+
Passing both raises `IdentityError`. On `GroupItem`, `key` is `str | None`
|
|
258
|
+
kept nullable for backward compatibility, but the server always populates it;
|
|
259
|
+
`id` is the UUID.
|
|
260
|
+
|
|
261
|
+
### `attributes.groups` vs `list_groups()`
|
|
262
|
+
|
|
263
|
+
- **`attributes.groups`** (from `get_current_identity()`): the group keys the *current user* belongs to, **scoped to the current app** — the system groups the caller holds by role (a subset of `owners`/`admins`/`developers`/`everyone`, not all four: an OWNER gets `owners`/`developers`/`everyone`, an app user gets only `everyone`; exposed regardless of app binding) plus custom groups bound to this app (via a view/manage permission binding or an app-role binding). Custom groups not bound to the app are excluded, and a system group the caller does not hold never appears. Keys are stable and immutable, so they are safe for authorization checks; bind a group to the app if you need to branch on it.
|
|
264
|
+
- **`list_groups()`**: all groups in the *tenant* (each with its stable `id` and `key`). Use for building UI pickers and admin views.
|
|
265
|
+
|
|
266
|
+
### Modes
|
|
267
|
+
|
|
268
|
+
| Mode | Condition | Behaviour |
|
|
269
|
+
|------|-----------|-----------|
|
|
270
|
+
| Local | `KEELSON_LOCAL_MODE=1` | Returns deterministic fixture data (no HTTP calls) |
|
|
271
|
+
| Keelson | Default | Calls the Keelson auth gateway via the platform-injected `KEELSON_DIRECTORY_BASE_URL`. |
|
|
272
|
+
|
|
273
|
+
### Environment variables
|
|
274
|
+
|
|
275
|
+
| Variable | Description |
|
|
276
|
+
|----------|-------------|
|
|
277
|
+
| `KEELSON_LOCAL_MODE` | Set to `1` to enable local mode (returns fixture data, no HTTP calls). |
|
|
278
|
+
| `KEELSON_DIRECTORY_BASE_URL` | **Canonical, platform-injected** base URL for `get_current_identity` and Directory calls. Use this. |
|
|
279
|
+
| `KEELSON_DIRECTORY_TOKEN` | App token for app-as-actor identity and Directory access. |
|
|
280
|
+
| `KEELSON_IDENTITY_BASE_URL` | Deprecated compatibility alias used only when neither `base_url` nor `KEELSON_DIRECTORY_BASE_URL` is set. |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Email SDK (`keelson_email`)
|
|
285
|
+
|
|
286
|
+
Inbound/outbound email.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from keelson import email
|
|
290
|
+
|
|
291
|
+
# Send
|
|
292
|
+
email.send(
|
|
293
|
+
to="user@example.com",
|
|
294
|
+
subject="Hello",
|
|
295
|
+
text="Plain text body",
|
|
296
|
+
html="<p>HTML body</p>",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Receive inbound emails via decorator
|
|
300
|
+
@email.on_receive
|
|
301
|
+
def handle(msg: email.InboundMessage):
|
|
302
|
+
print(msg.subject, msg.text)
|
|
303
|
+
email.send(
|
|
304
|
+
to=msg.reply_to or msg.from_,
|
|
305
|
+
subject=f"Re: {msg.subject}",
|
|
306
|
+
text="Got it!",
|
|
307
|
+
in_reply_to=msg.provider_message_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Handle bounce/complaint events
|
|
311
|
+
@email.on_event
|
|
312
|
+
def handle_event(event: email.EmailEventPayload):
|
|
313
|
+
if event.event_type == "bounce":
|
|
314
|
+
print("Bounced:", event.email_address)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Cross-language guaranteed API
|
|
318
|
+
|
|
319
|
+
| Function | Signature | Description |
|
|
320
|
+
|----------|-----------|-------------|
|
|
321
|
+
| `send` | `(to, subject, text=None, html=None, **kwargs)` | Send an email |
|
|
322
|
+
| `verify_webhook` | `(body, headers, secret) -> InboundMessage` | Verify Svix signature and parse inbound email |
|
|
323
|
+
| `verify_webhook_bytes` | `(body, headers, secret) -> InboundMessage` | Verify inbound email from raw bytes |
|
|
324
|
+
| `verify_event_webhook` | `(body, headers, secret) -> EmailEventPayload` | Verify Svix signature and parse event |
|
|
325
|
+
| `verify_event_webhook_bytes` | `(body, headers, secret) -> EmailEventPayload` | Verify event from raw bytes |
|
|
326
|
+
|
|
327
|
+
Attachment download is also guaranteed; in Python it is an instance method on `InboundAttachment`:
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
for att in msg.attachments:
|
|
331
|
+
data = att.download() # -> bytes
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Python-specific helpers
|
|
335
|
+
|
|
336
|
+
| Function | Signature | Description |
|
|
337
|
+
|----------|-----------|-------------|
|
|
338
|
+
| `on_receive` | `(handler)` | Decorator to handle inbound email (auto-starts webhook server) |
|
|
339
|
+
| `on_event` | `(handler)` | Decorator to handle bounce/complaint/delivery events |
|
|
340
|
+
| `serve` | `(**kwargs)` | Start webhook server manually |
|
|
341
|
+
|
|
342
|
+
When `KEELSON_EMAIL_WEBHOOK_SECRET` is set, `serve()` and the decorators automatically verify inbound webhooks.
|
|
343
|
+
|
|
344
|
+
**Data classes**: `Address`, `Attachment`, `InboundMessage`, `InboundAttachment`, `EmailEventPayload`, `SpamAssessment`, `AuthenticationResult`.
|
|
345
|
+
|
|
346
|
+
**Exception**: `EmailError`.
|
|
347
|
+
|
|
348
|
+
### Environment variables
|
|
349
|
+
|
|
350
|
+
| Variable | Description |
|
|
351
|
+
|----------|-------------|
|
|
352
|
+
| `KEELSON_EMAIL_API_URL` | Email API endpoint (required). |
|
|
353
|
+
| `KEELSON_EMAIL_TOKEN` | Bearer token (required). |
|
|
354
|
+
| `KEELSON_EMAIL_WEBHOOK_SECRET` | Optional Svix signing secret for auto-verification. |
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Testing
|
|
359
|
+
|
|
360
|
+
Run all SDK tests:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
uv sync --group dev
|
|
364
|
+
uv run pytest
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Build the source distribution and wheel from the repository root:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
uv build
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Run tests for individual modules:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
uv run pytest keelson_media/tests/
|
|
377
|
+
uv run pytest keelson_identity/tests/
|
|
378
|
+
uv run pytest keelson_email/tests/
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Lint:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
uv run ruff check .
|
|
385
|
+
```
|