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.
- trmnl_terminus-0.1.0/.gitignore +15 -0
- trmnl_terminus-0.1.0/LICENSE +21 -0
- trmnl_terminus-0.1.0/PKG-INFO +182 -0
- trmnl_terminus-0.1.0/README.md +161 -0
- trmnl_terminus-0.1.0/docs/real-server-smoke.md +135 -0
- trmnl_terminus-0.1.0/docs/releasing.md +59 -0
- trmnl_terminus-0.1.0/docs/specs/terminus-sdk.md +659 -0
- trmnl_terminus-0.1.0/examples/list_resources.py +58 -0
- trmnl_terminus-0.1.0/examples/render_screen.py +84 -0
- trmnl_terminus-0.1.0/pyproject.toml +64 -0
- trmnl_terminus-0.1.0/scripts/run_device_smoke.py +228 -0
- trmnl_terminus-0.1.0/scripts/run_real_smoke.py +204 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/__init__.py +55 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/client.py +270 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/errors.py +112 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/models.py +199 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/py.typed +0 -0
- trmnl_terminus-0.1.0/src/trmnl_terminus/resources.py +279 -0
- trmnl_terminus-0.1.0/tests/package_smoke.py +50 -0
- trmnl_terminus-0.1.0/tests/test_client.py +244 -0
- trmnl_terminus-0.1.0/tests/test_device_smoke_script.py +134 -0
- trmnl_terminus-0.1.0/tests/test_errors.py +167 -0
- trmnl_terminus-0.1.0/tests/test_examples.py +55 -0
- trmnl_terminus-0.1.0/tests/test_models.py +273 -0
- trmnl_terminus-0.1.0/tests/test_resources.py +404 -0
- trmnl_terminus-0.1.0/tests/test_smoke_script.py +88 -0
- trmnl_terminus-0.1.0/uv.lock +486 -0
|
@@ -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
|
+
```
|