plexus-python 0.7.1__tar.gz → 0.9.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.
Files changed (52) hide show
  1. {plexus_python-0.7.1 → plexus_python-0.9.0}/AGENTS.md +15 -10
  2. {plexus_python-0.7.1 → plexus_python-0.9.0}/API.md +56 -163
  3. {plexus_python-0.7.1 → plexus_python-0.9.0}/CHANGELOG.md +55 -2
  4. {plexus_python-0.7.1 → plexus_python-0.9.0}/PKG-INFO +15 -26
  5. {plexus_python-0.7.1 → plexus_python-0.9.0}/README.md +9 -20
  6. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mac_metrics.py +2 -0
  7. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/thermal_camera.py +1 -2
  8. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/uv.lock +4 -4
  9. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/__init__.py +3 -3
  10. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/buffer.py +17 -16
  11. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cameras/thermal.py +6 -6
  12. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cli.py +7 -9
  13. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/client.py +210 -143
  14. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/config.py +5 -6
  15. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/ws.py +97 -37
  16. {plexus_python-0.7.1 → plexus_python-0.9.0}/pyproject.toml +5 -5
  17. {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/scan_buses.py +1 -0
  18. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_basic.py +1 -0
  19. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_buffer.py +0 -2
  20. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_config.py +1 -2
  21. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_retry.py +102 -0
  22. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_video.py +0 -1
  23. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_ws.py +137 -8
  24. {plexus_python-0.7.1 → plexus_python-0.9.0}/uv.lock +92 -96
  25. plexus_python-0.7.1/skills/plexus/SKILL.md +0 -189
  26. plexus_python-0.7.1/skills/plexus/references/api.md +0 -331
  27. plexus_python-0.7.1/skills/plexus/references/sdk.md +0 -227
  28. {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  29. {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  30. {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  31. {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/workflows/ci.yml +0 -0
  32. {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/workflows/publish.yml +0 -0
  33. {plexus_python-0.7.1 → plexus_python-0.9.0}/.gitignore +0 -0
  34. {plexus_python-0.7.1 → plexus_python-0.9.0}/CODE_OF_CONDUCT.md +0 -0
  35. {plexus_python-0.7.1 → plexus_python-0.9.0}/CONTRIBUTING.md +0 -0
  36. {plexus_python-0.7.1 → plexus_python-0.9.0}/LICENSE +0 -0
  37. {plexus_python-0.7.1 → plexus_python-0.9.0}/SECURITY.md +0 -0
  38. {plexus_python-0.7.1 → plexus_python-0.9.0}/TODO.md +0 -0
  39. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/.python-version +0 -0
  40. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/README.md +0 -0
  41. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/basic.py +0 -0
  42. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/can.py +0 -0
  43. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/i2c_bme280.py +0 -0
  44. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mavlink.py +0 -0
  45. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mqtt.py +0 -0
  46. {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/pyproject.toml +0 -0
  47. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/_log.py +0 -0
  48. {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cameras/__init__.py +2 -2
  49. {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/plexus.service +0 -0
  50. {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/release.sh +0 -0
  51. {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/setup.sh +0 -0
  52. {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_thermal.py +0 -0
@@ -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://plexus-gateway.fly.dev` |
11
- | `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://plexus-gateway.fly.dev` |
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
 
@@ -30,7 +30,7 @@ Then control streaming, recording, and configuration from [app.plexus.company/de
30
30
  Send data directly via HTTP:
31
31
 
32
32
  ```bash
33
- curl -X POST https://plexus-gateway.fly.dev/ingest \
33
+ curl -X POST https://gateway.plexus.company/ingest \
34
34
  -H "x-api-key: YOUR_API_KEY" \
35
35
  -H "Content-Type: application/json" \
36
36
  -d '{
@@ -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,21 +87,19 @@ 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
  }
95
94
  ```
96
95
 
97
- | Field | Type | Required | Description |
98
- | ------------ | ------ | -------- | ---------------------------------------------- |
99
- | `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
100
- | `value` | any | Yes | See supported value types below |
96
+ | Field | Type | Required | Description |
97
+ | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98
+ | `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
99
+ | `value` | any | Yes | See supported value types below |
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
- | `source_id` | string | Yes | Your source identifier |
103
- | `tags` | object | No | Key-value labels |
104
- | `session_id` | string | No | Group data into sessions |
101
+ | `source_id` | string | Yes | Your source identifier |
102
+ | `tags` | object | No | Key-value labels |
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
 
@@ -286,7 +208,7 @@ import requests
286
208
  import time
287
209
 
288
210
  requests.post(
289
- "https://plexus-gateway.fly.dev/ingest",
211
+ "https://gateway.plexus.company/ingest",
290
212
  headers={"x-api-key": "plx_xxxxx"},
291
213
  json={
292
214
  "points": [{
@@ -302,7 +224,7 @@ requests.post(
302
224
  ### JavaScript
303
225
 
304
226
  ```javascript
305
- await fetch("https://plexus-gateway.fly.dev/ingest", {
227
+ await fetch("https://gateway.plexus.company/ingest", {
306
228
  method: "POST",
307
229
  headers: {
308
230
  "x-api-key": "plx_xxxxx",
@@ -344,7 +266,7 @@ func main() {
344
266
  }
345
267
 
346
268
  body, _ := json.Marshal(points)
347
- req, _ := http.NewRequest("POST", "https://plexus-gateway.fly.dev/ingest", bytes.NewBuffer(body))
269
+ req, _ := http.NewRequest("POST", "https://gateway.plexus.company/ingest", bytes.NewBuffer(body))
348
270
  req.Header.Set("x-api-key", "plx_xxxxx")
349
271
  req.Header.Set("Content-Type", "application/json")
350
272
 
@@ -363,7 +285,7 @@ func main() {
363
285
  // field entirely if you cannot guarantee NTP sync at send time.
364
286
  void sendToPlexus(const char* metric, float value) {
365
287
  HTTPClient http;
366
- http.begin("https://plexus-gateway.fly.dev/ingest");
288
+ http.begin("https://gateway.plexus.company/ingest");
367
289
  http.addHeader("Content-Type", "application/json");
368
290
  http.addHeader("x-api-key", "plx_xxxxx");
369
291
 
@@ -386,7 +308,7 @@ void sendToPlexus(const char* metric, float value) {
386
308
  API_KEY="plx_xxxxx"
387
309
  SOURCE_ID="sensor-001"
388
310
 
389
- curl -X POST https://plexus-gateway.fly.dev/ingest \
311
+ curl -X POST https://gateway.plexus.company/ingest \
390
312
  -H "x-api-key: $API_KEY" \
391
313
  -H "Content-Type: application/json" \
392
314
  -d "{
@@ -430,40 +352,9 @@ while True:
430
352
  px.send("attitude.pitch", msg.pitch)
431
353
  ```
432
354
 
433
- See [docs.plexus.dev/recipes](https://docs.plexus.dev/recipes) for more.
434
-
435
355
  ## Python SDK with Sensor Drivers
436
356
 
437
- For Raspberry Pi and other Linux devices, the Python SDK includes sensor drivers:
438
-
439
- ```bash
440
- pip install plexus-python[sensors]
441
- plexus start
442
- ```
443
-
444
- ### Supported Sensors
445
-
446
- | Sensor | Type | Metrics | I2C Address |
447
- | ------- | ----------- | ------------------------------------------------------------- | ----------- |
448
- | MPU6050 | 6-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68, 0x69 |
449
- | MPU9250 | 9-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68 |
450
- | BME280 | Environment | `temperature`, `humidity`, `pressure` | 0x76, 0x77 |
451
-
452
- ### Custom Sensors
453
-
454
- ```python
455
- from plexus.sensors import BaseSensor, SensorReading
456
-
457
- class MySensor(BaseSensor):
458
- name = "MySensor"
459
- metrics = ["voltage", "current"]
460
-
461
- def read(self):
462
- return [
463
- SensorReading("voltage", read_adc(0) * 3.3),
464
- SensorReading("current", read_adc(1) * 0.1),
465
- ]
466
- ```
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).
467
358
 
468
359
  ## Errors
469
360
 
@@ -492,24 +383,26 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
492
383
  ```
493
384
 
494
385
  **When to pass an explicit timestamp:**
386
+
495
387
  - You have a reliable wall-clock source (GPS, trusted hardware RTC, host NTP)
496
388
  - You are replaying or backfilling historical data
497
389
  - Your sensor provides its own wall-clock timestamp
498
390
 
499
391
  **When to omit timestamp:**
392
+
500
393
  - The device may have booted without NTP (Raspberry Pi, Jetson, field robots without network on first boot)
501
394
  - You have no reliable external time source
502
395
 
503
396
  **Known limitations:**
397
+
504
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.
505
- - HTTP transport (`transport="http"`) does not receive clock sync — timestamps default to the device clock uncorrected.
506
- - `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.
507
401
 
508
402
  ## Best Practices
509
403
 
510
404
  - **Batch points** - Send up to 100 points per request for HTTP
511
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
512
406
  - **Consistent source_id** - Use the same ID for each physical device/source
513
- - **Use tags** - Label data for filtering (e.g., `{"location": "lab"}`)
514
- - **Use sessions** - Group related data for easier analysis
515
- - **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.0] - 2026-08-27 - Flexible values and the real slug rule
6
+
7
+ Two fixes for data that looked like it was sent but never arrived, plus the
8
+ removal of a dead API.
9
+
10
+ ### Fixed
11
+
12
+ - **`send()` now infers a point's class from its value type** instead of always
13
+ tagging `class="metric"`. Numbers stay metrics; strings, bools, dicts and
14
+ lists become events. The gateway rejects a non-numeric value on
15
+ `class="metric"` and drops the *whole frame* — so `px.send("state", "RUNNING")`
16
+ silently took every other point in that batch down with it. Non-numeric values
17
+ have been advertised since 0.2.0; this is what makes them work.
18
+ An explicit `data_class="metric"` with a non-numeric value now raises
19
+ `PlexusError` locally rather than failing on the wire.
20
+
21
+ - **`source_id` validation matched an older, stricter rule than the server.**
22
+ The SDK enforced `^[a-z0-9][a-z0-9_-]{1,62}$` — no dots, minimum two
23
+ characters — while the gateway accepts `^[a-z0-9][a-z0-9._-]*$` up to 256
24
+ characters. Names production would have accepted (`rack.01`, `a`) raised
25
+ `ValueError` in the constructor before a single byte left the host, which
26
+ reads as an auth problem and is not one. The SDK now mirrors the wire rule.
27
+ Uuid-shaped ids are still rejected: the app resolves those as internal ids,
28
+ so such a source would be unreachable.
29
+
30
+ - Buffer drains are chunked, so a backlog over the gateway's 10k-points-per-batch
31
+ ceiling no longer wedges every subsequent send. The WebSocket auth wait is paid
32
+ once, on the first send, instead of stalling each call.
33
+
34
+ ### Removed
35
+
36
+ - `run()` context manager and its `POST /api/runs` plumbing. The `/api/runs` route never
37
+ existed on the platform (both notifications failed silently on every call), the ingest
38
+ loader drops `run_id` from points, and nothing was ever stored — so removal loses no
39
+ data. To group a slice of data, use `tags` on each point.
40
+
41
+ ### Security
42
+
43
+ - Bumped pillow to 12.3.0 and idna to 3.19 for published advisories.
44
+
45
+ ## [0.8.0] - 2026-07-02 - Command concurrency control
46
+
47
+ ### Added
48
+
49
+ - `concurrency` option on `on_command()` / `register_command()`. Defaults to `"accept"`
50
+ (unchanged behavior — overlapping invocations run concurrently). Set `"reject"` to refuse a
51
+ new invocation with an error result (`command already in progress: <name>`) while a previous
52
+ invocation of the same command is still running. Use it for handlers that drive exclusive
53
+ hardware (e.g. a pump init) so a client retry or double-click can't start two at once.
54
+ Protection is per command name; the immediate ack is still sent before the handler runs.
55
+
3
56
  ## [0.7.1] - 2026-06-02 - Remove install_id / source_id auto-suffix
4
57
 
5
58
  ### Changed
@@ -184,7 +237,7 @@
184
237
 
185
238
  ### Changed
186
239
 
187
- - Gateway WebSocket URL (`wss://plexus-gateway.fly.dev`) is now the SDK
240
+ - Gateway WebSocket URL (`wss://gateway.plexus.company`) is now the SDK
188
241
  default — no need to pass `ws_url` explicitly.
189
242
  - Removed the `[plexus] endpoint: …` line from the connection printout.
190
243
 
@@ -342,7 +395,7 @@ Breaking. `plexus-python` is now just the thin client — no agent, adapters, se
342
395
 
343
396
  ### Changed
344
397
 
345
- - Default ingest endpoint points directly at the Plexus gateway (`https://plexus-gateway.fly.dev/ingest`), not the Next.js app proxy
398
+ - Default ingest endpoint points directly at the Plexus gateway (`https://gateway.plexus.company/ingest`), not the Next.js app proxy
346
399
  - Client raises `ValueError` clearly when no API key is available, instead of invoking a login flow
347
400
 
348
401
  ## [0.1.0] - Initial release
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: plexus-python
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Thin Python SDK for Plexus — send telemetry in one line
5
- Project-URL: Homepage, https://plexus.dev
6
- Project-URL: Documentation, https://docs.plexus.dev
5
+ Project-URL: Homepage, https://plexus.company
6
+ Project-URL: Documentation, https://docs.plexus.company
7
7
  Project-URL: Repository, https://github.com/plexus-oss/plexus-python
8
8
  Project-URL: Issues, https://github.com/plexus-oss/plexus-python/issues
9
- Author-email: Plexus <hello@plexus.dev>
9
+ Author-email: Plexus <info@plexus.company>
10
10
  License-Expression: Apache-2.0
11
11
  License-File: LICENSE
12
12
  Keywords: fleet,hardware,iot,monitoring,observability,telemetry
@@ -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:
@@ -270,8 +259,8 @@ The SDK sends an `ack` frame before invoking the handler, then a `result` frame
270
259
  | Variable | Description | Default |
271
260
  | ----------------------- | ---------------------------- | -------------------------------- |
272
261
  | `PLEXUS_API_KEY` | API key (required) | none |
273
- | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://plexus-gateway.fly.dev` |
274
- | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://plexus-gateway.fly.dev` |
262
+ | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://gateway.plexus.company` |
263
+ | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://gateway.plexus.company` |
275
264
 
276
265
  ## Architecture
277
266