plexus-python 0.11.5__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.
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/bug_report.yml +4 -4
- {plexus_python-0.11.5 → plexus_python-0.11.6}/AGENTS.md +14 -5
- {plexus_python-0.11.5 → plexus_python-0.11.6}/API.md +57 -12
- {plexus_python-0.11.5 → plexus_python-0.11.6}/CHANGELOG.md +68 -1
- {plexus_python-0.11.5 → plexus_python-0.11.6}/PKG-INFO +27 -7
- {plexus_python-0.11.5 → plexus_python-0.11.6}/README.md +26 -6
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/__init__.py +1 -1
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/batching.py +4 -5
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/client.py +9 -9
- {plexus_python-0.11.5 → plexus_python-0.11.6}/pyproject.toml +1 -1
- plexus_python-0.11.6/scripts/setup.sh +462 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/scripts/verify_skills.py +3 -2
- {plexus_python-0.11.5 → plexus_python-0.11.6}/skills/README.md +3 -2
- {plexus_python-0.11.5 → plexus_python-0.11.6}/skills/plexus/SKILL.md +18 -13
- {plexus_python-0.11.5 → plexus_python-0.11.6}/skills/plexus-dashboard/SKILL.md +6 -6
- {plexus_python-0.11.5 → plexus_python-0.11.6}/skills/plexus-firmware/SKILL.md +19 -12
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_skills.py +12 -2
- {plexus_python-0.11.5 → plexus_python-0.11.6}/uv.lock +1 -1
- plexus_python-0.11.5/scripts/setup.sh +0 -351
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/.gitignore +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/CONTRIBUTING.md +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/LICENSE +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/SECURITY.md +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/.python-version +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/README.md +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/basic.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/can.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/mac_metrics.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/mavlink.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/mqtt.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/pyproject.toml +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/thermal_camera.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/examples/uv.lock +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/_log.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/buffer.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/cameras/__init__.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/cameras/thermal.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/cli.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/config.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/plexus/ws.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/scripts/release.sh +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/conftest.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_basic.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_batching.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_batching_wire.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_buffer.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_cli_diagnostics.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_config.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_isolation.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_rate_limit.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_retry.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_runs.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_thermal.py +0 -0
- {plexus_python-0.11.5 → plexus_python-0.11.6}/tests/test_video.py +0 -0
- {plexus_python-0.11.5 → 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:
|
|
9
|
+
label: SDK Version
|
|
10
10
|
description: Output of `plexus --version` or version in pyproject.toml
|
|
11
|
-
placeholder: "0.
|
|
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?
|
|
61
|
-
placeholder: "pip install plexus-python[
|
|
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
|
-
#
|
|
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
|
-
#
|
|
53
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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,7 +193,14 @@ 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
|
|
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
|
|
|
@@ -277,6 +319,7 @@ await fetch("https://gateway.plexus.company/ingest", {
|
|
|
277
319
|
body: JSON.stringify({
|
|
278
320
|
points: [
|
|
279
321
|
{
|
|
322
|
+
class: "metric",
|
|
280
323
|
metric: "temperature",
|
|
281
324
|
value: 72.5,
|
|
282
325
|
timestamp: Date.now() / 1000,
|
|
@@ -335,6 +378,7 @@ void sendToPlexus(const char* metric, float value) {
|
|
|
335
378
|
http.addHeader("x-api-key", "plx_xxxxx");
|
|
336
379
|
|
|
337
380
|
String payload = "{\"points\":[{";
|
|
381
|
+
payload += "\"class\":\"metric\",";
|
|
338
382
|
payload += "\"metric\":\"" + String(metric) + "\",";
|
|
339
383
|
payload += "\"value\":" + String(value) + ",";
|
|
340
384
|
payload += "\"timestamp\":" + String(time(nullptr)) + ",";
|
|
@@ -358,6 +402,7 @@ curl -X POST https://gateway.plexus.company/ingest \
|
|
|
358
402
|
-H "Content-Type: application/json" \
|
|
359
403
|
-d "{
|
|
360
404
|
\"points\": [{
|
|
405
|
+
\"class\": \"metric\",
|
|
361
406
|
\"metric\": \"temperature\",
|
|
362
407
|
\"value\": 72.5,
|
|
363
408
|
\"timestamp\": $(date +%s),
|
|
@@ -408,25 +453,25 @@ while True:
|
|
|
408
453
|
| 200 | Success |
|
|
409
454
|
| 400 | Bad request (check JSON format) |
|
|
410
455
|
| 401 | Invalid or missing API key |
|
|
411
|
-
| 403 | API key lacks permissions
|
|
456
|
+
| 403 | API key lacks permissions, or is limited to another device |
|
|
412
457
|
| 404 | Resource not found |
|
|
413
458
|
| 410 | Resource expired |
|
|
414
459
|
|
|
415
460
|
## Rate limits and batching
|
|
416
461
|
|
|
417
462
|
The gateway meters **messages**, not points. One `px.send()` call is one
|
|
418
|
-
message, whatever it carries:
|
|
463
|
+
message, whatever it carries. `send()` does not batch:
|
|
419
464
|
|
|
420
465
|
| Limit | Value |
|
|
421
466
|
| ---------------------------------- | ---------------- |
|
|
422
|
-
| Telemetry messages per WS connection |
|
|
423
|
-
| Hard ceiling per source (WS + HTTP) |
|
|
467
|
+
| Telemetry messages per WS connection | 2,000/s |
|
|
468
|
+
| Hard ceiling per source (WS + HTTP) | 2,000 messages/s, bursts up to 500 |
|
|
424
469
|
| Points per message | 10,000 |
|
|
425
470
|
| Message size | 1 MB |
|
|
426
471
|
|
|
427
472
|
Because the ceiling counts messages, the shape of your sends decides whether
|
|
428
|
-
you hit it.
|
|
429
|
-
over the limit. The same
|
|
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,
|
|
430
475
|
and the batches are also several times cheaper to store.
|
|
431
476
|
|
|
432
477
|
**Over the limit, the gateway discards the whole message.** It replies with a
|
|
@@ -495,4 +540,4 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
|
|
|
495
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
|
|
496
541
|
- **Consistent source_id** - Use the same ID for each physical device/source
|
|
497
542
|
- **Use tags** - Label data for filtering and grouping (e.g., `{"location": "lab"}`)
|
|
498
|
-
- **
|
|
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,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
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.
|
|
4
71
|
|
|
5
72
|
## [0.11.5] - 2026-09-21 - The commands API is off
|
|
6
73
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.11.
|
|
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)
|
|
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). `
|
|
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 —
|
|
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.
|
|
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=
|
|
241
|
+
px = Plexus(persistent_buffer=False)
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
Point counts and flush:
|
|
@@ -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:
|
|
@@ -310,7 +330,7 @@ credentials. See [skills/README.md](skills/README.md).
|
|
|
310
330
|
## Architecture
|
|
311
331
|
|
|
312
332
|
```
|
|
313
|
-
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
|
|
314
334
|
```
|
|
315
335
|
|
|
316
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)
|
|
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). `
|
|
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 —
|
|
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.
|
|
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=
|
|
204
|
+
px = Plexus(persistent_buffer=False)
|
|
189
205
|
```
|
|
190
206
|
|
|
191
207
|
Point counts and flush:
|
|
@@ -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:
|
|
@@ -273,7 +293,7 @@ credentials. See [skills/README.md](skills/README.md).
|
|
|
273
293
|
## Architecture
|
|
274
294
|
|
|
275
295
|
```
|
|
276
|
-
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
|
|
277
297
|
```
|
|
278
298
|
|
|
279
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.
|
|
@@ -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
|
|
6
|
-
connection and
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
186
|
-
#
|
|
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/
|
|
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
|
|
376
|
-
(
|
|
377
|
-
|
|
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
|
|
547
|
-
frames/s against a
|
|
548
|
-
Batched, the same
|
|
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:
|