nohead 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.
- nohead-0.1.0/.gitignore +6 -0
- nohead-0.1.0/CHANGELOG.md +21 -0
- nohead-0.1.0/LICENSE +21 -0
- nohead-0.1.0/PKG-INFO +364 -0
- nohead-0.1.0/README.md +340 -0
- nohead-0.1.0/pyproject.toml +62 -0
- nohead-0.1.0/src/nohead/__init__.py +77 -0
- nohead-0.1.0/src/nohead/_async/__init__.py +0 -0
- nohead-0.1.0/src/nohead/_async/_client.py +184 -0
- nohead-0.1.0/src/nohead/_async/_nohead.py +147 -0
- nohead-0.1.0/src/nohead/_async/resources/__init__.py +0 -0
- nohead-0.1.0/src/nohead/_async/resources/_resource.py +8 -0
- nohead-0.1.0/src/nohead/_async/resources/assets.py +128 -0
- nohead-0.1.0/src/nohead/_async/resources/other.py +47 -0
- nohead-0.1.0/src/nohead/_async/resources/records.py +343 -0
- nohead-0.1.0/src/nohead/_async/resources/schema.py +330 -0
- nohead-0.1.0/src/nohead/_async/resources/webhooks.py +128 -0
- nohead-0.1.0/src/nohead/_base.py +275 -0
- nohead-0.1.0/src/nohead/_errors.py +182 -0
- nohead-0.1.0/src/nohead/_generated/__init__.py +0 -0
- nohead-0.1.0/src/nohead/_generated/operations.py +70 -0
- nohead-0.1.0/src/nohead/_pagination.py +129 -0
- nohead-0.1.0/src/nohead/_sync/__init__.py +0 -0
- nohead-0.1.0/src/nohead/_sync/_client.py +185 -0
- nohead-0.1.0/src/nohead/_sync/_nohead.py +148 -0
- nohead-0.1.0/src/nohead/_sync/resources/__init__.py +0 -0
- nohead-0.1.0/src/nohead/_sync/resources/_resource.py +9 -0
- nohead-0.1.0/src/nohead/_sync/resources/assets.py +129 -0
- nohead-0.1.0/src/nohead/_sync/resources/other.py +48 -0
- nohead-0.1.0/src/nohead/_sync/resources/records.py +344 -0
- nohead-0.1.0/src/nohead/_sync/resources/schema.py +329 -0
- nohead-0.1.0/src/nohead/_sync/resources/webhooks.py +125 -0
- nohead-0.1.0/src/nohead/_uploads.py +109 -0
- nohead-0.1.0/src/nohead/_version.py +1 -0
- nohead-0.1.0/src/nohead/models.py +1073 -0
- nohead-0.1.0/src/nohead/params.py +129 -0
- nohead-0.1.0/src/nohead/py.typed +0 -0
- nohead-0.1.0/src/nohead/types.py +34 -0
- nohead-0.1.0/src/nohead/webhooks.py +123 -0
nohead-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to the `nohead` package that you can notice. Versions follow
|
|
4
|
+
[Semantic Versioning](https://semver.org): additive API changes are minor
|
|
5
|
+
releases; a change that could break your code is a major one. Each release's
|
|
6
|
+
section is its GitHub release's notes.
|
|
7
|
+
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
The first release.
|
|
11
|
+
|
|
12
|
+
- `Nohead` and `AsyncNohead`, with the same methods for every operation an
|
|
13
|
+
API key can call: records (with revisions, scheduling, bulk changes and
|
|
14
|
+
search), collections, fields and migrations, assets, webhooks and their
|
|
15
|
+
deliveries, the audit log, feature flags.
|
|
16
|
+
- Pages you can loop over (`for`, or `async for`), lenient Pydantic models,
|
|
17
|
+
typed request bodies.
|
|
18
|
+
- Typed errors per API error type, retries with idempotency keys,
|
|
19
|
+
`if_match` and change notes.
|
|
20
|
+
- `assets.upload()` in one call (paths, bytes or files), and webhook
|
|
21
|
+
verification (`nohead.webhooks.unwrap`).
|
nohead-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nohead
|
|
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.
|
nohead-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nohead
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Nohead API
|
|
5
|
+
Project-URL: Homepage, https://nohead.io
|
|
6
|
+
Project-URL: Repository, https://github.com/nohead-io/nohead-python
|
|
7
|
+
Project-URL: Issues, https://github.com/nohead-io/nohead-python/issues
|
|
8
|
+
Author-email: Nohead <hello@nohead.io>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,cms,headless,nohead,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx<1,>=0.28
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Nohead Python SDK
|
|
26
|
+
|
|
27
|
+
The official Python client for the [Nohead](https://nohead.io) API, sync and async: typed models, pagination you can loop over, retries that are safe for writes, one-call uploads and webhook verification.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from nohead import Nohead
|
|
31
|
+
|
|
32
|
+
nohead = Nohead() # reads NOHEAD_API_KEY
|
|
33
|
+
|
|
34
|
+
for post in nohead.records.list("posts", filter={"status": "published"}):
|
|
35
|
+
print(post.data["title"])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **Status:** 0.x, not yet published to PyPI. Until it is, install from GitHub: `pip install git+https://github.com/nohead-io/nohead-python`.
|
|
39
|
+
|
|
40
|
+
## Contents
|
|
41
|
+
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [Configuration](#configuration)
|
|
44
|
+
- [Async](#async)
|
|
45
|
+
- [Records](#records)
|
|
46
|
+
- [Pagination](#pagination)
|
|
47
|
+
- [Errors](#errors)
|
|
48
|
+
- [Retries and idempotency](#retries-and-idempotency)
|
|
49
|
+
- [Concurrency](#concurrency)
|
|
50
|
+
- [Assets](#assets)
|
|
51
|
+
- [Search](#search)
|
|
52
|
+
- [Schema](#schema)
|
|
53
|
+
- [Webhooks](#webhooks)
|
|
54
|
+
- [Types and models](#types-and-models)
|
|
55
|
+
- [Reference](#reference)
|
|
56
|
+
- [Development](#development)
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install nohead
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It needs Python 3.11 or newer. Its dependencies are httpx and Pydantic 2. Use it on servers: API keys are secrets.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
nohead = Nohead(
|
|
70
|
+
api_key=os.environ["NOHEAD_API_KEY"], # default: NOHEAD_API_KEY
|
|
71
|
+
base_url="https://api.nohead.io", # default: NOHEAD_API_URL, else production
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Option | Default | |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `api_key` | `NOHEAD_API_KEY` | A project API key (`sk_live_...`). Required. |
|
|
78
|
+
| `base_url` | `NOHEAD_API_URL`, else `https://api.nohead.io` | |
|
|
79
|
+
| `project_id` | the key's project | Looked up once with `GET /v1/me` when omitted. |
|
|
80
|
+
| `max_retries` | `2` | See [retries](#retries-and-idempotency). |
|
|
81
|
+
| `timeout` | `60.0` | Seconds per attempt. |
|
|
82
|
+
| `headers` | none | Added to every request. |
|
|
83
|
+
| `warnings` | `True` | Warns (`NoheadWarning`) about deprecated operations and plan usage, once each. |
|
|
84
|
+
| `http_client` | a new `httpx.Client` | Bring your own for proxies or custom transports. |
|
|
85
|
+
|
|
86
|
+
API keys belong to a project, so methods like `collections.list()` need no project ID. Collections can be named by ID or slug everywhere.
|
|
87
|
+
|
|
88
|
+
Close the client when you're done with it, or use it as a context manager: `with Nohead() as nohead: ...`.
|
|
89
|
+
|
|
90
|
+
`nohead.with_options(timeout=5, max_retries=0)` returns a client with other settings that shares the same connections.
|
|
91
|
+
|
|
92
|
+
## Async
|
|
93
|
+
|
|
94
|
+
`AsyncNohead` has the same methods; await them, and loop over lists with `async for`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from nohead import AsyncNohead
|
|
98
|
+
|
|
99
|
+
async with AsyncNohead() as nohead:
|
|
100
|
+
post = await nohead.records.get("rec_01J9...")
|
|
101
|
+
async for record in nohead.records.list("posts"):
|
|
102
|
+
print(record.data["title"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Records
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
draft = nohead.records.create("posts", data={"title": "Hello", "author": "rec_01J9..."})
|
|
109
|
+
post = nohead.records.get(draft.id, expand=["author"])
|
|
110
|
+
nohead.records.update(post.id, data={"title": "Hello again"}) # None clears a field
|
|
111
|
+
nohead.records.publish(post.id)
|
|
112
|
+
nohead.records.schedule(post.id, unpublish_at=datetime(2027, 1, 1, tzinfo=UTC))
|
|
113
|
+
nohead.records.delete(post.id) # soft delete; records.restore() undoes it
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Methods return models and raise on failure. Field values are in `record.data`, a dict keyed by field API key.
|
|
117
|
+
|
|
118
|
+
**More:**
|
|
119
|
+
|
|
120
|
+
- `count`, and `bulk` (up to 100 records at once)
|
|
121
|
+
- `diff(record, from_revision, to_revision)`
|
|
122
|
+
- `revisions.list`, `revisions.get` and `revisions.revert` (with `dry_run=True` for a preview)
|
|
123
|
+
|
|
124
|
+
## Pagination
|
|
125
|
+
|
|
126
|
+
List methods return the first page, which you can also loop over:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Every record, fetching pages as needed
|
|
130
|
+
for record in nohead.records.list("posts"):
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
# One page at a time
|
|
134
|
+
page = nohead.records.list("posts", limit=100)
|
|
135
|
+
page.data # this page's records
|
|
136
|
+
page.meta # next_cursor, has_more
|
|
137
|
+
while page.has_next_page():
|
|
138
|
+
page = page.get_next_page()
|
|
139
|
+
|
|
140
|
+
# Resume from a saved cursor
|
|
141
|
+
nohead.records.list("posts", cursor=saved_cursor)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
With `AsyncNohead`, `await nohead.records.list(...)` gives the first page and `async for` walks them all.
|
|
145
|
+
|
|
146
|
+
Filters are equality filters (for fields with several values: "contains"), and accept strings, numbers, booleans and datetimes:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
nohead.records.list(
|
|
150
|
+
"posts",
|
|
151
|
+
filter={"status": "published", "featured": True, "author": "rec_01J9..."},
|
|
152
|
+
sort="-published_at",
|
|
153
|
+
expand=["author", "tags"],
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Errors
|
|
158
|
+
|
|
159
|
+
Every exception is a `NoheadError`. API errors are `APIError`s with `status`, `type`, `message`, `request_id`, `details` and `headers`, in a class per type:
|
|
160
|
+
|
|
161
|
+
| Class | Status |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `InvalidRequestError` | 400 |
|
|
164
|
+
| `AuthenticationError` | 401 |
|
|
165
|
+
| `PlanLimitExceededError` | 402 |
|
|
166
|
+
| `AuthorizationError` | 403 |
|
|
167
|
+
| `NotFoundError` | 404 |
|
|
168
|
+
| `ConflictError` | 409 |
|
|
169
|
+
| `PreconditionFailedError` | 412 (`current_revision`) |
|
|
170
|
+
| `ValidationError` | 422 |
|
|
171
|
+
| `RateLimitError` | 429 (`retry_after`) |
|
|
172
|
+
| `InternalServerError` | 500 and other 5xx |
|
|
173
|
+
| `ServiceUnavailableError` | 503 |
|
|
174
|
+
|
|
175
|
+
Other errors:
|
|
176
|
+
|
|
177
|
+
- `APIConnectionError`, and `APITimeoutError`, which is a kind of `APIConnectionError`
|
|
178
|
+
- `UploadError`
|
|
179
|
+
- `WebhookVerificationError`
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from nohead import ValidationError
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
nohead.records.create("posts", data={})
|
|
186
|
+
except ValidationError as error:
|
|
187
|
+
for detail in error.details:
|
|
188
|
+
print(detail.field, detail.code, detail.message)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Retries and idempotency
|
|
192
|
+
|
|
193
|
+
Failed requests are retried twice by default (`max_retries`), with exponential backoff:
|
|
194
|
+
|
|
195
|
+
- what's retried: connection errors, timeouts, 429, 500, 502, 503, 504, and a 409 for a request that is still running
|
|
196
|
+
- `Retry-After` is honored up to 60 seconds; a longer one raises `RateLimitError` straight away
|
|
197
|
+
|
|
198
|
+
Every write gets an `Idempotency-Key` that stays the same across its retries, so a retry after a lost response never writes twice. To make a write safe across your own retries (a job that may run twice), pass a key:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
nohead.records.create("posts", data=data, idempotency_key=f"import-{row.id}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Writes also take `change_note`, a reason shown in history.
|
|
205
|
+
|
|
206
|
+
## Concurrency
|
|
207
|
+
|
|
208
|
+
Pass the revision you read to make sure nobody changed the record since:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from nohead import PreconditionFailedError
|
|
212
|
+
|
|
213
|
+
post = nohead.records.get(record_id)
|
|
214
|
+
try:
|
|
215
|
+
nohead.records.update(record_id, data={"title": title}, if_match=post)
|
|
216
|
+
except PreconditionFailedError as error:
|
|
217
|
+
... # changed since (now at error.current_revision): reload, and merge or ask
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`if_match` takes a record or a revision number, on `update`, `delete`, `publish`, `unpublish` and `revisions.revert`.
|
|
221
|
+
|
|
222
|
+
## Assets
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
asset = nohead.assets.upload("cover.jpg")
|
|
226
|
+
nohead.records.update(record_id, data={"cover": asset.id})
|
|
227
|
+
|
|
228
|
+
url = nohead.assets.image_url(asset.id, width=1200, format="webp").url
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**What `upload` accepts:** a path, bytes, or a file opened in binary mode.
|
|
232
|
+
|
|
233
|
+
**What it does:**
|
|
234
|
+
|
|
235
|
+
1. Creates the upload.
|
|
236
|
+
2. Sends the bytes straight to storage.
|
|
237
|
+
3. Completes the upload, which checks the file, and returns the `ready` asset.
|
|
238
|
+
|
|
239
|
+
**Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
|
|
240
|
+
|
|
241
|
+
**Uploading from a browser:** create the upload on your server with `create_upload`, `PUT` the file from the browser, then `complete` it.
|
|
242
|
+
|
|
243
|
+
## Search
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# One collection
|
|
247
|
+
for hit in nohead.records.search("posts", "content model"):
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
# Across the project
|
|
251
|
+
results = nohead.search("content model", collections=["posts", "pages"])
|
|
252
|
+
results.meta.total_estimate
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Search needs `search_enabled` collections and the `search:read` scope. It pages through the first 1,000 hits.
|
|
256
|
+
|
|
257
|
+
## Schema
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
nohead.collections.create(
|
|
261
|
+
name="Posts",
|
|
262
|
+
slug="posts",
|
|
263
|
+
fields=[{"name": "Title", "api_key": "title", "type": "text", "required": True}],
|
|
264
|
+
)
|
|
265
|
+
nohead.fields.create("posts", name="Summary", api_key="summary", type="long_text")
|
|
266
|
+
|
|
267
|
+
# Changes that rewrite records go through a migration; preview first
|
|
268
|
+
preview = nohead.fields.migrate("fld_...", type="long_text", dry_run=True)
|
|
269
|
+
migration = nohead.fields.migrate("fld_...", type="long_text")
|
|
270
|
+
nohead.migrations.get(migration.id)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
For schema as code, see the `nohead` CLI (`nohead schema pull/diff/push`).
|
|
274
|
+
|
|
275
|
+
## Webhooks
|
|
276
|
+
|
|
277
|
+
Verify a webhook request, then use its event:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from nohead.webhooks import unwrap
|
|
281
|
+
|
|
282
|
+
# e.g. in a Flask view
|
|
283
|
+
event = unwrap(request.get_data(), request.headers, secret=os.environ["NOHEAD_WEBHOOK_SECRET"])
|
|
284
|
+
if event.type == "record.published" and event.record:
|
|
285
|
+
rebuild(event.record.collection)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`unwrap` checks the signature and the timestamp (Standard Webhooks), and raises `WebhookVerificationError` if either is off. Pass the raw body: parsing and re-serializing JSON changes the bytes.
|
|
289
|
+
|
|
290
|
+
`event.data` is the payload. For convenience, `event.record`, `event.asset`, `event.collection`, `event.schema_change` and `event.webhook` parse its parts, and are None when the event has no such part.
|
|
291
|
+
|
|
292
|
+
It's also available as `nohead.webhooks.unwrap(...)` on a client. Events can arrive more than once, so deduplicate by the `webhook-id` header.
|
|
293
|
+
|
|
294
|
+
## Types and models
|
|
295
|
+
|
|
296
|
+
- **Responses:** Pydantic models, in `nohead.models`. The main ones (`Record`, `Collection`, `Field`, `Asset`, `Webhook`) are also importable from `nohead`.
|
|
297
|
+
- **They're lenient on purpose, because the API adds fields and values without notice:**
|
|
298
|
+
- Fields the SDK doesn't know yet are kept, in `model_extra`.
|
|
299
|
+
- Enums are plain strings.
|
|
300
|
+
- A response that doesn't match the models at all is still returned, unvalidated, with a `NoheadWarning`, rather than raised.
|
|
301
|
+
- **Timestamps** are `datetime`s.
|
|
302
|
+
- **Request bodies** are typed with the TypedDicts in `nohead.params`, so a type checker catches a misspelled field.
|
|
303
|
+
|
|
304
|
+
## Reference
|
|
305
|
+
|
|
306
|
+
| Resource | Methods |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `records` | `list`, `get`, `create`, `update`, `delete`, `restore`, `publish`, `unpublish`, `schedule`, `unschedule`, `count`, `bulk`, `diff`, `search` |
|
|
309
|
+
| `records.revisions` | `list`, `get`, `revert` |
|
|
310
|
+
| `search` | across the project |
|
|
311
|
+
| `collections` | `list`, `get`, `create`, `update`, `delete`, `restore`, `schema` |
|
|
312
|
+
| `collections.schema_changes` | `list`, `get` |
|
|
313
|
+
| `collections.search_index` | `get`, `rebuild` |
|
|
314
|
+
| `fields` | `list`, `create`, `update`, `delete`, `restore`, `reorder`, `remove_alias`, `migrate` |
|
|
315
|
+
| `migrations` | `list`, `get`, `cancel` |
|
|
316
|
+
| `assets` | `upload`, `create_upload`, `complete`, `list`, `get`, `delete`, `restore`, `image_url`, `download_url` |
|
|
317
|
+
| `webhooks` | `list`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `unwrap` |
|
|
318
|
+
| `webhooks.deliveries` | `list`, `get`, `retry` |
|
|
319
|
+
| `audit_events` | `list` |
|
|
320
|
+
| `feature_flags` | `list` |
|
|
321
|
+
| `me` | `get` |
|
|
322
|
+
| `health` | `check` |
|
|
323
|
+
|
|
324
|
+
The SDK covers every operation an API key can call. Organizations, projects, members and API keys are managed in the web app. The full API is documented at [docs.nohead.io](https://docs.nohead.io).
|
|
325
|
+
|
|
326
|
+
## Development
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
uv sync # Python 3.11+ and the dev tools
|
|
330
|
+
uv run pytest # unit and contract tests, both clients
|
|
331
|
+
uv run ruff format . && uv run ruff check . && uv run pyright
|
|
332
|
+
uv run python scripts/generate.py # after updating openapi.json
|
|
333
|
+
uv run python scripts/unasync.py # after changing src/nohead/_async
|
|
334
|
+
uv run python scripts/samples.py # after changing tests/calls.py (the docs' code samples)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**How the code is organized:**
|
|
338
|
+
|
|
339
|
+
- `openapi.json` is the API's published contract. `scripts/generate.py` derives three files from it:
|
|
340
|
+
- `src/nohead/models.py`
|
|
341
|
+
- `src/nohead/params.py`
|
|
342
|
+
- the operation table, `src/nohead/_generated/operations.py`
|
|
343
|
+
- **The async client is the source.** `src/nohead/_async` is written by hand, and `scripts/unasync.py` generates the sync client in `src/nohead/_sync` from it. Edit only the async code.
|
|
344
|
+
- `tests/test_contract.py` calls every public method of both clients. It fails when an API-key operation in the contract has no method, or when a request doesn't match its operation.
|
|
345
|
+
|
|
346
|
+
The smoke test (`smoke/smoke.py`) runs the core flow against a real API, with both clients, using the built package. Nohead's own CI runs it on every API contract change.
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
uv build
|
|
350
|
+
NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=sk_live_... \
|
|
351
|
+
uv run --isolated --no-project --with dist/nohead-0.1.0-py3-none-any.whl python smoke/smoke.py
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Releasing
|
|
355
|
+
|
|
356
|
+
1. Bump the version in `pyproject.toml` and `src/nohead/_version.py`.
|
|
357
|
+
2. Add a section for it to `CHANGELOG.md` (`## 1.2.3`), which becomes the release's notes.
|
|
358
|
+
3. Merge to `main`. Its ruleset requires the **CI passed** check, so the commit goes through a pull request or a branch whose CI passed, and force pushes are refused.
|
|
359
|
+
4. Run the **SDK release** workflow in the Nohead API repository. It runs this commit's smoke test against the API and pushes the tag `v1.2.3`. Nobody else can push `v*` tags: a tag ruleset lets only that workflow's deploy key through.
|
|
360
|
+
5. The tag starts `.github/workflows/release.yml`. Its publishing job runs in the `release` environment, which only `v*` tags can use, and the registry's trusted publisher accepts only that environment. It checks the version and its notes, tests and builds, and publishes to PyPI through trusted publishing (no token, with attestations). Then it creates the GitHub release with the built files.
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT
|