plexus-python 0.11.5__py3-none-any.whl → 0.11.6__py3-none-any.whl

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/__init__.py CHANGED
@@ -23,7 +23,7 @@ from plexus.client import (
23
23
  )
24
24
  from plexus.config import RetryConfig
25
25
 
26
- __version__ = "0.11.5"
26
+ __version__ = "0.11.6"
27
27
  __all__ = [
28
28
  "AuthenticationError",
29
29
  "BatchSender",
plexus/_skills/README.md CHANGED
@@ -36,8 +36,9 @@ Then just ask for what you want — "send my ESP32's battery voltage to Plexus",
36
36
  An agent that has not read these invents a plausible Plexus API and gets it
37
37
  wrong in ways that fail quietly. The three that cost the most real time:
38
38
 
39
- - The ingest array is **`points`**, not `metrics`, and every point needs a
40
- `class`. Getting this wrong is a 400 on every write.
39
+ - The ingest array is **`points`**, not `metrics`. Getting this wrong is a
40
+ 400 on every write. Every point should also carry a `class`: HTTP infers
41
+ it when missing, the WebSocket does not.
41
42
  - `timestamp` must be a **number**. An ISO-8601 string is rejected outright.
42
43
  - The query response is **columnar** — `series[m].avg[i]`, not
43
44
  `series[m][i].v`. Guessing wrong yields `undefined` with no error: an empty
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plexus
3
- description: Integrate with the Plexus telemetry API — send data, query metrics, subscribe to live streams. Use when the user mentions Plexus, plexus.company, gateway.plexus.company, plexus-data-api.fly.dev, or plx_ API keys. ALSO USE when the request involves IoT/hardware telemetry, fleet observability, sending sensor data to a backend, querying device time-series, building a fleet dashboard, monitoring drones/satellites/robots/edge devices, or any phrase like "send my sensor readings somewhere", "store telemetry", "track a fleet", "ingest metrics", or "device observability" — even if "Plexus" is never said.
3
+ description: Integrate with the Plexus telemetry API — send data, query metrics, subscribe to live streams. Use when the user mentions Plexus, plexus.company, gateway.plexus.company, api.plexus.company, or plx_ API keys. ALSO USE when the request involves IoT/hardware telemetry, fleet observability, sending sensor data to a backend, querying device time-series, building a fleet dashboard, monitoring drones/satellites/robots/edge devices, or any phrase like "send my sensor readings somewhere", "store telemetry", "track a fleet", "ingest metrics", or "device observability" — even if "Plexus" is never said.
4
4
  tools: Read, Write, Edit, Bash, WebFetch
5
5
  ---
6
6
 
@@ -12,7 +12,7 @@ Plexus is a telemetry/observability platform for hardware fleets (drones, satell
12
12
 
13
13
  Trigger on any of:
14
14
 
15
- - The user mentions "Plexus", "plexus.company", `plx_` keys, `gateway.plexus.company`, or `plexus-data-api.fly.dev`
15
+ - The user mentions "Plexus", "plexus.company", `plx_` keys, `gateway.plexus.company`, or `api.plexus.company`
16
16
  - The user wants to ingest telemetry, query device metrics, or stream live points
17
17
  - The user is building a dashboard, alert pipeline, or analysis on top of fleet telemetry
18
18
  - The user pastes a Plexus curl example and asks for help
@@ -27,19 +27,21 @@ Two base URLs. Authenticate with an `x-api-key` header on HTTP.
27
27
  | Purpose | Host |
28
28
  | -------------------------------------------------- | -------------------------------- |
29
29
  | Ingest | `https://gateway.plexus.company` |
30
- | Read API (sources, metrics, logs, fleet, live stream) | `https://plexus-data-api.fly.dev` |
30
+ | Read API (sources, metrics, events, fleet, live stream) | `https://api.plexus.company` |
31
31
 
32
32
  ```
33
33
  x-api-key: plx_...
34
34
  ```
35
35
 
36
- The OpenAPI spec for the read API lives at `https://plexus-data-api.fly.dev/openapi.json` — fetch it when you need exact schemas. **It does not list the WebSocket endpoints** (FastAPI omits them), so use the Live section below for those.
36
+ The OpenAPI spec for the read API lives at `https://api.plexus.company/openapi.json` — fetch it when you need exact schemas. **It does not list the WebSocket endpoints** (FastAPI omits them), so use the Live section below for those.
37
37
 
38
38
  Read keys from env, never hardcode:
39
39
 
40
40
  - `PLEXUS_API_KEY` — server / CLI
41
41
  - `NEXT_PUBLIC_PLEXUS_API_KEY` — client-side Next.js (only if the user explicitly accepts the trade-off)
42
42
 
43
+ Keys are made at `https://app.plexus.company/api`. The optional **Limit to device slug** field binds a key to one `source_id`; the gateway then refuses it for any other source (`403` on `/ingest`). Recommend a bound key per device for hardware that ships to customers, and a fleet key only for servers you control.
44
+
43
45
  ## Endpoint cheat sheet
44
46
 
45
47
  ### Send (gateway)
@@ -57,13 +59,16 @@ Read keys from env, never hardcode:
57
59
 
58
60
  Per point:
59
61
 
60
- - **`class` is required** and must be `"metric"` or `"event"`. Omitting it is a 400.
62
+ - **Set `class` on every point**: `"metric"` (the value must be a number) or `"event"` (string, bool, object, array). On HTTP `/ingest` a missing `class` is inferred — numbers become metrics, everything else events — but the WebSocket path rejects a point without it, and an explicit class says what you meant. The Python SDK always sets it.
61
63
  - **The array is `points`, not `metrics`.** Sending `metrics: [...]` returns `400 {"error":"'points' array is required"}`. This is the single most common mistake.
62
64
  - `metric` (string) and `value` are required.
63
65
  - `timestamp` must be a **number** — an ISO-8601 string is rejected with `points[i].timestamp must be a number`. Epoch **milliseconds** is the intended unit; a positive value under `1e12` is interpreted as **seconds** and scaled up automatically.
64
66
  - Omitting `timestamp` is safe: the point is stamped with gateway receive time. (This used to land at 1970 and be invisible — fixed, the loader now falls back to `ingested_at`.)
65
67
  - `source_id` may be set at the envelope level, per point, or both — per point wins.
66
68
  - The gateway creates the source on first write; no registration step.
69
+ - Event limits: a string value up to 256 bytes, an object/array value up to 4,096 bytes of JSON, up to 16 tags per point. Larger is a 400.
70
+ - **There is no log upload endpoint.** Send log lines that matter as event points, e.g. `{"class":"event","metric":"log","value":{"level":"error","msg":"..."}}` (`px.event("log", {...})` in plexus-python).
71
+ - **Free plan:** `/ingest` works. The device WebSocket and video are refused (`streaming_requires_plan`); plexus-python falls back to HTTP by itself. Free keeps 7 days of history and up to 3 devices.
67
72
 
68
73
  `POST /api/v1/write` also exists as a Prometheus/Alloy/OTel/Telegraf remote-write receiver. Do not reach for it unless the user already runs one of those.
69
74
 
@@ -78,7 +83,7 @@ Paths are `/v1/sources/...`. `/v1/devices/...` is a deprecated alias — bare `/
78
83
  - `GET /v1/sources/{id}/metrics` → `string[]` (metric names)
79
84
  - `GET /v1/sources/{id}/metrics/latest` → `{ metrics: { [name]: number } }`
80
85
  - `GET /v1/sources/{id}/metrics/query?metrics=a,b&last=1h` → columnar, see below. Also takes `start`, `end`, `interval`.
81
- - `GET /v1/sources/{id}/logs?last=1h&limit=1000` → log rows (also `tail`, `name`, `start`, `end`)
86
+ - `GET /v1/sources/{id}/events?last=1h&limit=1000` → `[{ timestamp_ms, metric, value, tags }]`, the `class: "event"` points (also `tail`, `name`, `start`, `end`). Plexus has no separate log type. `/logs` is an old alias for the same route; use `/events`.
82
87
  - `GET /v1/fleet/health` → `{ sources_total, sources_online }`
83
88
  - `GET /v1/fleet/metrics?metric=X&last=1h` → `{ metric, interval, sources_online, sources_w_metric, sources: [...], truncated }`
84
89
 
@@ -117,10 +122,10 @@ const points = s.timestamp_ms.map((t, i) => ({ t, v: s.avg[i] }));
117
122
  ### Live (data API, not the gateway)
118
123
 
119
124
  ```
120
- WS wss://plexus-data-api.fly.dev/v1/sources/{source_id}/metrics/stream?metrics=a,b
125
+ WS wss://api.plexus.company/v1/sources/{source_id}/metrics/stream?metrics=a,b
121
126
  ```
122
127
 
123
- Also `/logs/stream` and `/video/stream` under the same source prefix.
128
+ Also `/events/stream` (event points) and `/video/stream` under the same source prefix.
124
129
 
125
130
  **Auth is the first message, not a header.** Immediately after connect, send:
126
131
 
@@ -202,26 +207,26 @@ let the pipeline settle first.
202
207
 
203
208
  - **Polling cadences for dashboards**: latest values 5s, charts 10s, fleet health 10s, source list 30s. Use SWR or TanStack Query with `refreshInterval`.
204
209
  - **Time ranges**: prefer `last=1h` (relative) over `start`/`end` (absolute) — easier to reason about and less timezone footgun. `start`/`end` are ISO date-times, not epoch ms.
205
- - **Batching ingest**: the gateway meters **messages, not points** — 500/s per WebSocket connection, 2000/s per source, up to 10,000 points in one message. Buffer up to 64 points or 5 seconds, whichever first. One send per reading at bench rates exceeds the limit and the overflow is *discarded*, reported asynchronously as `RATE_LIMITED` after the send returned. In plexus-python use `px.batch(interval_ms=50)`; `px.send()` alone is one message per call. On 429 / 5xx, exponential backoff with max 3 attempts.
210
+ - **Batching ingest**: the gateway meters **messages, not points** — 2,000/s per WebSocket connection and per source (bursts up to 500), up to 10,000 points in one message. Buffer up to 64 points or 5 seconds, whichever first. One send per reading at bench rates exceeds the limit and the overflow is *discarded*, reported asynchronously as `RATE_LIMITED` after the send returned. In plexus-python use `px.batch(interval_ms=50)`; `px.send()` alone is one message per call. On 429 / 5xx, exponential backoff with max 3 attempts.
206
211
  - **Source IDs are slugs**: `drone-001`, `sat-alpha-3`, `bench.rig-2`. Must match `^[a-z0-9][a-z0-9._-]*$`, up to 256 characters — dots are legal and a single character is legal. A uuid-shaped slug is rejected: every resolver reads uuid-shaped refs as internal ids, so such a source would be unreachable. Stable, lowercase.
207
212
  - **Source IDs are not deduplicated.** The gateway writes whatever `source_id` you declare. Two devices declaring the same name merge into one source.
208
213
  - **Metric names are opaque and may be long.** Anything that round-trips a name must use the identical string on both sides or the series and its metadata will not join.
209
214
 
210
215
  ## Common pitfalls
211
216
 
212
- - **`points`, not `metrics`, on ingest**, and every point needs `class`. The two most common 400s.
217
+ - **`points`, not `metrics`, on ingest.** The most common 400. Set `class` on every point too: HTTP infers it when missing, the WebSocket does not, and `class: "metric"` with a non-numeric value is a 400.
213
218
  - **Numeric timestamps only.** ISO strings 400. Milliseconds unless the value is under `1e12`, in which case it's read as seconds.
214
219
  - **The query response is columnar.** `series[m].avg[i]`, not `series[m][i].v`. Reaching for `.t`/`.v` yields `undefined` and an empty chart with no error.
215
220
  - **The live stream is on the data API, and auths by first message.** There is no `/v1/stream` on the gateway.
216
221
  - **Telemetry frames are batched** — `points` is an array. Handling one frame as one point silently drops data.
217
- - `start`/`end` are **ISO date-times on every endpoint** that takes them — `query`, `logs` and `fleet/metrics` alike. `last=1h` is easier and works on all three.
222
+ - `start`/`end` are **ISO date-times on every endpoint** that takes them — `query`, `events` and `fleet/metrics` alike. `last=1h` is easier and works on all three.
218
223
  - `auto_downsampled: true` in a query response means the bucket size was picked for you — surface it in the UI so users understand what they're looking at.
219
224
 
220
225
  ## When unsure
221
226
 
222
- Fetch `https://plexus-data-api.fly.dev/openapi.json` for the authoritative HTTP schema (it will not show WebSocket routes), and check a real response before writing parsing code. `scripts/verify_skills.py` in this repo checks these docs against the live spec — run it if something here looks stale.
227
+ Fetch `https://api.plexus.company/openapi.json` for the authoritative HTTP schema (it will not show WebSocket routes), and check a real response before writing parsing code. `scripts/verify_skills.py` in this repo checks these docs against the live spec — run it if something here looks stale.
223
228
 
224
229
  This cheat sheet has drifted before. Corrected 2026-09-01 against the shipped
225
230
  gateway and app (runs API — which an earlier revision of API.md wrongly said did
226
231
  not exist; message-not-point metering and `px.batch()`; the real slug rule,
227
- which permits dots, single characters and 256 bytes). Corrected 2026-08-27 against Data API 0.1.0 (ingest array name, `sources`/`devices` paths, removed per-source health, columnar query) and again 2026-08-28 against gateway + API source (the live-stream host/path/auth/frame shape, the required `class` field, numeric-only timestamps, the redirect chain, and the now-fixed 1970 timestamp behavior).
232
+ which permits dots, single characters and 256 bytes). Corrected 2026-08-27 against Data API 0.1.0 (ingest array name, `sources`/`devices` paths, removed per-source health, columnar query) and again 2026-08-28 against gateway + API source (the live-stream host/path/auth/frame shape, the required `class` field, numeric-only timestamps, the redirect chain, and the now-fixed 1970 timestamp behavior). Corrected 2026-09-22: the read API host is `api.plexus.company`; `class` is inferred on HTTP `/ingest` (not a 400) but required on the WebSocket; key binding to one source; Free-plan WebSocket refusal; event size limits; no log upload.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plexus-dashboard
3
- description: Scaffold a working web dashboard against the Plexus telemetry API — device picker, latest-value tiles, time-series charts, log viewer. Use when the user wants to build a frontend, dashboard, ops UI, mission control, or fleet view on top of Plexus data. Triggering phrases include "show me my drones/satellites/robots", "build a dashboard for my fleet", "vibe code a Plexus frontend", "fleet monitoring UI", "telemetry dashboard", "device status page", "live charts of sensor data", or "ops view for my hardware" — even if "Plexus" is never said, as long as the data source is the Plexus API.
3
+ description: Scaffold a working web dashboard against the Plexus telemetry API — device picker, latest-value tiles, time-series charts, event viewer. Use when the user wants to build a frontend, dashboard, ops UI, mission control, or fleet view on top of Plexus data. Triggering phrases include "show me my drones/satellites/robots", "build a dashboard for my fleet", "vibe code a Plexus frontend", "fleet monitoring UI", "telemetry dashboard", "device status page", "live charts of sensor data", or "ops view for my hardware" — even if "Plexus" is never said, as long as the data source is the Plexus API.
4
4
  tools: Read, Write, Edit, Bash, WebFetch
5
5
  ---
6
6
 
@@ -37,7 +37,7 @@ Dashboard
37
37
  └─ Detail pane
38
38
  ├─ Tile row: every metric from /metrics/latest as a big-number card
39
39
  ├─ Chart grid: one line chart per metric over the last 1h via /metrics/query
40
- └─ (Optional) Log pane: /logs in a virtualized list
40
+ └─ (Optional) Event pane: /events (event points: faults, state changes, log lines) in a virtualized list
41
41
  ```
42
42
 
43
43
  ## Polling cadences
@@ -71,7 +71,7 @@ Ask exactly two questions, no more:
71
71
  Create `src/lib/plexus.ts`:
72
72
 
73
73
  ```ts
74
- const BASE = "https://plexus-data-api.fly.dev";
74
+ const BASE = "https://api.plexus.company";
75
75
  const KEY = process.env.NEXT_PUBLIC_PLEXUS_API_KEY!;
76
76
 
77
77
  export class PlexusError extends Error {
@@ -158,7 +158,7 @@ export function subscribeMetrics(
158
158
  ) {
159
159
  const qs = metrics.length ? `?metrics=${metrics.join(",")}` : "";
160
160
  const ws = new WebSocket(
161
- `wss://plexus-data-api.fly.dev/v1/sources/${sourceId}/metrics/stream${qs}`,
161
+ `wss://api.plexus.company/v1/sources/${sourceId}/metrics/stream${qs}`,
162
162
  );
163
163
 
164
164
  // Auth is the FIRST MESSAGE, not a header. Send it within 10s or the
@@ -199,6 +199,6 @@ Wire it alongside SWR: keep `useSWR` for initial load (UI hydrates with a value
199
199
 
200
200
  ## When unsure about endpoint shapes
201
201
 
202
- `https://plexus-data-api.fly.dev/openapi.json` is the source of truth for HTTP. It does **not** list WebSocket routes — the generic `plexus` skill documents those.
202
+ `https://api.plexus.company/openapi.json` is the source of truth for HTTP. It does **not** list WebSocket routes — the generic `plexus` skill documents those.
203
203
 
204
- Corrected 2026-08-28: `/v1/devices` → `/v1/sources`, removed the non-existent per-source health endpoint, fixed the columnar query type, and replaced the live-stream section (the old `wss://gateway.plexus.company/v1/stream` 404s, and the "browsers can't authenticate" caveat no longer holds).
204
+ Corrected 2026-08-28: `/v1/devices` → `/v1/sources`, removed the non-existent per-source health endpoint, fixed the columnar query type, and replaced the live-stream section (the old `wss://gateway.plexus.company/v1/stream` 404s, and the "browsers can't authenticate" caveat no longer holds). Corrected 2026-09-22: the read API host is `api.plexus.company`.
@@ -14,7 +14,7 @@ Adds a small, well-behaved Plexus ingest client to device-side code. Optimized f
14
14
  - Targets include: ESP32 / Arduino / RP2040, Raspberry Pi, NVIDIA Jetson, embedded Linux gateways, drone autopilot companion computers, satellite OBC software
15
15
  - Languages: C / C++, MicroPython, CPython, Rust, Go (for edge gateways)
16
16
 
17
- **If the target runs CPython (Pi, Jetson, any embedded Linux with room), stop and use the SDK instead** — `pip install plexus-python`, then `Plexus(api_key=..., source_id=...)`. It handles backoff, store-and-forward buffering and the WebSocket transport. Hand-rolling is for targets the SDK can't reach.
17
+ **If the target runs CPython (Pi, Jetson, any embedded Linux with room), stop and use the SDK instead** — `pip install plexus-python`, then `Plexus(api_key=..., source_id=...)`. It handles backoff, store-and-forward buffering and the WebSocket transport, and falls back to HTTP on its own (including on the Free plan, where the gateway refuses the device WebSocket). Hand-rolling is for targets the SDK can't reach.
18
18
 
19
19
  **`send()` does NOT batch.** Every call is its own message on the wire, and the ceiling below counts messages. Above a few readings a second, use `px.batch()`:
20
20
 
@@ -56,12 +56,14 @@ Response:
56
56
  { "success": true, "count": 2, "source_id": "drone-001" }
57
57
  ```
58
58
 
59
- Four things the gateway will reject you for. Get these right or nothing lands:
59
+ Three things the gateway will reject you for, and one to always set. Get these right or nothing lands:
60
60
 
61
61
  1. **The array is `points`.** `"metrics": [...]` returns `400 {"error":"'points' array is required"}`.
62
- 2. **Every point needs `class`**, either `"metric"` or `"event"`. There is no default.
63
- 3. **`timestamp` is a number, never a string.** An ISO-8601 string returns `points[i].timestamp must be a number`. Epoch **milliseconds**; a positive value below `1e12` is read as **seconds** and scaled for you, so either unit works as long as it's numeric.
64
- 4. **`source_id` must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars)** and is not deduplicated two devices declaring the same id merge into one source. This is what SD-card clones do when they all boot as `raspberrypi`.
62
+ 2. **`timestamp` is a number, never a string.** An ISO-8601 string returns `points[i].timestamp must be a number`. Epoch **milliseconds**; a positive value below `1e12` is read as **seconds** and scaled for you, so either unit works as long as it's numeric.
63
+ 3. **`source_id` must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars)** and is not deduplicated two devices declaring the same id merge into one source. This is what SD-card clones do when they all boot as `raspberrypi`.
64
+ 4. **Set `class` on every point**: `"metric"` for numbers, `"event"` for strings, bools, objects and arrays. HTTP `/ingest` infers it when missing (numbers → metric, anything else → event), but the WebSocket rejects a point without it, and `"class": "metric"` on a non-number is a 400. Set it explicitly.
65
+
66
+ Events are for faults, state changes and log lines. There is no log upload: send the lines that matter as `{"class":"event","metric":"log","value":{"level":"error","msg":"..."}}`. Limits: a string value up to 256 bytes, an object/array value up to 4,096 bytes of JSON, up to 16 tags.
65
67
 
66
68
  `timestamp` is optional. Omit it and the gateway stamps the point with its receive time — which is the right move on a device whose clock has never been NTP-synced. Per-point `tags` (a flat string→string map) are supported and optional.
67
69
 
@@ -77,12 +79,12 @@ decides whether you hit a limit:
77
79
 
78
80
  | Limit | Value |
79
81
  | --- | --- |
80
- | Telemetry messages per WebSocket connection | 500/s sustained, 2000 burst |
81
- | Hard ceiling per source (WS and HTTP) | 2000 messages/s |
82
+ | Telemetry messages per WebSocket connection | 2,000/s |
83
+ | Hard ceiling per source (WS and HTTP) | 2,000 messages/s, bursts up to 500 |
82
84
  | Points per message | 10,000 |
83
85
  | Body size | 1 MB (WS) / 5 MB (HTTP) |
84
86
 
85
- Eight channels at 100 Hz sent one at a time is 800 messages/s — over the limit,
87
+ 25 channels at 100 Hz sent one at a time is 2,500 messages/s — over the limit,
86
88
  and **the overflow is discarded**. Batched every 50 ms it is 20 messages/s.
87
89
 
88
90
  Over the limit the gateway drops the whole message and answers `RATE_LIMITED`,
@@ -118,6 +120,7 @@ The `source_id` must be stable across reboots **and unique across the fleet**. U
118
120
  - ESP32 / Arduino: NVS / preferences storage, **not** hardcoded in firmware
119
121
  - Raspberry Pi / Linux: `/etc/plexus/key` with mode 0600, or env var
120
122
  - Never compile keys into a binary that ships to multiple devices — one leak compromises the fleet
123
+ - For devices in customer hands, give each one its own key made at `https://app.plexus.company/api` with **Limit to device slug** set to its `source_id`. The gateway refuses that key for any other source (`403`), so a key pulled off one unit cannot write as the rest of the fleet
121
124
 
122
125
  ### 5. Bound memory
123
126
 
@@ -142,7 +145,7 @@ _lock = _thread.allocate_lock()
142
145
  def emit(metric, value):
143
146
  """Called from sensor-read context. Cheap — just appends to a buffer."""
144
147
  with _lock:
145
- # class is REQUIRED. timestamp must be a NUMBER (epoch ms) — omit it
148
+ # Always set class. timestamp must be a NUMBER (epoch ms) — omit it
146
149
  # entirely if this board has never NTP-synced and the gateway will
147
150
  # stamp it on receive.
148
151
  _buf.append({
@@ -203,10 +206,12 @@ Use the SDK — `pip install plexus-python` — unless there's a reason not to:
203
206
  from plexus import Plexus
204
207
 
205
208
  px = Plexus(api_key=os.environ["PLEXUS_API_KEY"], source_id="pi-fieldunit-03")
206
- px.send("battery.voltage", 11.8) # class=metric, batched for you
209
+ px.send("battery.voltage", 11.8) # class=metric, one message per call
207
210
  px.event("fault", "undervoltage lockout") # class=event
208
211
  ```
209
212
 
213
+ `send()` does not batch. Above a few readings a second, use `with px.batch(interval_ms=50) as b:` as shown at the top.
214
+
210
215
  If you must hand-roll: `requests` with a `Session` for connection pooling, same batching + backoff rules, key from `os.environ["PLEXUS_API_KEY"]`, flush on a background thread on a 5-second tick.
211
216
 
212
217
  ### C (ESP-IDF / generic embedded)
@@ -247,11 +252,13 @@ If they see `{"success":true,"count":1,"source_id":"test-laptop"}`, auth + conne
247
252
 
248
253
  ```bash
249
254
  curl -H "x-api-key: $PLEXUS_API_KEY" \
250
- https://plexus-data-api.fly.dev/v1/sources/test-laptop/metrics/latest
255
+ https://api.plexus.company/v1/sources/test-laptop/metrics/latest
251
256
  ```
252
257
 
253
258
  ## When unsure
254
259
 
255
- The ingest contract above is the authority for the device side. For reading data back, fetch `https://plexus-data-api.fly.dev/openapi.json` or use the generic `plexus` skill.
260
+ The ingest contract above is the authority for the device side. For reading data back, fetch `https://api.plexus.company/openapi.json` or use the generic `plexus` skill.
256
261
 
257
262
  Corrected 2026-08-28 against gateway source (`ingest.go`, `validate.go`): the array is `points` not `metrics`, `class` is required, timestamps must be numeric, and the response is `{success, count, source_id}`. Every template in the previous version of this file would have 400'd.
263
+
264
+ Corrected 2026-09-22: `class` is inferred on HTTP `/ingest` when missing (it was documented as a 400) but is required on the WebSocket, so set it anyway; per-device keys via "Limit to device slug"; the read API host is `api.plexus.company`; `px.send()` is one message per call, not batched.
plexus/batching.py CHANGED
@@ -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
 
plexus/client.py CHANGED
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plexus-python
3
- Version: 0.11.5
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:
@@ -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.
@@ -0,0 +1,19 @@
1
+ plexus/__init__.py,sha256=71KGed-vePAvaK-OaC6gPUX4JDY_OUbtxwbvvk8EKv8,808
2
+ plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
3
+ plexus/batching.py,sha256=s0ipaQTMPIW9d6VOwSLpmMSvbYF1o5WIJ06jlFezVfk,10432
4
+ plexus/buffer.py,sha256=UNv_jEcrDwbkjJ6uhCehb7uBI2EuFEwO40waDpZn_5I,9579
5
+ plexus/cli.py,sha256=YFkptze8LRc6mBmDgbsTeU8J6i8MMsdTWvtm3jCINiI,21773
6
+ plexus/client.py,sha256=agPH8_nBdfL4SA6OUKJcP8RnBNHSTSm2aqt2GW95UGo,53834
7
+ plexus/config.py,sha256=RuDh5UdVGdVQld5kQlXZO6CVXO4tS0HBalyaoAlXNvc,4416
8
+ plexus/ws.py,sha256=xQhJCOizX-V34tF3-r7Zpzs_wnBbNSCeEoZeFFXWH7E,19763
9
+ plexus/cameras/__init__.py,sha256=AVu1vE1xfYk9lz1cprHlTfi0ieHf84UhQh9Z92E05b8,490
10
+ plexus/cameras/thermal.py,sha256=-klCEJQG5GlLtELaowWOPYomwMsLK8O5pmmX0PqUrTA,12022
11
+ plexus/_skills/README.md,sha256=yOJCcwIjzwk3g6EJkzkkTaSU7l1axOaqHj0UYRyzdg4,3306
12
+ plexus/_skills/plexus/SKILL.md,sha256=E-csIYsMMiipqK-5pYWRgFtxzY6MZBPfNgXmrtpBRRo,15088
13
+ plexus/_skills/plexus-dashboard/SKILL.md,sha256=qJJVsFbkZe1LqnsfaNPxMFS1BWqm_VqztLv33g7tZ3M,9613
14
+ plexus/_skills/plexus-firmware/SKILL.md,sha256=jx9noI4P6mY9pXMKLNBIw6tVuAVipeU_w2L1vwXBdRM,13420
15
+ plexus_python-0.11.6.dist-info/METADATA,sha256=37ryJgYdD1rz4CyQbQyGkjbVgtQtTBATBOwrHHb5vhY,15667
16
+ plexus_python-0.11.6.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
17
+ plexus_python-0.11.6.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
18
+ plexus_python-0.11.6.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
19
+ plexus_python-0.11.6.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- plexus/__init__.py,sha256=m5CEi_FQT7_gW36vrWebsu0F1i8xwvmVu6w6580i8lc,808
2
- plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
3
- plexus/batching.py,sha256=mPMa3m9xK-DCwRmuyL_aqaTltimIOtwoDzz4PfKWyh8,10452
4
- plexus/buffer.py,sha256=UNv_jEcrDwbkjJ6uhCehb7uBI2EuFEwO40waDpZn_5I,9579
5
- plexus/cli.py,sha256=YFkptze8LRc6mBmDgbsTeU8J6i8MMsdTWvtm3jCINiI,21773
6
- plexus/client.py,sha256=H69DR30pj3X8PiURZsViGTDHsIsQ2Kn4muHNECsioKw,53865
7
- plexus/config.py,sha256=RuDh5UdVGdVQld5kQlXZO6CVXO4tS0HBalyaoAlXNvc,4416
8
- plexus/ws.py,sha256=xQhJCOizX-V34tF3-r7Zpzs_wnBbNSCeEoZeFFXWH7E,19763
9
- plexus/cameras/__init__.py,sha256=AVu1vE1xfYk9lz1cprHlTfi0ieHf84UhQh9Z92E05b8,490
10
- plexus/cameras/thermal.py,sha256=-klCEJQG5GlLtELaowWOPYomwMsLK8O5pmmX0PqUrTA,12022
11
- plexus/_skills/README.md,sha256=9TNo9mssmBiBPkrYBiA2JJHWOIAEw6DvCWCYAvfzmjM,3243
12
- plexus/_skills/plexus/SKILL.md,sha256=imC6CuWSItyN3j_5YSxfhvMTSxz3Qo5xBN2ewlO4Fdg,13459
13
- plexus/_skills/plexus-dashboard/SKILL.md,sha256=8xjdvI3orM31XHTLv1fSAWstCGMwt1HnEbo0SlpTxV4,9508
14
- plexus/_skills/plexus-firmware/SKILL.md,sha256=C5tOGu0_LOb76n8US2SLpWSdZdeHQIa4e_5fvkX4DGU,12093
15
- plexus_python-0.11.5.dist-info/METADATA,sha256=-iTng_ciXZM_KAUTE-9Yb8XfLyFYLRkxmzslRnS7Ve4,13784
16
- plexus_python-0.11.5.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
17
- plexus_python-0.11.5.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
18
- plexus_python-0.11.5.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
19
- plexus_python-0.11.5.dist-info/RECORD,,