plexus-python 0.11.4__tar.gz → 0.11.6__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 (61) hide show
  1. {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/bug_report.yml +4 -4
  2. {plexus_python-0.11.4 → plexus_python-0.11.6}/AGENTS.md +14 -5
  3. {plexus_python-0.11.4 → plexus_python-0.11.6}/API.md +61 -14
  4. {plexus_python-0.11.4 → plexus_python-0.11.6}/CHANGELOG.md +85 -1
  5. {plexus_python-0.11.4 → plexus_python-0.11.6}/PKG-INFO +30 -8
  6. {plexus_python-0.11.4 → plexus_python-0.11.6}/README.md +29 -7
  7. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/__init__.py +1 -1
  8. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/batching.py +4 -5
  9. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/client.py +9 -9
  10. {plexus_python-0.11.4 → plexus_python-0.11.6}/pyproject.toml +1 -1
  11. plexus_python-0.11.6/scripts/setup.sh +462 -0
  12. {plexus_python-0.11.4 → plexus_python-0.11.6}/scripts/verify_skills.py +3 -2
  13. {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/README.md +3 -2
  14. {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus/SKILL.md +21 -17
  15. {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus-dashboard/SKILL.md +6 -6
  16. {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus-firmware/SKILL.md +19 -12
  17. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_skills.py +12 -2
  18. {plexus_python-0.11.4 → plexus_python-0.11.6}/uv.lock +1 -1
  19. plexus_python-0.11.4/scripts/setup.sh +0 -351
  20. {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/workflows/ci.yml +0 -0
  23. {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/workflows/publish.yml +0 -0
  24. {plexus_python-0.11.4 → plexus_python-0.11.6}/.gitignore +0 -0
  25. {plexus_python-0.11.4 → plexus_python-0.11.6}/CODE_OF_CONDUCT.md +0 -0
  26. {plexus_python-0.11.4 → plexus_python-0.11.6}/CONTRIBUTING.md +0 -0
  27. {plexus_python-0.11.4 → plexus_python-0.11.6}/LICENSE +0 -0
  28. {plexus_python-0.11.4 → plexus_python-0.11.6}/SECURITY.md +0 -0
  29. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/.python-version +0 -0
  30. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/README.md +0 -0
  31. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/basic.py +0 -0
  32. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/can.py +0 -0
  33. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/i2c_bme280.py +0 -0
  34. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mac_metrics.py +0 -0
  35. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mavlink.py +0 -0
  36. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mqtt.py +0 -0
  37. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/pyproject.toml +0 -0
  38. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/thermal_camera.py +0 -0
  39. {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/uv.lock +0 -0
  40. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/_log.py +0 -0
  41. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/buffer.py +0 -0
  42. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cameras/__init__.py +0 -0
  43. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cameras/thermal.py +0 -0
  44. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cli.py +0 -0
  45. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/config.py +0 -0
  46. {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/ws.py +0 -0
  47. {plexus_python-0.11.4 → plexus_python-0.11.6}/scripts/release.sh +0 -0
  48. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/conftest.py +0 -0
  49. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_basic.py +0 -0
  50. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_batching.py +0 -0
  51. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_batching_wire.py +0 -0
  52. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_buffer.py +0 -0
  53. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_cli_diagnostics.py +0 -0
  54. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_config.py +0 -0
  55. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_isolation.py +0 -0
  56. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_rate_limit.py +0 -0
  57. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_retry.py +0 -0
  58. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_runs.py +0 -0
  59. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_thermal.py +0 -0
  60. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_video.py +0 -0
  61. {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_ws.py +0 -0
@@ -6,9 +6,9 @@ body:
6
6
  - type: input
7
7
  id: agent-version
8
8
  attributes:
9
- label: Agent Version
9
+ label: SDK Version
10
10
  description: Output of `plexus --version` or version in pyproject.toml
11
- placeholder: "0.9.6"
11
+ placeholder: "0.11.5"
12
12
  validations:
13
13
  required: true
14
14
 
@@ -57,8 +57,8 @@ body:
57
57
  id: extras
58
58
  attributes:
59
59
  label: Installed Extras
60
- description: Which optional extras are installed? (e.g., sensors, can, mavlink, camera)
61
- placeholder: "pip install plexus-python[sensors,can]"
60
+ description: Which optional extras are installed? The only one is `video`.
61
+ placeholder: "pip install plexus-python[video]"
62
62
 
63
63
  - type: textarea
64
64
  id: logs
@@ -24,7 +24,8 @@ plexus whoami # Show the local credential summary
24
24
 
25
25
  `plexus init` opens a browser to `app.plexus.company/auth/cli`, waits for the callback, and
26
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.
27
+ `api_key=` to `Plexus()`) instead of running `init`; get a key at app.plexus.company/api. The optional
28
+ "Limit to device slug" field there binds a key to one `source_id`; use it for devices in customer hands.
28
29
 
29
30
  ## Exit Codes
30
31
 
@@ -43,14 +44,22 @@ px = Plexus(api_key="plx_xxxxx", source_id="device-001")
43
44
  px.send("temperature", 72.5)
44
45
  px.send("pressure", 1013.25, tags={"unit": "hPa"})
45
46
 
46
- # Batch
47
+ # One message for a list of readings
47
48
  px.send_batch([
48
49
  ("temperature", 72.5),
49
50
  ("pressure", 1013.25),
50
51
  ])
51
52
 
52
- # Persistent buffering for reliability
53
- px = Plexus(api_key="plx_xxxxx", persistent_buffer=True)
53
+ # send() is one message per call and does not batch. Above a few readings
54
+ # a second, let a background thread group them:
55
+ with px.batch(interval_ms=50) as b:
56
+ b.send("temperature", 72.5)
57
+
58
+ # Events (faults, state changes, log lines). No log upload, no logging.Handler.
59
+ px.event("log", {"level": "error", "msg": "IMU read timed out"})
60
+
61
+ # The on-disk (SQLite) buffer is on by default; this keeps it in memory only
62
+ px = Plexus(api_key="plx_xxxxx", persistent_buffer=False)
54
63
  ```
55
64
 
56
65
  ## Key Conventions
@@ -58,5 +67,5 @@ px = Plexus(api_key="plx_xxxxx", persistent_buffer=True)
58
67
  - Config lives in `~/.plexus/config.json`
59
68
  - API keys are prefixed with `plx_`
60
69
  - Source IDs (device slugs) namespace metrics
61
- - HTTP ingest → `POST /ingest` on gateway; WebSocket → `/ws/device` for streaming + commands
70
+ - HTTP ingest → `POST /ingest` on gateway; WebSocket → `/ws/device` for streaming (paid plans; the SDK falls back to HTTP on Free)
62
71
  - Gateway resolves `org_id` server-side from the API key — clients do not supply it
@@ -9,7 +9,13 @@ Send telemetry data to Plexus using HTTP or WebSocket.
9
9
  | Method | Use Case |
10
10
  | --------- | ----------------------------------------------- |
11
11
  | HTTP POST | Simple scripts, batch uploads, embedded devices |
12
- | WebSocket | Real-time streaming, UI-controlled devices |
12
+ | WebSocket | Real-time streaming and video (paid plans) |
13
+
14
+ The Python SDK picks for you: it tries the WebSocket and falls back to HTTP on
15
+ its own. On the Free plan the gateway refuses the device WebSocket
16
+ (`streaming_requires_plan`), so the SDK sends everything over HTTP and
17
+ telemetry and events still land. Live streaming and video need a paid plan.
18
+ Free also allows up to 3 devices and 7 days of history.
13
19
 
14
20
  ## Quick Start
15
21
 
@@ -18,12 +24,12 @@ Send telemetry data to Plexus using HTTP or WebSocket.
18
24
  Set up your device with one command using an API key:
19
25
 
20
26
  ```bash
21
- # With API key (fleet provisioning — get from Settings → Developer)
27
+ # With an API key (get one at app.plexus.company/api)
22
28
  curl -sL https://app.plexus.company/setup | bash -s -- --key plx_your_api_key
23
29
 
24
30
  ```
25
31
 
26
- Then control streaming, recording, and configuration from [app.plexus.company/devices](https://app.plexus.company/devices).
32
+ Then find the device at [app.plexus.company/devices](https://app.plexus.company/devices).
27
33
 
28
34
  ### Option 2: Direct HTTP
29
35
 
@@ -63,9 +69,16 @@ Plexus uses API keys for all authentication:
63
69
  **Option B: Manual creation**
64
70
 
65
71
  1. Sign up at [app.plexus.company](https://app.plexus.company)
66
- 2. Go to Settings → Developer
72
+ 2. Go to [app.plexus.company/api](https://app.plexus.company/api)
67
73
  3. Create an API key (starts with `plx_`)
68
74
 
75
+ **Limit to device slug** (optional, on the same form) binds the key to one
76
+ `source_id`. The gateway then refuses it for any other source: `403` on
77
+ `/ingest`, and a `source_not_allowed` error on the WebSocket. Use a bound key
78
+ on every device you put in a customer's hands, so a key pulled off one unit
79
+ cannot write as the rest of your fleet. Leave it empty for a fleet key that can
80
+ write as any source in your org.
81
+
69
82
  ## HTTP API
70
83
 
71
84
  ### Authentication
@@ -97,6 +110,7 @@ x-api-key: plx_xxxxx
97
110
 
98
111
  | Field | Type | Required | Description |
99
112
  | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | `class` | string | WS only | `"metric"` (numeric value) or `"event"` (anything else). Set it explicitly. Over HTTP a missing `class` is inferred from the value; over WebSocket it is required. |
100
114
  | `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
101
115
  | `value` | any | Yes | See supported value types below |
102
116
  | `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). |
@@ -113,6 +127,27 @@ x-api-key: plx_xxxxx
113
127
  | object | `{"x": 1.2, "y": 3.4, "z": 5.6}` | Vector data, structured readings |
114
128
  | array | `[1.0, 2.0, 3.0, 4.0]` | Waveforms, multiple values |
115
129
 
130
+ ### Events and logs
131
+
132
+ Things that happen, rather than things you measure, are `class: "event"`
133
+ points: faults, state changes, operator actions, log lines. The Python SDK
134
+ sends them with `px.event(name, data)`.
135
+
136
+ Limits per event point: a string value up to 256 bytes, an object or array
137
+ value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects larger
138
+ points with a 400.
139
+
140
+ There is no log-file upload and no `logging.Handler`. Forward the log lines
141
+ that matter as events:
142
+
143
+ ```python
144
+ px.event("log", {"level": "error", "msg": "IMU read timed out"})
145
+ ```
146
+
147
+ Send errors, warnings and state changes, not every debug line: each call is
148
+ one message, and the gateway limits messages (see
149
+ [Rate limits and batching](#rate-limits-and-batching)).
150
+
116
151
  ### Runs
117
152
 
118
153
  A **run** is a named time window on a source — a hot-fire, a bench sequence, a
@@ -158,14 +193,21 @@ To group data without a window, plain `tags` on each point still work.
158
193
 
159
194
  ## WebSocket API
160
195
 
161
- For real-time UI-controlled streaming, devices connect via WebSocket.
196
+ For real-time streaming and video, devices connect via WebSocket. This needs a
197
+ paid plan: on Free, the gateway answers `device_auth` with an error frame
198
+ (`"code": "streaming_requires_plan"`) and closes. Send over HTTP instead; the
199
+ Python SDK does this for you.
200
+
201
+ Video frames go over this socket. They are relayed live to anyone watching and
202
+ stored only when someone presses **Record** in the app, for up to 4 hours per
203
+ recording.
162
204
 
163
205
  ### Connection Flow
164
206
 
165
207
  1. Device connects to the gateway
166
208
  2. Device authenticates with API key (and advertises any registered commands)
167
209
  3. Device streams `telemetry` frames
168
- 4. Dashboard/API invokes registered commands via `typed_command`
210
+ 4. Plexus invokes registered commands via `typed_command` (see the note under Commands)
169
211
 
170
212
  ### Device Authentication
171
213
 
@@ -227,7 +269,9 @@ Devices authenticate using an API key. The gateway echoes the declared `source_i
227
269
 
228
270
  ### Commands
229
271
 
230
- 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()`.
272
+ Commands 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()`.
273
+
274
+ > **Note:** the public API route for sending commands (`POST /v1/sources/{id}/commands`) was turned off on 2026-09-21 and returns `410 Gone`. Nothing in Plexus can currently trigger a custom handler.
231
275
 
232
276
  ```json
233
277
  // Server → Device
@@ -275,6 +319,7 @@ await fetch("https://gateway.plexus.company/ingest", {
275
319
  body: JSON.stringify({
276
320
  points: [
277
321
  {
322
+ class: "metric",
278
323
  metric: "temperature",
279
324
  value: 72.5,
280
325
  timestamp: Date.now() / 1000,
@@ -333,6 +378,7 @@ void sendToPlexus(const char* metric, float value) {
333
378
  http.addHeader("x-api-key", "plx_xxxxx");
334
379
 
335
380
  String payload = "{\"points\":[{";
381
+ payload += "\"class\":\"metric\",";
336
382
  payload += "\"metric\":\"" + String(metric) + "\",";
337
383
  payload += "\"value\":" + String(value) + ",";
338
384
  payload += "\"timestamp\":" + String(time(nullptr)) + ",";
@@ -356,6 +402,7 @@ curl -X POST https://gateway.plexus.company/ingest \
356
402
  -H "Content-Type: application/json" \
357
403
  -d "{
358
404
  \"points\": [{
405
+ \"class\": \"metric\",
359
406
  \"metric\": \"temperature\",
360
407
  \"value\": 72.5,
361
408
  \"timestamp\": $(date +%s),
@@ -406,25 +453,25 @@ while True:
406
453
  | 200 | Success |
407
454
  | 400 | Bad request (check JSON format) |
408
455
  | 401 | Invalid or missing API key |
409
- | 403 | API key lacks permissions |
456
+ | 403 | API key lacks permissions, or is limited to another device |
410
457
  | 404 | Resource not found |
411
458
  | 410 | Resource expired |
412
459
 
413
460
  ## Rate limits and batching
414
461
 
415
462
  The gateway meters **messages**, not points. One `px.send()` call is one
416
- message, whatever it carries:
463
+ message, whatever it carries. `send()` does not batch:
417
464
 
418
465
  | Limit | Value |
419
466
  | ---------------------------------- | ---------------- |
420
- | Telemetry messages per WS connection | 500/s sustained, 2000 burst |
421
- | Hard ceiling per source (WS + HTTP) | 2000 messages/s |
467
+ | Telemetry messages per WS connection | 2,000/s |
468
+ | Hard ceiling per source (WS + HTTP) | 2,000 messages/s, bursts up to 500 |
422
469
  | Points per message | 10,000 |
423
470
  | Message size | 1 MB |
424
471
 
425
472
  Because the ceiling counts messages, the shape of your sends decides whether
426
- you hit it. Eight channels at 100 Hz sent one at a time is 800 messages/s —
427
- over the limit. The same 800 readings/s batched every 100 ms is 10 messages/s,
473
+ you hit it. 25 channels at 100 Hz sent one at a time is 2,500 messages/s —
474
+ over the limit. The same 2,500 readings/s batched every 100 ms is 10 messages/s,
428
475
  and the batches are also several times cheaper to store.
429
476
 
430
477
  **Over the limit, the gateway discards the whole message.** It replies with a
@@ -493,4 +540,4 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
493
540
  - **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
494
541
  - **Consistent source_id** - Use the same ID for each physical device/source
495
542
  - **Use tags** - Label data for filtering and grouping (e.g., `{"location": "lab"}`)
496
- - **Prefer WebSocket** - For real-time UI-controlled devices, the SDK connects over WebSocket by default
543
+ - **Let the SDK pick the transport** - It uses the WebSocket when your plan allows it and falls back to HTTP otherwise
@@ -1,6 +1,90 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.11.6] - 2026-09-22 - Docs and skills catch up
4
+
5
+ Docs and agent skills caught up with the platform. The only code change is
6
+ one error message. The skills ship inside the wheel, so this release is how
7
+ they reach users.
8
+
9
+ ### Fixed
10
+
11
+ - **The 0.11.4 entry below overstated the `class` bug.** It said raw-HTTP
12
+ examples without `class` return 400. They don't: `POST /ingest` infers a
13
+ missing `class` (numbers become metrics, everything else events). The
14
+ WebSocket is the path that requires it, and `"class": "metric"` on a
15
+ non-number is still a 400. The skills and `API.md` now say that, and still
16
+ tell raw-HTTP senders to set `class` explicitly. The JavaScript, Arduino
17
+ and Bash examples in `API.md` now set it too.
18
+
19
+ - **Where to get an API key.** The README said "Devices → Add Device" and
20
+ `API.md` said "Settings → Developer". Keys are made at
21
+ `app.plexus.company/api`, and the "no API key" error now says so instead
22
+ of pointing at `/devices`.
23
+
24
+ - **The README said `setup.sh` refuses to run without `--name`.** It
25
+ doesn't. Without a name the SDK makes up a random `source-xxxxxxxx` id.
26
+ The README now says to pass `--name` and what happens if you don't.
27
+
28
+ - **The README said to "enable" the SQLite buffer.** It is on by default.
29
+ The example now shows how to turn it off.
30
+
31
+ - **The firmware skill said `px.send()` is "batched for you".** It is one
32
+ message per call. Only `px.batch()` batches.
33
+
34
+ - **The bug-report template asked which of `sensors`, `can`, `mavlink` and
35
+ `camera` you had installed.** None of those extras exist. The only one is
36
+ `video`.
37
+
38
+ ### Changed
39
+
40
+ - **The read API host in the skills is `https://api.plexus.company`**, not
41
+ the raw Fly hostname. `scripts/verify_skills.py` checks it by default, and
42
+ a test keeps the old hostname out of the skills.
43
+
44
+ - **Documented "Limit to device slug".** A key made with it can only send as
45
+ that one `source_id`. The docs recommend one for every device in a
46
+ customer's hands.
47
+
48
+ - **Documented the Free plan's limits.** The gateway refuses the device
49
+ WebSocket on Free (`streaming_requires_plan`); the SDK falls back to HTTP,
50
+ so telemetry and events still land. Live streaming and video need a paid
51
+ plan. Free allows 3 devices and 7 days of history.
52
+
53
+ - **Documented how video is kept.** Frames are relayed live, and stored only
54
+ when someone presses Record in the app, up to 4 hours per recording.
55
+
56
+ - **Documented logs.** There is no log upload and no `logging.Handler`.
57
+ Forward the lines that matter with `px.event("log", {...})`. Event limits
58
+ are now written down: 256 bytes for a string value, 4,096 bytes of JSON
59
+ for an object or array, 16 tags.
60
+
61
+ - **`scripts/setup.sh` re-synced with the script `/setup` serves.** The old
62
+ copy still installed `[sensors]` and `[picamera]` extras that were removed
63
+ in 0.2.0. It also links `github.com/plexus-oss/plexus-python` and
64
+ `app.plexus.company/api` now.
65
+
66
+ - **The skills read device events from `/v1/sources/{id}/events`.** It is
67
+ live on the data API as of 2026-09-22. `/logs` still works as an alias.
68
+
69
+ - **Rate limits quote the gateway's current ceiling**: 2,000 messages/s per
70
+ connection and per source, not the old 500/s.
71
+
72
+ ## [0.11.5] - 2026-09-21 - The commands API is off
73
+
74
+ No code changes. The docs described a way to send commands to a device that
75
+ no longer exists.
76
+
77
+ ### Changed
78
+
79
+ - **`POST /v1/sources/{id}/commands` is turned off, and the docs now say so.**
80
+ Plexus turned the endpoint off on 2026-09-21. It returns `410 Gone`.
81
+ Any API key of any scope could send any command name to any device in
82
+ the org, which is not safe for commands that reach hardware.
83
+ - The agent skill no longer offers to send commands, and it tells agents
84
+ not to call the endpoint.
85
+ - README and `API.md` say plainly that nothing in Plexus can currently
86
+ trigger an `on_command` handler.
87
+ - `on_command` itself is unchanged.
4
88
 
5
89
  ## [0.11.4] - 2026-09-08 - Docs that actually work
6
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plexus-python
3
- Version: 0.11.4
3
+ Version: 0.11.6
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
@@ -55,7 +55,9 @@ px = Plexus(api_key="plx_xxx", source_id="device-001")
55
55
  px.send("temperature", 72.5)
56
56
  ```
57
57
 
58
- Get an API key at [app.plexus.company](https://app.plexus.company) Devices Add Device.
58
+ Get an API key at [app.plexus.company/api](https://app.plexus.company/api), or run `plexus init` to authorize the machine in a browser.
59
+
60
+ When you create a key there, you can fill in **Limit to device slug**. That key can then only send as that one `source_id`; the gateway refuses it for any other. Use one of these for every device you put in a customer's hands, so a key pulled off one unit cannot write as the rest of your fleet.
59
61
 
60
62
  ## Device identity
61
63
 
@@ -66,7 +68,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
66
68
  --key plx_xxx --name drone-01
67
69
  ```
68
70
 
69
- The name must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). `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`.
71
+ The name is turned into the device's `source_id`, which must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). Pass `--name` every time. Without it, and without `source_id=...` in code, the SDK makes up a random id like `source-1a2b3c4d` on first run and saves it to `~/.plexus/config.json`. Don't use the hostname: cloned SD-card images all boot as `raspberrypi`, and their telemetry merges into one source.
70
72
 
71
73
  **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
74
 
@@ -115,7 +117,7 @@ px.send_batch([
115
117
 
116
118
  ### `batch()` — coalesce a fast stream of readings
117
119
 
118
- Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 500/s on a connection. Eight channels at 100 Hz sent one at a time is 800 messages/s, and the overflow is discarded before it is stored.
120
+ Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 2,000/s on a connection. 25 channels at 100 Hz sent one at a time is 2,500 messages/s, and the overflow is discarded before it is stored.
119
121
 
120
122
  ```python
121
123
  with px.batch(interval_ms=50) as b:
@@ -154,10 +156,24 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
154
156
 
155
157
  The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
156
158
 
159
+ Limits per event: a string value up to 256 bytes, a dict or list value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects anything larger.
160
+
161
+ ### Logs
162
+
163
+ There is no log-file upload and no `logging.Handler` in this package. To get important log lines into Plexus, send them as events:
164
+
165
+ ```python
166
+ px.event("log", {"level": "error", "msg": "IMU read timed out"})
167
+ ```
168
+
169
+ Forward the lines you would want on the timeline next to your telemetry (errors, warnings, state changes), not every debug line. Each call is one message, and the gateway limits messages (see [`batch()`](#batch--coalesce-a-fast-stream-of-readings)).
170
+
157
171
  ## Video streaming
158
172
 
159
173
  Two methods depending on whether you control the capture loop or just have a URL.
160
174
 
175
+ Video needs a paid plan: frames go over the WebSocket, which the gateway refuses on the Free plan. Frames are relayed live to anyone watching. They are stored only when someone presses **Record** in the app, for up to 4 hours per recording.
176
+
161
177
  ### `send_video_frame(frame, camera_id)` — send frames you capture yourself
162
178
 
163
179
  Use this when your code owns the capture loop — a `picamera2` callback, an OpenCV `VideoCapture` loop, or an FFmpeg pipe you manage. Pass each frame and the SDK ships it to Plexus over WebSocket.
@@ -219,10 +235,10 @@ See [`examples/`](examples/) for runnable versions of each.
219
235
 
220
236
  ## Reliability
221
237
 
222
- Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. Enable SQLite persistence to survive restarts and power loss:
238
+ Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. The buffer is on disk (SQLite) by default, so it survives restarts and power loss. To keep it in memory only:
223
239
 
224
240
  ```python
225
- px = Plexus(persistent_buffer=True)
241
+ px = Plexus(persistent_buffer=False)
226
242
  ```
227
243
 
228
244
  Point counts and flush:
@@ -255,7 +271,7 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
255
271
  By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — the gateway's device wire protocol. This gives you:
256
272
 
257
273
  - lower-latency streaming of telemetry,
258
- - live command delivery from the UI / API to the device.
274
+ - the channel that will carry actions triggered from a Plexus dashboard.
259
275
 
260
276
  If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
261
277
 
@@ -266,6 +282,10 @@ px = Plexus()
266
282
 
267
283
  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.
268
284
 
285
+ Either way, plain `px.send()` is one message per call; it does not batch. `px.send_batch()` sends one list as one message, and `px.batch()` groups a fast stream for you in the background.
286
+
287
+ **On the Free plan** the gateway refuses the device WebSocket (`streaming_requires_plan`). The SDK falls back to HTTP by itself, so `send()`, `send_batch()`, `batch()` and `event()` all still work. Live streaming and video need a paid plan. Free also caps you at 3 devices and 7 days of history.
288
+
269
289
  ### Handling commands
270
290
 
271
291
  Register a handler before the first `send()` so the command is advertised in the auth frame:
@@ -283,6 +303,8 @@ px.send("temperature", 72.5) # opens the socket, waits for auth
283
303
 
284
304
  The SDK sends an `ack` frame before invoking the handler, then a `result` frame with whatever the handler returns (or an `error` frame if it raises).
285
305
 
306
+ > **Note:** nothing in Plexus can currently trigger a custom handler. The API route for sending commands was turned off on 2026-09-21. Triggering handlers from a dashboard, with permissions and a record of every run, is being rebuilt.
307
+
286
308
  ## Environment Variables
287
309
 
288
310
  | Variable | Description | Default |
@@ -308,7 +330,7 @@ credentials. See [skills/README.md](skills/README.md).
308
330
  ## Architecture
309
331
 
310
332
  ```
311
- Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
333
+ Your code ── px.send() ── WebSocket /ws/device (or HTTP POST /ingest) ──> plexus-gateway ──> ClickHouse + Dashboard
312
334
  ```
313
335
 
314
336
  One thin path. No agent, no daemon, no adapters. If you want the full HardwareOps platform — dashboards, alerts, RCA, fleet views — that's the web UI at app.plexus.company. This package gets your data there.
@@ -18,7 +18,9 @@ px = Plexus(api_key="plx_xxx", source_id="device-001")
18
18
  px.send("temperature", 72.5)
19
19
  ```
20
20
 
21
- Get an API key at [app.plexus.company](https://app.plexus.company) Devices Add Device.
21
+ Get an API key at [app.plexus.company/api](https://app.plexus.company/api), or run `plexus init` to authorize the machine in a browser.
22
+
23
+ When you create a key there, you can fill in **Limit to device slug**. That key can then only send as that one `source_id`; the gateway refuses it for any other. Use one of these for every device you put in a customer's hands, so a key pulled off one unit cannot write as the rest of your fleet.
22
24
 
23
25
  ## Device identity
24
26
 
@@ -29,7 +31,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
29
31
  --key plx_xxx --name drone-01
30
32
  ```
31
33
 
32
- The name must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). `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`.
34
+ The name is turned into the device's `source_id`, which must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). Pass `--name` every time. Without it, and without `source_id=...` in code, the SDK makes up a random id like `source-1a2b3c4d` on first run and saves it to `~/.plexus/config.json`. Don't use the hostname: cloned SD-card images all boot as `raspberrypi`, and their telemetry merges into one source.
33
35
 
34
36
  **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
37
 
@@ -78,7 +80,7 @@ px.send_batch([
78
80
 
79
81
  ### `batch()` — coalesce a fast stream of readings
80
82
 
81
- Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 500/s on a connection. Eight channels at 100 Hz sent one at a time is 800 messages/s, and the overflow is discarded before it is stored.
83
+ Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 2,000/s on a connection. 25 channels at 100 Hz sent one at a time is 2,500 messages/s, and the overflow is discarded before it is stored.
82
84
 
83
85
  ```python
84
86
  with px.batch(interval_ms=50) as b:
@@ -117,10 +119,24 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
117
119
 
118
120
  The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
119
121
 
122
+ Limits per event: a string value up to 256 bytes, a dict or list value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects anything larger.
123
+
124
+ ### Logs
125
+
126
+ There is no log-file upload and no `logging.Handler` in this package. To get important log lines into Plexus, send them as events:
127
+
128
+ ```python
129
+ px.event("log", {"level": "error", "msg": "IMU read timed out"})
130
+ ```
131
+
132
+ Forward the lines you would want on the timeline next to your telemetry (errors, warnings, state changes), not every debug line. Each call is one message, and the gateway limits messages (see [`batch()`](#batch--coalesce-a-fast-stream-of-readings)).
133
+
120
134
  ## Video streaming
121
135
 
122
136
  Two methods depending on whether you control the capture loop or just have a URL.
123
137
 
138
+ Video needs a paid plan: frames go over the WebSocket, which the gateway refuses on the Free plan. Frames are relayed live to anyone watching. They are stored only when someone presses **Record** in the app, for up to 4 hours per recording.
139
+
124
140
  ### `send_video_frame(frame, camera_id)` — send frames you capture yourself
125
141
 
126
142
  Use this when your code owns the capture loop — a `picamera2` callback, an OpenCV `VideoCapture` loop, or an FFmpeg pipe you manage. Pass each frame and the SDK ships it to Plexus over WebSocket.
@@ -182,10 +198,10 @@ See [`examples/`](examples/) for runnable versions of each.
182
198
 
183
199
  ## Reliability
184
200
 
185
- Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. Enable SQLite persistence to survive restarts and power loss:
201
+ Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. The buffer is on disk (SQLite) by default, so it survives restarts and power loss. To keep it in memory only:
186
202
 
187
203
  ```python
188
- px = Plexus(persistent_buffer=True)
204
+ px = Plexus(persistent_buffer=False)
189
205
  ```
190
206
 
191
207
  Point counts and flush:
@@ -218,7 +234,7 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
218
234
  By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — the gateway's device wire protocol. This gives you:
219
235
 
220
236
  - lower-latency streaming of telemetry,
221
- - live command delivery from the UI / API to the device.
237
+ - the channel that will carry actions triggered from a Plexus dashboard.
222
238
 
223
239
  If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
224
240
 
@@ -229,6 +245,10 @@ px = Plexus()
229
245
 
230
246
  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.
231
247
 
248
+ Either way, plain `px.send()` is one message per call; it does not batch. `px.send_batch()` sends one list as one message, and `px.batch()` groups a fast stream for you in the background.
249
+
250
+ **On the Free plan** the gateway refuses the device WebSocket (`streaming_requires_plan`). The SDK falls back to HTTP by itself, so `send()`, `send_batch()`, `batch()` and `event()` all still work. Live streaming and video need a paid plan. Free also caps you at 3 devices and 7 days of history.
251
+
232
252
  ### Handling commands
233
253
 
234
254
  Register a handler before the first `send()` so the command is advertised in the auth frame:
@@ -246,6 +266,8 @@ px.send("temperature", 72.5) # opens the socket, waits for auth
246
266
 
247
267
  The SDK sends an `ack` frame before invoking the handler, then a `result` frame with whatever the handler returns (or an `error` frame if it raises).
248
268
 
269
+ > **Note:** nothing in Plexus can currently trigger a custom handler. The API route for sending commands was turned off on 2026-09-21. Triggering handlers from a dashboard, with permissions and a record of every run, is being rebuilt.
270
+
249
271
  ## Environment Variables
250
272
 
251
273
  | Variable | Description | Default |
@@ -271,7 +293,7 @@ credentials. See [skills/README.md](skills/README.md).
271
293
  ## Architecture
272
294
 
273
295
  ```
274
- Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
296
+ Your code ── px.send() ── WebSocket /ws/device (or HTTP POST /ingest) ──> plexus-gateway ──> ClickHouse + Dashboard
275
297
  ```
276
298
 
277
299
  One thin path. No agent, no daemon, no adapters. If you want the full HardwareOps platform — dashboards, alerts, RCA, fleet views — that's the web UI at app.plexus.company. This package gets your data there.
@@ -23,7 +23,7 @@ from plexus.client import (
23
23
  )
24
24
  from plexus.config import RetryConfig
25
25
 
26
- __version__ = "0.11.4"
26
+ __version__ = "0.11.6"
27
27
  __all__ = [
28
28
  "AuthenticationError",
29
29
  "BatchSender",
@@ -2,11 +2,10 @@
2
2
 
3
3
  `px.send()` transmits immediately: every call is its own WebSocket frame. That
4
4
  is the right shape for a script sampling a sensor once a second, and the wrong
5
- one for a test bench. The gateway allows 500 telemetry messages per second on a
6
- connection and hard-drops a source above 2000/s, and both ceilings count
7
- *messages*, not points — so eight channels at 100 Hz is 800 frames/s and lands
8
- over the limit, while the same 800 readings coalesced into ten frames is not
9
- close to it. The dropped frames come back as `RATE_LIMITED`, after `send()` has
5
+ one for a test bench. The gateway allows 2,000 telemetry messages per second on
6
+ a connection and per source, and the ceiling counts *messages*, not points — so
7
+ 25 channels at 100 Hz is 2,500 frames/s and lands over the limit, while the same
8
+ 2,500 readings coalesced into ten frames is not close to it. The dropped frames come back as `RATE_LIMITED`, after `send()` has
10
9
  already returned True, which is why this is a batching problem rather than a
11
10
  retry one: by the time anyone can react, the points are gone.
12
11
 
@@ -182,8 +182,8 @@ class RateLimitedError(PlexusError):
182
182
  # The wire slug rule (gateway validate.go sourceIDPattern, max length =
183
183
  # MaxStringLen 256) — the old stricter local regex rejected dots, 1-char and
184
184
  # >63-char slugs that the gateway accepts. Uuid-shaped slugs are additionally
185
- # rejected (TypeScript SDK parity): the Plexus app resolves uuid-shaped refs
186
- # as internal ids, which would make such a source unreachable.
185
+ # rejected: the Plexus app resolves uuid-shaped refs as internal ids, which
186
+ # would make such a source unreachable.
187
187
  _SOURCE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9._-]*$')
188
188
  _SOURCE_ID_UUID_RE = re.compile(
189
189
  r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
@@ -271,7 +271,7 @@ class Plexus:
271
271
  if not self.api_key:
272
272
  raise ValueError(
273
273
  "No API key. Pass api_key=... or set PLEXUS_API_KEY. "
274
- "Get a key at app.plexus.company/devices."
274
+ "Get a key at app.plexus.company/api."
275
275
  )
276
276
 
277
277
  self.endpoint = (endpoint or get_endpoint()).rstrip("/")
@@ -372,9 +372,9 @@ class Plexus:
372
372
  def _infer_class(value: FlexValue) -> str:
373
373
  """Numbers are metrics; everything else (str/bool/dict/list) is an event.
374
374
 
375
- Mirrors the gateway (ingest.go inferClass) and the TypeScript SDK
376
- (wire.ts inferClass). bool is a subclass of int in Python, so it must
377
- be excluded explicitly or True/False would wrongly become metrics.
375
+ Mirrors the gateway, which infers class the same way on HTTP /ingest
376
+ (ingest.go). bool is a subclass of int in Python, so it must be
377
+ excluded explicitly or True/False would wrongly become metrics.
378
378
  """
379
379
  return "metric" if isinstance(value, (int, float)) and not isinstance(value, bool) else "event"
380
380
 
@@ -543,9 +543,9 @@ class Plexus:
543
543
 
544
544
  Use this whenever readings arrive faster than a few times a second.
545
545
  `send()` puts every reading in its own WebSocket frame, and the gateway
546
- limits *frames*, not points — so eight channels at 100 Hz is 800
547
- frames/s against a 500/s ceiling, and the overflow is discarded.
548
- Batched, the same 800 readings/s is 10 frames/s.
546
+ limits *frames*, not points — so 25 channels at 100 Hz is 2,500
547
+ frames/s against a 2,000/s ceiling, and the overflow is discarded.
548
+ Batched, the same 2,500 readings/s is 10 frames/s.
549
549
 
550
550
  with px.batch(interval_ms=50) as b:
551
551
  while running:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plexus-python"
7
- version = "0.11.4"
7
+ version = "0.11.6"
8
8
  description = "Thin Python SDK for Plexus — send telemetry in one line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"