trmnl-terminus 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.
@@ -0,0 +1,15 @@
1
+ .env
2
+ .env.*
3
+ .venv/
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ smoke-artifacts/
15
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hsperker
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,182 @@
1
+ Metadata-Version: 2.5
2
+ Name: trmnl-terminus
3
+ Version: 0.1.0
4
+ Summary: Synchronous Python SDK for Terminus Server
5
+ Project-URL: Documentation, https://github.com/hsperker/trmnl-terminus-sdk#readme
6
+ Project-URL: Issues, https://github.com/hsperker/trmnl-terminus-sdk/issues
7
+ Project-URL: Repository, https://github.com/hsperker/trmnl-terminus-sdk
8
+ Author: hsperker
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: e-ink,sdk,terminus,trmnl
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: httpx<1,>=0.27
19
+ Requires-Dist: pydantic<3,>=2
20
+ Description-Content-Type: text/markdown
21
+
22
+ # TRMNL Terminus Python SDK
23
+
24
+ `trmnl-terminus` is a small, synchronous Python client for automating a
25
+ self-hosted [Terminus](https://github.com/usetrmnl/terminus) server. It handles
26
+ authentication and token rotation, validates requests and responses, and keeps
27
+ the underlying HTTP contract visible.
28
+
29
+ ## Compatibility
30
+
31
+ SDK `0.1.x` supports exactly Terminus tag `0.71.0`, commit
32
+ [`e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56`](https://github.com/usetrmnl/terminus/commit/e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56).
33
+ It makes no compatibility claim for Terminus `main`, older releases, or later
34
+ releases.
35
+
36
+ Python 3.12 or newer is required.
37
+
38
+ ## Install
39
+
40
+ Add the SDK to an uv project:
41
+
42
+ ```sh
43
+ uv add trmnl-terminus
44
+ ```
45
+
46
+ Or install it with pip:
47
+
48
+ ```sh
49
+ python -m pip install trmnl-terminus
50
+ ```
51
+
52
+ To work from source, clone this repository and create its locked environment:
53
+
54
+ ```sh
55
+ git clone https://github.com/hsperker/trmnl-terminus-sdk.git
56
+ cd trmnl-terminus-sdk
57
+ uv sync --locked
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ Terminus authenticates with an email address and password. Keep both outside
63
+ your source code:
64
+
65
+ ```sh
66
+ export TERMINUS_BASE_URL="https://terminus.example.test"
67
+ export TERMINUS_EMAIL="you@example.test"
68
+ export TERMINUS_PASSWORD="..."
69
+ ```
70
+
71
+ Then inspect your devices without changing the server:
72
+
73
+ ```python
74
+ import os
75
+
76
+ from pydantic import SecretStr
77
+
78
+ from trmnl_terminus import Credentials, TerminusClient
79
+
80
+ credentials = Credentials(
81
+ email=os.environ["TERMINUS_EMAIL"],
82
+ password=SecretStr(os.environ["TERMINUS_PASSWORD"]),
83
+ )
84
+
85
+ with TerminusClient(
86
+ os.environ["TERMINUS_BASE_URL"],
87
+ credentials=credentials,
88
+ ) as client:
89
+ for device in client.devices.list():
90
+ print(device.id, device.label, device.playlist_id)
91
+ ```
92
+
93
+ The client logs in on demand. It rotates access and refresh tokens when needed
94
+ and never writes them to disk unless you supply a `TokenStore`.
95
+
96
+ ## Examples
97
+
98
+ The repository includes two runnable examples:
99
+
100
+ | Example | What it proves | Server changes |
101
+ | --- | --- | --- |
102
+ | [`examples/list_resources.py`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/examples/list_resources.py) | Lists models, devices, screens, and playlists | None |
103
+ | [`examples/render_screen.py`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/examples/render_screen.py) | Creates an HTML screen, downloads its rendered image, then deletes the screen | Temporary screen |
104
+
105
+ Run the read-only example after setting the three variables above:
106
+
107
+ ```sh
108
+ uv run python examples/list_resources.py
109
+ ```
110
+
111
+ Rendering requires an explicit mutation opt-in. Remote-screen deletion runs in
112
+ a `finally` block, so cleanup is attempted even when the image download fails:
113
+
114
+ ```sh
115
+ TERMINUS_ALLOW_MUTATIONS=1 \
116
+ uv run python examples/render_screen.py rendered-screen.png
117
+ ```
118
+
119
+ To prove the full path on real hardware, use the guarded
120
+ [`physical-device proof workflow`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/scripts/run_device_smoke.py).
121
+ It creates a temporary playlist and screen, assigns them to an existing device,
122
+ waits for visual confirmation, restores the original playlist, and cleans up.
123
+ Read the
124
+ [`real-server smoke guide`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/real-server-smoke.md)
125
+ before running it.
126
+
127
+ Playlist assignment does not wake a device. The new image appears on its next
128
+ scheduled poll, power cycle, or manual refresh.
129
+
130
+ ## Supported API
131
+
132
+ | Resource | Operations in `0.1` |
133
+ | --- | --- |
134
+ | `client.models` | `list()` |
135
+ | `client.devices` | `list()`, `get(id)`, `update(id, DevicePatch(...))` |
136
+ | `client.screens` | `list()`, `create(...)`, `delete(id)`, `read_bytes(...)`, `download(...)` |
137
+ | `client.playlists` | `list()`, `get(id)`, `create(...)`, `update(...)`, `delete(id)` |
138
+ | `client.request(...)` | Raw escape hatch for unsupported Server API endpoints |
139
+
140
+ Device updates deliberately support playlist assignment only. Terminus
141
+ `0.71.0` also lacks a native screen lookup endpoint, so the SDK does not fake
142
+ one by fetching every screen. The
143
+ [SDK specification](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/specs/terminus-sdk.md)
144
+ defines the complete boundary and wire contract.
145
+
146
+ ## Errors and secrets
147
+
148
+ HTTP failures raise typed `TerminusError` subclasses. Server responses that use
149
+ RFC Problem Details are available as structured `ProblemDetails` on response
150
+ errors. The original `httpx.Response` remains available for debugging, but
151
+ exception strings do not include credentials, tokens, request bodies, or
152
+ response bodies.
153
+
154
+ TLS certificate verification is enabled by default. For a private certificate
155
+ authority, pass an `ssl.SSLContext`; the examples never disable verification.
156
+
157
+ ## Development
158
+
159
+ uv owns the development environment and lockfile:
160
+
161
+ ```sh
162
+ uv sync --locked
163
+ uv run pytest
164
+ uv run ruff check .
165
+ uv run ruff format --check .
166
+ uv run mypy src examples
167
+ uv build --no-sources
168
+ ```
169
+
170
+ The test suite uses mocked HTTP boundaries. Live server and physical-device
171
+ checks are separate, guarded workflows because they create and delete real
172
+ resources.
173
+
174
+ Maintainers should follow the
175
+ [`release checklist`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/releasing.md);
176
+ publishing uses a GitHub environment and PyPI Trusted Publishing instead of a
177
+ stored API token.
178
+
179
+ ## License
180
+
181
+ MIT. See
182
+ [`LICENSE`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/LICENSE).
@@ -0,0 +1,161 @@
1
+ # TRMNL Terminus Python SDK
2
+
3
+ `trmnl-terminus` is a small, synchronous Python client for automating a
4
+ self-hosted [Terminus](https://github.com/usetrmnl/terminus) server. It handles
5
+ authentication and token rotation, validates requests and responses, and keeps
6
+ the underlying HTTP contract visible.
7
+
8
+ ## Compatibility
9
+
10
+ SDK `0.1.x` supports exactly Terminus tag `0.71.0`, commit
11
+ [`e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56`](https://github.com/usetrmnl/terminus/commit/e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56).
12
+ It makes no compatibility claim for Terminus `main`, older releases, or later
13
+ releases.
14
+
15
+ Python 3.12 or newer is required.
16
+
17
+ ## Install
18
+
19
+ Add the SDK to an uv project:
20
+
21
+ ```sh
22
+ uv add trmnl-terminus
23
+ ```
24
+
25
+ Or install it with pip:
26
+
27
+ ```sh
28
+ python -m pip install trmnl-terminus
29
+ ```
30
+
31
+ To work from source, clone this repository and create its locked environment:
32
+
33
+ ```sh
34
+ git clone https://github.com/hsperker/trmnl-terminus-sdk.git
35
+ cd trmnl-terminus-sdk
36
+ uv sync --locked
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ Terminus authenticates with an email address and password. Keep both outside
42
+ your source code:
43
+
44
+ ```sh
45
+ export TERMINUS_BASE_URL="https://terminus.example.test"
46
+ export TERMINUS_EMAIL="you@example.test"
47
+ export TERMINUS_PASSWORD="..."
48
+ ```
49
+
50
+ Then inspect your devices without changing the server:
51
+
52
+ ```python
53
+ import os
54
+
55
+ from pydantic import SecretStr
56
+
57
+ from trmnl_terminus import Credentials, TerminusClient
58
+
59
+ credentials = Credentials(
60
+ email=os.environ["TERMINUS_EMAIL"],
61
+ password=SecretStr(os.environ["TERMINUS_PASSWORD"]),
62
+ )
63
+
64
+ with TerminusClient(
65
+ os.environ["TERMINUS_BASE_URL"],
66
+ credentials=credentials,
67
+ ) as client:
68
+ for device in client.devices.list():
69
+ print(device.id, device.label, device.playlist_id)
70
+ ```
71
+
72
+ The client logs in on demand. It rotates access and refresh tokens when needed
73
+ and never writes them to disk unless you supply a `TokenStore`.
74
+
75
+ ## Examples
76
+
77
+ The repository includes two runnable examples:
78
+
79
+ | Example | What it proves | Server changes |
80
+ | --- | --- | --- |
81
+ | [`examples/list_resources.py`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/examples/list_resources.py) | Lists models, devices, screens, and playlists | None |
82
+ | [`examples/render_screen.py`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/examples/render_screen.py) | Creates an HTML screen, downloads its rendered image, then deletes the screen | Temporary screen |
83
+
84
+ Run the read-only example after setting the three variables above:
85
+
86
+ ```sh
87
+ uv run python examples/list_resources.py
88
+ ```
89
+
90
+ Rendering requires an explicit mutation opt-in. Remote-screen deletion runs in
91
+ a `finally` block, so cleanup is attempted even when the image download fails:
92
+
93
+ ```sh
94
+ TERMINUS_ALLOW_MUTATIONS=1 \
95
+ uv run python examples/render_screen.py rendered-screen.png
96
+ ```
97
+
98
+ To prove the full path on real hardware, use the guarded
99
+ [`physical-device proof workflow`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/scripts/run_device_smoke.py).
100
+ It creates a temporary playlist and screen, assigns them to an existing device,
101
+ waits for visual confirmation, restores the original playlist, and cleans up.
102
+ Read the
103
+ [`real-server smoke guide`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/real-server-smoke.md)
104
+ before running it.
105
+
106
+ Playlist assignment does not wake a device. The new image appears on its next
107
+ scheduled poll, power cycle, or manual refresh.
108
+
109
+ ## Supported API
110
+
111
+ | Resource | Operations in `0.1` |
112
+ | --- | --- |
113
+ | `client.models` | `list()` |
114
+ | `client.devices` | `list()`, `get(id)`, `update(id, DevicePatch(...))` |
115
+ | `client.screens` | `list()`, `create(...)`, `delete(id)`, `read_bytes(...)`, `download(...)` |
116
+ | `client.playlists` | `list()`, `get(id)`, `create(...)`, `update(...)`, `delete(id)` |
117
+ | `client.request(...)` | Raw escape hatch for unsupported Server API endpoints |
118
+
119
+ Device updates deliberately support playlist assignment only. Terminus
120
+ `0.71.0` also lacks a native screen lookup endpoint, so the SDK does not fake
121
+ one by fetching every screen. The
122
+ [SDK specification](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/specs/terminus-sdk.md)
123
+ defines the complete boundary and wire contract.
124
+
125
+ ## Errors and secrets
126
+
127
+ HTTP failures raise typed `TerminusError` subclasses. Server responses that use
128
+ RFC Problem Details are available as structured `ProblemDetails` on response
129
+ errors. The original `httpx.Response` remains available for debugging, but
130
+ exception strings do not include credentials, tokens, request bodies, or
131
+ response bodies.
132
+
133
+ TLS certificate verification is enabled by default. For a private certificate
134
+ authority, pass an `ssl.SSLContext`; the examples never disable verification.
135
+
136
+ ## Development
137
+
138
+ uv owns the development environment and lockfile:
139
+
140
+ ```sh
141
+ uv sync --locked
142
+ uv run pytest
143
+ uv run ruff check .
144
+ uv run ruff format --check .
145
+ uv run mypy src examples
146
+ uv build --no-sources
147
+ ```
148
+
149
+ The test suite uses mocked HTTP boundaries. Live server and physical-device
150
+ checks are separate, guarded workflows because they create and delete real
151
+ resources.
152
+
153
+ Maintainers should follow the
154
+ [`release checklist`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/docs/releasing.md);
155
+ publishing uses a GitHub environment and PyPI Trusted Publishing instead of a
156
+ stored API token.
157
+
158
+ ## License
159
+
160
+ MIT. See
161
+ [`LICENSE`](https://github.com/hsperker/trmnl-terminus-sdk/blob/main/LICENSE).
@@ -0,0 +1,135 @@
1
+ # Real-server smoke test
2
+
3
+ The smoke test creates and deletes screens and playlists. It never changes a
4
+ device or its assigned playlist.
5
+
6
+ Run it only against a server intended for mutation. Supply configuration through
7
+ the process environment; do not create a repository `.env` file:
8
+
9
+ ```sh
10
+ export TERMINUS_BASE_URL='https://terminus.example.test'
11
+ export TERMINUS_EMAIL='smoke-user@example.test'
12
+ read -rs TERMINUS_PASSWORD
13
+ export TERMINUS_PASSWORD
14
+ export TERMINUS_ALLOW_MUTATION_TESTS=1
15
+ uv run python scripts/run_real_smoke.py
16
+ ```
17
+
18
+ The runner prints no configured URL, email, password, or tokens. It gives every
19
+ resource a random suffix and deletes all created resources even when an assertion
20
+ fails. Cleanup failure makes the run fail.
21
+
22
+ ## Physical-device proof
23
+
24
+ The guarded device runner temporarily assigns its own playlist and screen to one
25
+ existing device, waits for visual confirmation, then restores the device's original
26
+ playlist assignment:
27
+
28
+ ```sh
29
+ export TERMINUS_BASE_URL='https://terminus.example.test'
30
+ export TERMINUS_EMAIL='device-proof-user@example.test'
31
+ read -rs TERMINUS_PASSWORD
32
+ export TERMINUS_PASSWORD
33
+ export TERMINUS_DEVICE_ID=123
34
+ export TERMINUS_ALLOW_DEVICE_MUTATIONS=1
35
+ uv run python scripts/run_device_smoke.py
36
+ ```
37
+
38
+ Use a positive device ID for a device that already has a playlist. An unassigned
39
+ device cannot be used because SDK v0.1 intentionally cannot restore
40
+ `playlist_id: null`.
41
+
42
+ Playlist assignment does not wake a physical device. When the runner says the
43
+ temporary display is assigned, use the device's normal manual refresh or power-cycle
44
+ action. Type `seen` only after the device visibly shows the heading
45
+ `SDK DEVICE PROOF`; the prompt times out after ten minutes.
46
+
47
+ The runner restores the original assignment and verifies it before deleting the
48
+ temporary playlist and screen. Any proof, restoration, or cleanup failure makes the
49
+ run fail. If restoration cannot be verified, deletion is skipped and the uniquely
50
+ SDK-prefixed resources remain for manual recovery. The runner does not print
51
+ configuration values, resource identifiers, labels, HTML, device details, response
52
+ bodies, passwords, or tokens.
53
+
54
+ ## Latest developer evidence
55
+
56
+ On 2026-09-09, the guarded physical-device proof passed against public Terminus
57
+ commit `2d91851b2c038f9964ffb066e4a765b3e88121d2`. Through the public typed SDK, the
58
+ runner created and attached a temporary screen and playlist, assigned the
59
+ playlist, and verified the assignment by reading it back.
60
+
61
+ After a manual device wake, the user saw the black `SDK DEVICE PROOF` heading
62
+ centered on a white background within a solid black frame. The runner then
63
+ restored and read back the original assignment, deleted both temporary resources,
64
+ and exited zero. An independent audit through `devices.list()`, `screens.list()`,
65
+ and `playlists.list()` required exactly one device, confirmed a non-null current
66
+ playlist assignment, and found zero screens or playlists with the proof prefix.
67
+
68
+ That Terminus revision is an untagged commit after 0.71.0. This is developer
69
+ evidence only; it is not the release proof required against a clean 0.71.0
70
+ instance with controlled session settings.
71
+
72
+ ## Developer evidence
73
+
74
+ An existing server can prove that the user-facing path works in that deployment.
75
+ It cannot prove release compatibility unless its Terminus tag and resolved
76
+ commit are known independently.
77
+
78
+ The default developer run proves:
79
+
80
+ - login and raw-token authorization;
81
+ - model listing;
82
+ - playlist and HTML-screen creation;
83
+ - screen-list readback of the created resource;
84
+ - a real rendered-image download;
85
+ - ordered playlist replacement and readback;
86
+ - cleanup.
87
+
88
+ Set `TERMINUS_REFRESH_WAIT_SECONDS` only when the server has a short access-token
89
+ lifetime. The runner then waits, makes another request, and requires both access
90
+ and refresh tokens to rotate.
91
+
92
+ Mutation replay is intentionally absent. Terminus 0.71.0 can keep a previously
93
+ issued access token valid after refresh, and without session expiration that
94
+ token is long-lived. The SDK recovers after a mutating 401 but leaves any retry
95
+ to the caller because the server-side conditions for safe automatic replay
96
+ cannot be proven.
97
+
98
+ ## Release evidence
99
+
100
+ A release run starts with clean storage and a Terminus checkout at the proposed
101
+ tag and resolved commit. Register the first user through Terminus's supported
102
+ registration flow; Terminus verifies the first account automatically. Confirm
103
+ that startup seeded at least one model.
104
+
105
+ Use matching short values for `API_ACCESS_TOKEN_PERIOD`,
106
+ `SESSION_LIFETIME_LIMIT`, and `SESSION_INACTIVITY_LIMIT`, then set
107
+ `TERMINUS_REFRESH_WAIT_SECONDS` long enough to enter the SDK's two-second refresh
108
+ window. Record the tag, commit, image identifier if used, and smoke result in the
109
+ release evidence. Never record credentials or tokens.
110
+
111
+ ### Terminus 0.71.0 release proof
112
+
113
+ On 2026-09-08, the upstream `0.71.0` tag independently resolved to commit
114
+ `e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56`. The official image was
115
+ `ghcr.io/usetrmnl/terminus:0.71.0` with canonical multi-platform index digest
116
+ `sha256:18b672e4958a5a274822b6e34e76b92bcd0a8b483b7728fa559d4fab342262c7`;
117
+ the exercised ARM64 manifest digest was
118
+ `sha256:a5e2f322ea30fbaa5c2630ae02dcbaa6bfc44d889d92228050f4df8d7bb0c462`.
119
+ The image labels reported the same version and commit.
120
+
121
+ The proof used a uniquely named disposable project, fresh PostgreSQL and Valkey
122
+ volumes, and a localhost-only web binding. The first account was created through
123
+ the CSRF-protected `/register` form, and the SDK returned at least one synchronized
124
+ model. `API_ACCESS_TOKEN_PERIOD`, `SESSION_LIFETIME_LIMIT`, and
125
+ `SESSION_INACTIVITY_LIMIT` were each 60 seconds. With a 49.2-second refresh wait
126
+ and the runner's two-second skew, the unchanged smoke runner reported
127
+ `PASS: real screen, image, playlist, and cleanup` after requiring both access and
128
+ refresh token values to change.
129
+
130
+ That run covered HTML-screen rendering, a non-empty rendered-image download,
131
+ screen list readback, ordered playlist replacement and readback, and deletion of
132
+ the created screen and playlist. A separate clean-stack pass followed cleanup
133
+ with an API check and found no matching smoke resources. Final host-side
134
+ inspection found zero project containers, networks, volumes, or temporary runtime
135
+ files.
@@ -0,0 +1,59 @@
1
+ # Releasing `trmnl-terminus`
2
+
3
+ Releases are built and published by
4
+ [`release.yml`](../.github/workflows/release.yml). Do not upload a local build
5
+ or store a PyPI API token in GitHub.
6
+
7
+ ## One-time setup for the first release
8
+
9
+ 1. Create a GitHub environment named `pypi`. Restrict it to protected tags and,
10
+ if desired, require a maintainer's approval.
11
+ 2. In your PyPI account's **Publishing** settings, add a pending GitHub
12
+ publisher with these exact values:
13
+
14
+ | Field | Value |
15
+ | --- | --- |
16
+ | PyPI project name | `trmnl-terminus` |
17
+ | GitHub owner | `hsperker` |
18
+ | GitHub repository | `trmnl-terminus-sdk` |
19
+ | Workflow filename | `release.yml` |
20
+ | Environment name | `pypi` |
21
+
22
+ A pending publisher does not reserve the PyPI name. It becomes a normal
23
+ publisher when the first upload succeeds.
24
+
25
+ ## Release checklist
26
+
27
+ 1. Set the version with `uv version <version>` and commit the updated
28
+ `pyproject.toml` and `uv.lock`.
29
+ 2. Run the same checks as CI:
30
+
31
+ ```sh
32
+ uv sync --locked
33
+ uv run pytest -q
34
+ uv run ruff check .
35
+ uv run ruff format --check .
36
+ uv run mypy src examples
37
+ uv build --no-sources
38
+ ```
39
+
40
+ 3. Merge the release commit to `main` and wait for CI to pass.
41
+ 4. Create and push an annotated tag whose value matches `pyproject.toml`:
42
+
43
+ ```sh
44
+ git tag -a v0.1.0 -m v0.1.0
45
+ git push origin v0.1.0
46
+ ```
47
+
48
+ The release workflow checks the tag against the package version, reruns the
49
+ source checks, builds the wheel and source distribution, installs and tests
50
+ both artifacts, generates attestations, and then publishes through the `pypi`
51
+ environment.
52
+
53
+ After the workflow finishes, verify the project page and install it without a
54
+ source checkout:
55
+
56
+ ```sh
57
+ uv run --no-project --with trmnl-terminus \
58
+ python -c "import trmnl_terminus; print('install ok')"
59
+ ```