plexus-python 0.8.0__tar.gz → 0.9.1__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.
Files changed (49) hide show
  1. {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/workflows/publish.yml +17 -3
  2. {plexus_python-0.8.0 → plexus_python-0.9.1}/AGENTS.md +15 -10
  3. {plexus_python-0.8.0 → plexus_python-0.9.1}/API.md +41 -149
  4. {plexus_python-0.8.0 → plexus_python-0.9.1}/CHANGELOG.md +53 -0
  5. {plexus_python-0.8.0 → plexus_python-0.9.1}/PKG-INFO +10 -21
  6. {plexus_python-0.8.0 → plexus_python-0.9.1}/README.md +7 -18
  7. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mac_metrics.py +2 -0
  8. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/thermal_camera.py +1 -2
  9. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/uv.lock +4 -4
  10. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/__init__.py +3 -3
  11. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/buffer.py +17 -16
  12. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cameras/thermal.py +6 -6
  13. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cli.py +7 -9
  14. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/client.py +199 -142
  15. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/config.py +3 -4
  16. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/ws.py +36 -29
  17. {plexus_python-0.8.0 → plexus_python-0.9.1}/pyproject.toml +2 -2
  18. {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/release.sh +13 -0
  19. {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/scan_buses.py +1 -0
  20. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_basic.py +1 -0
  21. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_buffer.py +0 -2
  22. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_config.py +1 -2
  23. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_retry.py +102 -0
  24. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_video.py +0 -1
  25. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_ws.py +8 -8
  26. {plexus_python-0.8.0 → plexus_python-0.9.1}/uv.lock +92 -96
  27. {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  28. {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/workflows/ci.yml +0 -0
  31. {plexus_python-0.8.0 → plexus_python-0.9.1}/.gitignore +0 -0
  32. {plexus_python-0.8.0 → plexus_python-0.9.1}/CODE_OF_CONDUCT.md +0 -0
  33. {plexus_python-0.8.0 → plexus_python-0.9.1}/CONTRIBUTING.md +0 -0
  34. {plexus_python-0.8.0 → plexus_python-0.9.1}/LICENSE +0 -0
  35. {plexus_python-0.8.0 → plexus_python-0.9.1}/SECURITY.md +0 -0
  36. {plexus_python-0.8.0 → plexus_python-0.9.1}/TODO.md +0 -0
  37. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/.python-version +0 -0
  38. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/README.md +0 -0
  39. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/basic.py +0 -0
  40. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/can.py +0 -0
  41. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/i2c_bme280.py +0 -0
  42. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mavlink.py +0 -0
  43. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mqtt.py +0 -0
  44. {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/pyproject.toml +0 -0
  45. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/_log.py +0 -0
  46. {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cameras/__init__.py +2 -2
  47. {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/plexus.service +0 -0
  48. {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/setup.sh +0 -0
  49. {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_thermal.py +0 -0
@@ -30,10 +30,24 @@ jobs:
30
30
  match = re.search(r'^version\s*=\s*\"(.+?)\"', f.read(), re.MULTILINE)
31
31
  print(match.group(1))
32
32
  ")
33
- echo "Git tag version: $TAG"
34
- echo "Code version: $CODE_VERSION"
33
+ INIT_VERSION=$(python -c "
34
+ import re
35
+ with open('plexus/__init__.py') as f:
36
+ match = re.search(r'^__version__\s*=\s*\"(.+?)\"', f.read(), re.MULTILINE)
37
+ print(match.group(1))
38
+ ")
39
+ echo "Git tag version: $TAG"
40
+ echo "pyproject.toml: $CODE_VERSION"
41
+ echo "plexus/__init__.py: $INIT_VERSION"
35
42
  if [ "$TAG" != "$CODE_VERSION" ]; then
36
- echo "::error::Tag v$TAG does not match code version $CODE_VERSION"
43
+ echo "::error::Tag v$TAG does not match pyproject.toml version $CODE_VERSION"
44
+ exit 1
45
+ fi
46
+ # 0.9.0 shipped to PyPI with __version__ still reading 0.8.0: the CI
47
+ # job that checks this does not gate publish, and this job only read
48
+ # pyproject.toml. Check it here, where it can actually stop an upload.
49
+ if [ "$TAG" != "$INIT_VERSION" ]; then
50
+ echo "::error::Tag v$TAG does not match plexus/__init__.py version $INIT_VERSION"
37
51
  exit 1
38
52
  fi
39
53
 
@@ -4,22 +4,27 @@ Machine-readable interface for AI assistants and automation scripts.
4
4
 
5
5
  ## Environment Variables
6
6
 
7
- | Variable | Description | Default |
8
- | ----------------------- | ------------------------------------- | -------------------------------- |
9
- | `PLEXUS_API_KEY` | API key for authentication (required) | none |
10
- | `PLEXUS_GATEWAY_URL` | Gateway HTTP ingest URL | `https://gateway.plexus.company` |
11
- | `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://gateway.plexus.company` |
7
+ | Variable | Description | Default |
8
+ | ----------------------- | ----------------------------------------------- | -------------------------------- |
9
+ | `PLEXUS_API_KEY` | API key for authentication (required) | none |
10
+ | `PLEXUS_GATEWAY_URL` | Gateway HTTP ingest URL | `https://gateway.plexus.company` |
11
+ | `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://gateway.plexus.company` |
12
+ | `PLEXUS_ENDPOINT` | Product app URL (runs, frame uploads, CLI auth) | `https://app.plexus.company` |
13
+ | `PLEXUS_QUIET` | Set `1`/`true`/`yes` to silence `[plexus]` stderr status lines | unset (status lines on) |
12
14
 
13
15
  ## CLI Commands
14
16
 
15
17
  ```bash
16
- plexus start # Stream with PLEXUS_API_KEY env var
17
- plexus start --key plx_xxxxx # Pass key inline
18
- plexus start --device-id my-device # Set device identifier
19
- plexus reset # Clear config
18
+ plexus init # Authorize this machine, save an API key locally (alias: login)
19
+ plexus init --name my-key # Label for the issued key (default: cli-<hostname>)
20
+ plexus init --force # Overwrite an existing local key
21
+ plexus logout # Forget the local API key
22
+ plexus whoami # Show the local credential summary
20
23
  ```
21
24
 
22
- An API key is required — set `PLEXUS_API_KEY` or pass `--key`. There is no interactive signup; get a key at app.plexus.company/devices.
25
+ `plexus init` opens a browser to `app.plexus.company/auth/cli`, waits for the callback, and
26
+ persists the issued key to `~/.plexus/config.json`. Alternatively, set `PLEXUS_API_KEY` (or pass
27
+ `api_key=` to `Plexus()`) instead of running `init`; get a key at app.plexus.company/api.
23
28
 
24
29
  ## Exit Codes
25
30
 
@@ -55,8 +55,8 @@ Plexus uses API keys for all authentication:
55
55
 
56
56
  **Option A: CLI setup (recommended for devices)**
57
57
 
58
- 1. Run `plexus start` on your device
59
- 2. Sign up or sign in directly in the terminal
58
+ 1. Run `plexus init` on your device
59
+ 2. Authorize the machine in the browser tab it opens
60
60
  3. API key is saved to `~/.plexus/config.json`
61
61
 
62
62
  **Option B: Manual creation**
@@ -77,7 +77,7 @@ x-api-key: plx_xxxxx
77
77
 
78
78
  ### Send Data
79
79
 
80
- **POST** `/api/ingest`
80
+ **POST** `https://gateway.plexus.company/ingest`
81
81
 
82
82
  ```json
83
83
  {
@@ -87,8 +87,7 @@ x-api-key: plx_xxxxx
87
87
  "value": 72.5,
88
88
  "timestamp": 1699900000.123,
89
89
  "source_id": "sensor-001",
90
- "tags": { "location": "lab" },
91
- "session_id": "test-001"
90
+ "tags": { "location": "lab" }
92
91
  }
93
92
  ]
94
93
  }
@@ -101,7 +100,6 @@ x-api-key: plx_xxxxx
101
100
  | `timestamp` | float | No | Unix timestamp in seconds (or ms if ≥ 1e12). Omit to use device time. Over WebSocket, the Python SDK applies a server-synced clock correction when omitted — see [Clock correction](#clock-correction). |
102
101
  | `source_id` | string | Yes | Your source identifier |
103
102
  | `tags` | object | No | Key-value labels |
104
- | `session_id` | string | No | Group data into sessions |
105
103
 
106
104
  ### Supported Value Types
107
105
 
@@ -115,29 +113,7 @@ x-api-key: plx_xxxxx
115
113
 
116
114
  ### Sessions
117
115
 
118
- Group related data for analysis and playback.
119
-
120
- **Create session:**
121
-
122
- ```json
123
- POST /api/sessions
124
- {
125
- "session_id": "test-001",
126
- "name": "Motor Test Run",
127
- "source_id": "sensor-001",
128
- "status": "active"
129
- }
130
- ```
131
-
132
- **End session:**
133
-
134
- ```json
135
- PATCH /api/sessions/{session_id}
136
- {
137
- "status": "completed",
138
- "ended_at": "2024-01-15T10:30:00Z"
139
- }
140
- ```
116
+ > **Removed / not built.** There is no sessions or runs REST API (`POST /api/sessions`, `POST /api/runs`) — no such route exists in the gateway or platform, and the SDK's former `run()` context has been removed. To group a slice of data, use plain `tags` on each point.
141
117
 
142
118
  ## WebSocket API
143
119
 
@@ -146,13 +122,13 @@ For real-time UI-controlled streaming, devices connect via WebSocket.
146
122
  ### Connection Flow
147
123
 
148
124
  1. Device connects to the gateway
149
- 2. Device authenticates with API key
150
- 3. Device reports available sensors
151
- 4. Dashboard controls streaming via messages
125
+ 2. Device authenticates with API key (and advertises any registered commands)
126
+ 3. Device streams `telemetry` frames
127
+ 4. Dashboard/API invokes registered commands via `typed_command`
152
128
 
153
129
  ### Device Authentication
154
130
 
155
- Devices authenticate using an API key. The `source_id` in the request is the device's _desired_ name; the server may return a different, auto-suffixed name in the `authenticated` frame if the desired name is already claimed by another device (see [Device identity](../README.md#device-identity) in the README).
131
+ Devices authenticate using an API key. The gateway echoes the declared `source_id` back unchanged there is no auto-suffixing, so pick a unique `source_id` per device.
156
132
 
157
133
  ```json
158
134
  // Device → Server
@@ -160,9 +136,8 @@ Devices authenticate using an API key. The `source_id` in the request is the dev
160
136
  "type": "device_auth",
161
137
  "api_key": "plx_xxxxx",
162
138
  "source_id": "drone-01",
163
- "install_id": "c9f2e0b46f4a4f6a8c3e1d5b0a2e7f91",
164
139
  "platform": "python-sdk",
165
- "agent_version": "0.3.1"
140
+ "agent_version": "0.8.0"
166
141
  }
167
142
 
168
143
  // Server → Device
@@ -171,53 +146,34 @@ Devices authenticate using an API key. The `source_id` in the request is the dev
171
146
  "source_id": "drone-01",
172
147
  "server_time_ms": 1746100800000
173
148
  }
174
-
175
- // Server → Device (collision case)
176
- {
177
- "type": "authenticated",
178
- "source_id": "drone-01_2",
179
- "server_time_ms": 1746100800000
180
- }
181
149
  ```
182
150
 
183
- The SDK **adopts** whatever `source_id` the server returns and uses it for all subsequent frames, heartbeats, and reconnects. It also persists the assigned name locally so reconnects go straight to the claimed slot.
184
-
185
151
  `server_time_ms` is the gateway's current Unix time in milliseconds. The Python SDK uses it to compute a clock offset (`server_time - device_time`) that is applied to every SDK-generated timestamp for the lifetime of the connection. This corrects for devices that boot without NTP or have an unreliable RTC — a common condition on embedded Linux. See [Clock correction](#clock-correction) for details and limitations.
186
152
 
187
- `install_id` is a stable per-installation UUID, generated on the device's first run and saved to `~/.plexus/config.json`. It lets the server distinguish a rebooting device from a new device trying to claim an existing name. Legacy SDKs that omit `install_id` continue to work as before (the server passes the declared `source_id` through unchanged).
153
+ > **Removed:** `install_id` and server-side `source_id` auto-suffixing were removed in 0.7.1. The device_auth frame no longer carries an `install_id`, and the client does not adopt or persist a server-assigned name.
188
154
 
189
- ### Message Types (Dashboard → Device)
155
+ ### Frame Types
190
156
 
191
- | Type | Description |
192
- | --------------- | ------------------------------------ |
193
- | `start_stream` | Start streaming sensor data |
194
- | `stop_stream` | Stop streaming |
195
- | `start_session` | Start recording to a session |
196
- | `stop_session` | Stop recording |
197
- | `configure` | Configure sensor (e.g., sample rate) |
198
- | `ping` | Keepalive request |
157
+ **Device Server**
199
158
 
200
- ### Message Types (Device → Dashboard)
159
+ | Type | Description |
160
+ | ---------------- | ----------------------------------------------- |
161
+ | `device_auth` | Authenticate on connect (see above) |
162
+ | `telemetry` | Sensor data points |
163
+ | `heartbeat` | Liveness ping (every 30s) |
164
+ | `command_result` | `ack` / `result` / `error` for a typed command |
201
165
 
202
- | Type | Description |
203
- | ----------------- | ----------------------- |
204
- | `telemetry` | Sensor data points |
205
- | `session_started` | Confirm session started |
206
- | `session_stopped` | Confirm session stopped |
207
- | `pong` | Keepalive response |
166
+ **Server Device**
208
167
 
209
- ### Start Streaming
168
+ | Type | Description |
169
+ | --------------- | ----------------------------------------------------- |
170
+ | `authenticated` | Auth accepted; carries `server_time_ms` |
171
+ | `typed_command` | Invoke a command the device registered via `on_command` |
210
172
 
211
- ```json
212
- // Dashboard → Device
213
- {
214
- "type": "start_stream",
215
- "source_id": "my-device-001",
216
- "metrics": ["accel_x", "accel_y", "accel_z"],
217
- "interval_ms": 100
218
- }
173
+ ### Telemetry
219
174
 
220
- // Device → Dashboard (continuous)
175
+ ```json
176
+ // Device → Server (streamed continuously)
221
177
  {
222
178
  "type": "telemetry",
223
179
  "points": [
@@ -228,54 +184,20 @@ The SDK **adopts** whatever `source_id` the server returns and uses it for all s
228
184
  }
229
185
  ```
230
186
 
231
- ### Start Session (Recording)
187
+ ### Commands
232
188
 
233
- ```json
234
- // Dashboard → Device
235
- {
236
- "type": "start_session",
237
- "source_id": "my-device-001",
238
- "session_id": "session_1699900000_abc123",
239
- "session_name": "Motor Test",
240
- "metrics": [],
241
- "interval_ms": 100
242
- }
189
+ Dashboard/API actions reach the device as a single `typed_command` envelope; the device replies with `command_result` frames. Register handlers with `px.on_command(...)` before the first `send()`.
243
190
 
244
- // Device → Dashboard
245
- {
246
- "type": "session_started",
247
- "session_id": "session_1699900000_abc123",
248
- "session_name": "Motor Test"
249
- }
191
+ ```json
192
+ // Server → Device
193
+ { "type": "typed_command", "id": "cmd-1", "command": "reboot", "params": { "delay_s": 0 } }
250
194
 
251
- // Device streams telemetry with session_id tag
252
- {
253
- "type": "telemetry",
254
- "session_id": "session_1699900000_abc123",
255
- "points": [
256
- {
257
- "metric": "accel_x",
258
- "value": 0.12,
259
- "timestamp": 1699900000123,
260
- "tags": { "session_id": "session_1699900000_abc123" }
261
- }
262
- ]
263
- }
195
+ // Device Server (ack, then result or error)
196
+ { "type": "command_result", "id": "cmd-1", "command": "reboot", "event": "ack" }
197
+ { "type": "command_result", "id": "cmd-1", "command": "reboot", "event": "result", "result": { "ok": true } }
264
198
  ```
265
199
 
266
- ### Configure Sensor
267
-
268
- ```json
269
- // Dashboard → Device
270
- {
271
- "type": "configure",
272
- "source_id": "my-device-001",
273
- "sensor": "MPU6050",
274
- "config": {
275
- "sample_rate": 50
276
- }
277
- }
278
- ```
200
+ > **Removed / not built.** There are no raw `start_stream`, `stop_stream`, `start_session`, `stop_session`, `configure`, `session_started`, or `session_stopped` frames — the earlier agent-style streaming/recording protocol was removed. Dashboard-driven control now flows through the `typed_command` envelope above.
279
201
 
280
202
  ## Code Examples
281
203
 
@@ -432,36 +354,7 @@ while True:
432
354
 
433
355
  ## Python SDK with Sensor Drivers
434
356
 
435
- For Raspberry Pi and other Linux devices, the Python SDK includes sensor drivers:
436
-
437
- ```bash
438
- pip install plexus-python[sensors]
439
- plexus start
440
- ```
441
-
442
- ### Supported Sensors
443
-
444
- | Sensor | Type | Metrics | I2C Address |
445
- | ------- | ----------- | ------------------------------------------------------------- | ----------- |
446
- | MPU6050 | 6-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68, 0x69 |
447
- | MPU9250 | 9-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68 |
448
- | BME280 | Environment | `temperature`, `humidity`, `pressure` | 0x76, 0x77 |
449
-
450
- ### Custom Sensors
451
-
452
- ```python
453
- from plexus.sensors import BaseSensor, SensorReading
454
-
455
- class MySensor(BaseSensor):
456
- name = "MySensor"
457
- metrics = ["voltage", "current"]
458
-
459
- def read(self):
460
- return [
461
- SensorReading("voltage", read_adc(0) * 3.3),
462
- SensorReading("current", read_adc(1) * 0.1),
463
- ]
464
- ```
357
+ > **Removed / not built.** There is no `plexus-python[sensors]` extra and no `plexus.sensors` module (`BaseSensor` / `SensorReading` do not exist). The only optional extras are `[video]` and `[dev]`. This SDK stays out of your decode path — read your sensor with whatever library you already use and pass values to `px.send()` (see [Bring Your Own Protocol](#bring-your-own-protocol) above).
465
358
 
466
359
  ## Errors
467
360
 
@@ -503,14 +396,13 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
503
396
  **Known limitations:**
504
397
 
505
398
  - The clock offset refreshes only on WebSocket reconnect. A device with a drifting RTC that stays connected for many days will accumulate uncorrected drift between reconnects proportional to the drift rate.
506
- - HTTP transport (`transport="http"`) does not receive clock sync — timestamps default to the device clock uncorrected.
507
- - `send_batch()` takes one shared `timestamp` for the whole batch, not per-point. For per-point timestamps, call `send()` in a loop.
399
+ - The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the device clock uncorrected.
400
+ - `send_batch()` takes one shared `timestamp` by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
508
401
 
509
402
  ## Best Practices
510
403
 
511
404
  - **Batch points** - Send up to 100 points per request for HTTP
512
405
  - **Omit timestamp when unsure** - The Python SDK applies server-synced clock correction when `timestamp` is omitted over WebSocket; only pass an explicit timestamp when you have a reliable wall-clock source
513
406
  - **Consistent source_id** - Use the same ID for each physical device/source
514
- - **Use tags** - Label data for filtering (e.g., `{"location": "lab"}`)
515
- - **Use sessions** - Group related data for easier analysis
516
- - **Prefer WebSocket** - For real-time UI-controlled devices, use `plexus start`
407
+ - **Use tags** - Label data for filtering and grouping (e.g., `{"location": "lab"}`)
408
+ - **Prefer WebSocket** - For real-time UI-controlled devices, the SDK connects over WebSocket by default
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.9.1] - 2026-08-27 - Version sync
6
+
7
+ ### Fixed
8
+
9
+ - `plexus.__version__` reported `0.8.0` in the 0.9.0 release. The release script
10
+ and the publish workflow both validated `pyproject.toml` only; the CI job that
11
+ compares the two version sites does not gate publishing, so it failed after the
12
+ upload had already gone out. Both gates now check `plexus/__init__.py` too.
13
+ 0.9.0 is functionally identical to this release — only the reported version
14
+ string was wrong.
15
+
16
+ ## [0.9.0] - 2026-08-27 - Flexible values and the real slug rule
17
+
18
+ Two fixes for data that looked like it was sent but never arrived, plus the
19
+ removal of a dead API.
20
+
21
+ ### Fixed
22
+
23
+ - **`send()` now infers a point's class from its value type** instead of always
24
+ tagging `class="metric"`. Numbers stay metrics; strings, bools, dicts and
25
+ lists become events. The gateway rejects a non-numeric value on
26
+ `class="metric"` and drops the *whole frame* — so `px.send("state", "RUNNING")`
27
+ silently took every other point in that batch down with it. Non-numeric values
28
+ have been advertised since 0.2.0; this is what makes them work.
29
+ An explicit `data_class="metric"` with a non-numeric value now raises
30
+ `PlexusError` locally rather than failing on the wire.
31
+
32
+ - **`source_id` validation matched an older, stricter rule than the server.**
33
+ The SDK enforced `^[a-z0-9][a-z0-9_-]{1,62}$` — no dots, minimum two
34
+ characters — while the gateway accepts `^[a-z0-9][a-z0-9._-]*$` up to 256
35
+ characters. Names production would have accepted (`rack.01`, `a`) raised
36
+ `ValueError` in the constructor before a single byte left the host, which
37
+ reads as an auth problem and is not one. The SDK now mirrors the wire rule.
38
+ Uuid-shaped ids are still rejected: the app resolves those as internal ids,
39
+ so such a source would be unreachable.
40
+
41
+ - Buffer drains are chunked, so a backlog over the gateway's 10k-points-per-batch
42
+ ceiling no longer wedges every subsequent send. The WebSocket auth wait is paid
43
+ once, on the first send, instead of stalling each call.
44
+
45
+ ### Removed
46
+
47
+ - `run()` context manager and its `POST /api/runs` plumbing. The `/api/runs` route never
48
+ existed on the platform (both notifications failed silently on every call), the ingest
49
+ loader drops `run_id` from points, and nothing was ever stored — so removal loses no
50
+ data. To group a slice of data, use `tags` on each point.
51
+
52
+ ### Security
53
+
54
+ - Bumped pillow to 12.3.0 and idna to 3.19 for published advisories.
55
+
3
56
  ## [0.8.0] - 2026-07-02 - Command concurrency control
4
57
 
5
58
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: plexus-python
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Thin Python SDK for Plexus — send telemetry in one line
5
5
  Project-URL: Homepage, https://plexus.company
6
6
  Project-URL: Documentation, https://docs.plexus.company
@@ -27,7 +27,7 @@ Requires-Dist: numpy>=1.24; extra == 'dev'
27
27
  Requires-Dist: opencv-python-headless>=4.8; extra == 'dev'
28
28
  Requires-Dist: pytest-cov; extra == 'dev'
29
29
  Requires-Dist: pytest>=9.0.3; extra == 'dev'
30
- Requires-Dist: ruff; extra == 'dev'
30
+ Requires-Dist: ruff==0.15.12; extra == 'dev'
31
31
  Requires-Dist: websockets>=12; extra == 'dev'
32
32
  Provides-Extra: video
33
33
  Requires-Dist: numpy>=1.24; extra == 'video'
@@ -68,7 +68,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
68
68
 
69
69
  The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `setup.sh` refuses to run without `--name` (or without a TTY to prompt for one) — this is deliberate, because the previous `hostname` fallback silently merged telemetry from cloned SD-card images that all booted as `raspberrypi`.
70
70
 
71
- **If two devices end up requesting the same name**, the gateway auto-suffixes: the first connection gets `drone-01`, the second gets `drone-01_2`, the third `drone-01_3`, and so on. The SDK logs the rename at INFO and persists the assigned name to `~/.plexus/config.json` so the device keeps its identity across reboots. Under the hood, a per-installation UUID (`install_id`, lazily generated on first run) is what lets the gateway tell "same device reconnecting" from "different device claiming the same name."
71
+ **Names are not auto-deduplicated.** The gateway echoes back whatever `source_id` you declare, unchanged pick a unique name per device (that's what `--name` and `source_id=...` are for). Two devices that declare the same name write into the same source.
72
72
 
73
73
  In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
74
74
 
@@ -111,7 +111,7 @@ px.send_batch([
111
111
  ])
112
112
  ```
113
113
 
114
- `points` is a list of `(metric, value)` tuples. All points share the same timestamp (now, unless you pass `timestamp=t`). For independent timestamps per point, call `send()` in a loop instead.
114
+ `points` is a list of `(metric, value)` tuples, or `(metric, value, timestamp)` 3-tuples when you need a per-point timestamp. Points without their own timestamp share the batch timestamp (now, unless you pass `timestamp=t`).
115
115
 
116
116
  ### `event(name, data)` — record a discrete occurrence
117
117
 
@@ -125,16 +125,6 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
125
125
 
126
126
  The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
127
127
 
128
- ### `run(run_id)` — group data into a named recording
129
-
130
- ```python
131
- with px.run("thermal-cycle-001"):
132
- while running:
133
- px.send("temperature", read_temp())
134
- ```
135
-
136
- All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
137
-
138
128
  ## Video streaming
139
129
 
140
130
  Two methods depending on whether you control the capture loop or just have a URL.
@@ -228,8 +218,8 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
228
218
 
229
219
  **Known limits:**
230
220
  - Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
231
- - HTTP-only transport (`transport="http"`) does not receive clock sync — timestamps default to the uncorrected device clock.
232
- - `send_batch()` shares one timestamp across the whole batch. For per-point timestamps, call `send()` in a loop.
221
+ - The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the uncorrected device clock.
222
+ - `send_batch()` shares one timestamp across the batch by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
233
223
 
234
224
  ## Transport
235
225
 
@@ -241,13 +231,12 @@ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway
241
231
  If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
242
232
 
243
233
  ```python
244
- # default — ws with http fallback
234
+ # ws with transparent http fallback — this is the only mode
245
235
  px = Plexus()
246
-
247
- # force http (legacy)
248
- px = Plexus(transport="http")
249
236
  ```
250
237
 
238
+ There is no transport selector: the SDK always prefers the WebSocket and falls back to `POST /ingest` on its own when the socket is unavailable.
239
+
251
240
  ### Handling commands
252
241
 
253
242
  Register a handler before the first `send()` so the command is advertised in the auth frame:
@@ -31,7 +31,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
31
31
 
32
32
  The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `setup.sh` refuses to run without `--name` (or without a TTY to prompt for one) — this is deliberate, because the previous `hostname` fallback silently merged telemetry from cloned SD-card images that all booted as `raspberrypi`.
33
33
 
34
- **If two devices end up requesting the same name**, the gateway auto-suffixes: the first connection gets `drone-01`, the second gets `drone-01_2`, the third `drone-01_3`, and so on. The SDK logs the rename at INFO and persists the assigned name to `~/.plexus/config.json` so the device keeps its identity across reboots. Under the hood, a per-installation UUID (`install_id`, lazily generated on first run) is what lets the gateway tell "same device reconnecting" from "different device claiming the same name."
34
+ **Names are not auto-deduplicated.** The gateway echoes back whatever `source_id` you declare, unchanged pick a unique name per device (that's what `--name` and `source_id=...` are for). Two devices that declare the same name write into the same source.
35
35
 
36
36
  In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
37
37
 
@@ -74,7 +74,7 @@ px.send_batch([
74
74
  ])
75
75
  ```
76
76
 
77
- `points` is a list of `(metric, value)` tuples. All points share the same timestamp (now, unless you pass `timestamp=t`). For independent timestamps per point, call `send()` in a loop instead.
77
+ `points` is a list of `(metric, value)` tuples, or `(metric, value, timestamp)` 3-tuples when you need a per-point timestamp. Points without their own timestamp share the batch timestamp (now, unless you pass `timestamp=t`).
78
78
 
79
79
  ### `event(name, data)` — record a discrete occurrence
80
80
 
@@ -88,16 +88,6 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
88
88
 
89
89
  The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
90
90
 
91
- ### `run(run_id)` — group data into a named recording
92
-
93
- ```python
94
- with px.run("thermal-cycle-001"):
95
- while running:
96
- px.send("temperature", read_temp())
97
- ```
98
-
99
- All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
100
-
101
91
  ## Video streaming
102
92
 
103
93
  Two methods depending on whether you control the capture loop or just have a URL.
@@ -191,8 +181,8 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
191
181
 
192
182
  **Known limits:**
193
183
  - Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
194
- - HTTP-only transport (`transport="http"`) does not receive clock sync — timestamps default to the uncorrected device clock.
195
- - `send_batch()` shares one timestamp across the whole batch. For per-point timestamps, call `send()` in a loop.
184
+ - The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the uncorrected device clock.
185
+ - `send_batch()` shares one timestamp across the batch by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
196
186
 
197
187
  ## Transport
198
188
 
@@ -204,13 +194,12 @@ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway
204
194
  If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
205
195
 
206
196
  ```python
207
- # default — ws with http fallback
197
+ # ws with transparent http fallback — this is the only mode
208
198
  px = Plexus()
209
-
210
- # force http (legacy)
211
- px = Plexus(transport="http")
212
199
  ```
213
200
 
201
+ There is no transport selector: the SDK always prefers the WebSocket and falls back to `POST /ingest` on its own when the socket is unavailable.
202
+
214
203
  ### Handling commands
215
204
 
216
205
  Register a handler before the first `send()` so the command is advertised in the auth frame:
@@ -11,7 +11,9 @@ Run:
11
11
 
12
12
  import argparse
13
13
  import time
14
+
14
15
  import psutil
16
+
15
17
  from plexus import Plexus
16
18
 
17
19
  parser = argparse.ArgumentParser(description="Stream Mac system metrics to Plexus.")
@@ -53,8 +53,7 @@ def main() -> None:
53
53
  print(f"Error: {e}", file=sys.stderr)
54
54
  sys.exit(1)
55
55
 
56
- px = Plexus(transport="ws")
57
- px.wait_connected()
56
+ px = Plexus()
58
57
 
59
58
  interval = 1.0 / FPS
60
59
  frame_count = 0
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.12"
4
4
 
5
5
  [[package]]
@@ -389,11 +389,11 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "idna"
392
- version = "3.13"
392
+ version = "3.19"
393
393
  source = { registry = "https://pypi.org/simple" }
394
- sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
394
+ sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
395
395
  wheels = [
396
- { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
396
+ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
397
397
  ]
398
398
 
399
399
  [[package]]
@@ -7,8 +7,8 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
7
7
  px.send("temperature", 72.5)
8
8
  """
9
9
 
10
- from plexus.client import Plexus, PlexusError, AuthenticationError, read_mjpeg_frames
10
+ from plexus.client import AuthenticationError, Plexus, PlexusError, read_mjpeg_frames
11
11
  from plexus.config import RetryConfig
12
12
 
13
- __version__ = "0.8.0"
14
- __all__ = ["Plexus", "PlexusError", "AuthenticationError", "RetryConfig", "read_mjpeg_frames"]
13
+ __version__ = "0.9.1"
14
+ __all__ = ["AuthenticationError", "Plexus", "PlexusError", "RetryConfig", "read_mjpeg_frames"]