plexus-python 0.4.9__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {plexus_python-0.4.9 → plexus_python-0.5.2}/.github/workflows/ci.yml +1 -1
  2. {plexus_python-0.4.9 → plexus_python-0.5.2}/CHANGELOG.md +82 -0
  3. {plexus_python-0.4.9 → plexus_python-0.5.2}/PKG-INFO +91 -19
  4. {plexus_python-0.4.9 → plexus_python-0.5.2}/README.md +87 -13
  5. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/uv.lock +3 -3
  6. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/__init__.py +1 -1
  7. plexus_python-0.5.2/plexus/_log.py +15 -0
  8. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/client.py +105 -44
  9. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/ws.py +87 -19
  10. {plexus_python-0.4.9 → plexus_python-0.5.2}/pyproject.toml +4 -6
  11. plexus_python-0.5.2/skills/plexus/SKILL.md +189 -0
  12. plexus_python-0.5.2/skills/plexus/references/api.md +331 -0
  13. plexus_python-0.5.2/skills/plexus/references/sdk.md +227 -0
  14. plexus_python-0.5.2/tests/test_basic.py +114 -0
  15. {plexus_python-0.4.9 → plexus_python-0.5.2}/tests/test_retry.py +12 -11
  16. plexus_python-0.5.2/uv.lock +503 -0
  17. plexus_python-0.4.9/tests/test_basic.py +0 -61
  18. plexus_python-0.4.9/uv.lock +0 -1061
  19. {plexus_python-0.4.9 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {plexus_python-0.4.9 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {plexus_python-0.4.9 → plexus_python-0.5.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {plexus_python-0.4.9 → plexus_python-0.5.2}/.github/workflows/publish.yml +0 -0
  23. {plexus_python-0.4.9 → plexus_python-0.5.2}/.gitignore +0 -0
  24. {plexus_python-0.4.9 → plexus_python-0.5.2}/AGENTS.md +0 -0
  25. {plexus_python-0.4.9 → plexus_python-0.5.2}/API.md +0 -0
  26. {plexus_python-0.4.9 → plexus_python-0.5.2}/CODE_OF_CONDUCT.md +0 -0
  27. {plexus_python-0.4.9 → plexus_python-0.5.2}/CONTRIBUTING.md +0 -0
  28. {plexus_python-0.4.9 → plexus_python-0.5.2}/LICENSE +0 -0
  29. {plexus_python-0.4.9 → plexus_python-0.5.2}/SECURITY.md +0 -0
  30. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/.python-version +0 -0
  31. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/README.md +0 -0
  32. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/basic.py +0 -0
  33. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/can.py +0 -0
  34. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/i2c_bme280.py +0 -0
  35. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/mac_metrics.py +0 -0
  36. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/mavlink.py +0 -0
  37. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/mqtt.py +0 -0
  38. {plexus_python-0.4.9 → plexus_python-0.5.2}/examples/pyproject.toml +0 -0
  39. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/buffer.py +0 -0
  40. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/cli.py +0 -0
  41. {plexus_python-0.4.9 → plexus_python-0.5.2}/plexus/config.py +0 -0
  42. {plexus_python-0.4.9 → plexus_python-0.5.2}/scripts/plexus.service +0 -0
  43. {plexus_python-0.4.9 → plexus_python-0.5.2}/scripts/release.sh +0 -0
  44. {plexus_python-0.4.9 → plexus_python-0.5.2}/scripts/scan_buses.py +0 -0
  45. {plexus_python-0.4.9 → plexus_python-0.5.2}/scripts/setup.sh +0 -0
  46. {plexus_python-0.4.9 → plexus_python-0.5.2}/tests/test_buffer.py +0 -0
  47. {plexus_python-0.4.9 → plexus_python-0.5.2}/tests/test_config.py +0 -0
  48. {plexus_python-0.4.9 → plexus_python-0.5.2}/tests/test_video.py +0 -0
  49. {plexus_python-0.4.9 → plexus_python-0.5.2}/tests/test_ws.py +0 -0
@@ -31,7 +31,7 @@ jobs:
31
31
  runs-on: ubuntu-latest
32
32
  strategy:
33
33
  matrix:
34
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
34
+ python-version: ['3.10', '3.11', '3.12', '3.13']
35
35
 
36
36
  steps:
37
37
  - uses: actions/checkout@v4
@@ -1,5 +1,87 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.2] - 2026-05-21 - DX hardening for hardware engineers
4
+
5
+ ### Fixed
6
+
7
+ - Error messages now reference `plexus init` instead of the non-existent `plexus start`.
8
+ - `close()` now attempts to flush any buffered points before tearing down the transport,
9
+ preventing silent data loss on graceful shutdown.
10
+ - `persistent_buffer` default changed from `False` to `True` — store-and-forward is now
11
+ on by default, matching the `~/.plexus/config.json` default and the right choice for
12
+ field hardware. Pass `persistent_buffer=False` to opt out (e.g. in test fixtures).
13
+
14
+ ### Added
15
+
16
+ - `send_batch()` now accepts 3-tuples `(metric, value, timestamp)` alongside the existing
17
+ 2-tuple form. Per-point timestamps let sensors on different interrupt timers share a
18
+ single batch call. 2-tuples continue to use the shared `timestamp` argument.
19
+ - `on_command()` now warns immediately via stderr if called after the WebSocket has already
20
+ authenticated, making the "register before first send()" ordering requirement visible
21
+ rather than silently broken.
22
+ - `source_id` is validated against `^[a-z0-9][a-z0-9_-]{1,62}$` at construction time.
23
+ Invalid names now raise `ValueError` with a clear message instead of failing obscurely
24
+ at the gateway.
25
+
26
+ ### Changed
27
+
28
+ - `_say()` / `_QUIET` consolidated into a new internal `plexus/_log.py` module.
29
+ Previously duplicated verbatim between `client.py` and `ws.py`.
30
+
31
+ ## [0.5.1] - 2026-05-19 - Binary video frames + non-blocking send
32
+
33
+ ### Performance
34
+
35
+ - `send_video_frame()` now sends a compact binary WebSocket frame instead of
36
+ JSON+base64. The binary header encodes source_id, camera_id, width, height,
37
+ and timestamp_ms; the JPEG payload follows raw. Eliminates the 33% base64
38
+ wire overhead, reducing per-frame bandwidth by ~25% and raising the
39
+ sustainable FPS ceiling from ~15–20 fps to ~20–25 fps at 1280×720 quality 85.
40
+ - Gateway decodes the binary header and re-encodes as JSON+base64 before
41
+ relaying to browsers — no changes required in the frontend, data_api, or any
42
+ other consumer.
43
+
44
+ ### Reliability
45
+
46
+ - `send_video_frame()` is now non-blocking. Frames are placed into a
47
+ `queue.Queue(maxsize=2)` drained by a dedicated `plexus-video` daemon thread.
48
+ When the queue is full (sender backlogged) frames are dropped rather than
49
+ blocking the capture pipeline, preventing deadlocks at any FPS.
50
+ - `stop()` / `close()` now exits cleanly within 0.5 s regardless of in-flight
51
+ sends. Previously a slow or hung WebSocket write could stall shutdown
52
+ indefinitely.
53
+
54
+ ### Changed
55
+
56
+ - Removed `import base64` from `client.py` (no longer needed on the send path).
57
+ - `send_video_frame()` calls `ws.send_video_frame_async()` instead of the
58
+ internal `ws._send_frame()`.
59
+
60
+ ### Wire protocol
61
+
62
+ - Gateway handles both binary frames (SDK ≥ 0.5.1) and legacy JSON text frames
63
+ transparently — older SDKs continue to work unchanged.
64
+
65
+ ## [0.5.0] - 2026-05-19 - Security hardening, dep cleanup, Python 3.10+ only
66
+
67
+ ### Security
68
+
69
+ - Removed `requests` (and its transitive deps `urllib3`, `idna`) entirely —
70
+ replaced with stdlib `urllib.request`. Closes 6 Dependabot alerts (#6, #9,
71
+ #10, #11, #12, #13, #19) by eliminating the vulnerability surface rather than
72
+ patching it.
73
+ - Bumped `Pillow>=12.2.0` (fixes #14, #15, #16, #17, #18, #20 — OOB write,
74
+ FITS decompression bomb, font integer overflow, PDF parsing DoS, and related
75
+ CVEs).
76
+ - Bumped `pytest>=9.0.3` in dev deps (fixes #7).
77
+
78
+ ### Changed
79
+
80
+ - Dropped Python 3.8 and 3.9 support — both are past EOL and the patched
81
+ versions of Pillow and pytest all require `>=3.10`. `requires-python` is now
82
+ `>=3.10`.
83
+ - CI matrix: removed 3.8/3.9 runners, added 3.13.
84
+
3
85
  ## [0.4.9] - 2026-05-19 - Video input broadening and wire safety
4
86
 
5
87
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.4.9
3
+ Version: 0.5.2
4
4
  Summary: Thin Python SDK for Plexus — send telemetry in one line
5
5
  Project-URL: Homepage, https://plexus.dev
6
6
  Project-URL: Documentation, https://docs.plexus.dev
@@ -15,22 +15,20 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: Apache Software License
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Topic :: Scientific/Engineering
23
22
  Classifier: Topic :: System :: Hardware
24
- Requires-Python: >=3.9
25
- Requires-Dist: requests>=2.32.4
23
+ Requires-Python: >=3.10
26
24
  Requires-Dist: websocket-client>=1.7
27
25
  Provides-Extra: dev
28
26
  Requires-Dist: pytest-cov; extra == 'dev'
29
- Requires-Dist: pytest>=8.3.5; extra == 'dev'
27
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
30
28
  Requires-Dist: ruff; extra == 'dev'
31
29
  Requires-Dist: websockets>=12; extra == 'dev'
32
30
  Provides-Extra: video
33
- Requires-Dist: pillow>=11.2.1; extra == 'video'
31
+ Requires-Dist: pillow>=12.2.0; extra == 'video'
34
32
  Description-Content-Type: text/markdown
35
33
 
36
34
  # plexus-python
@@ -70,34 +68,108 @@ The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `setup.sh` refuses to run with
70
68
 
71
69
  In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
72
70
 
73
- ## Usage
71
+ ## Core methods
74
72
 
75
- ```python
76
- from plexus import Plexus
73
+ ### `send(metric, value)` — stream a reading
74
+
75
+ The main method. Call it every time you have a new sensor reading.
77
76
 
77
+ ```python
78
78
  px = Plexus(source_id="rig-01") # reads PLEXUS_API_KEY from env
79
79
 
80
- # Numbers
81
80
  px.send("engine.rpm", 3450)
82
- px.send("coolant.temperature", 82.3, tags={"unit": "C"})
81
+ px.send("coolant.temp", 82.3)
82
+ ```
83
83
 
84
- # Strings, bools, objects, arrays all JSON-serializable
85
- px.send("vehicle.state", "RUNNING")
86
- px.send("motor.enabled", True)
87
- px.send("position", {"x": 1.5, "y": 2.3, "z": 0.8})
84
+ `metric` is a dot-namespaced string (`"motor.rpm"`, `"gps.fix_quality"`). `value` accepts any JSON-serializable type:
88
85
 
89
- # Batch
86
+ | Type | Example | When to use |
87
+ |------|---------|-------------|
88
+ | `float` / `int` | `72.5`, `3450` | Sensor readings, counters |
89
+ | `str` | `"RUNNING"`, `"E_STALL"` | State machines, error codes |
90
+ | `bool` | `True` | Binary flags |
91
+ | `dict` | `{"x": 1.5, "y": 2.3}` | Vectors, structured readings |
92
+ | `list` | `[0.5, 1.2, -0.3]` | Waveforms, joint angles |
93
+
94
+ Optional arguments:
95
+ - `tags={"motor_id": "A1"}` — key-value labels for filtering in the dashboard
96
+ - `timestamp=t` — explicit Unix timestamp in seconds; omit to let the SDK pick (see [Timestamps](#timestamps-and-clock-correction))
97
+
98
+ ### `send_batch(points)` — send multiple readings at once
99
+
100
+ Use this when you sample several sensors together and want them to share a timestamp and land in one network call.
101
+
102
+ ```python
90
103
  px.send_batch([
91
- ("temperature", 72.5),
92
- ("pressure", 1013.25),
104
+ ("temperature", 22.4),
105
+ ("humidity", 58.1),
106
+ ("pressure", 1013.2),
93
107
  ])
108
+ ```
109
+
110
+ `points` is a list of `(metric, value)` tuples. All points share the same timestamp (now, unless you pass `timestamp=t`). For independent timestamps per point, call `send()` in a loop instead.
111
+
112
+ ### `event(name, data)` — record a discrete occurrence
113
+
114
+ Use `event()` for things that *happen* rather than things you *measure continuously*. Faults, state transitions, operator actions, log entries — anything you'd put on a timeline as a marker rather than plot as a graph.
115
+
116
+ ```python
117
+ px.event("fault", "E-stop triggered")
118
+ px.event("state_change", {"from": "IDLE", "to": "RUNNING"})
119
+ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
120
+ ```
121
+
122
+ The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
123
+
124
+ ### `run(run_id)` — group data into a named recording
94
125
 
95
- # Named run for grouping related data
126
+ ```python
96
127
  with px.run("thermal-cycle-001"):
97
128
  while running:
98
129
  px.send("temperature", read_temp())
99
130
  ```
100
131
 
132
+ All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
133
+
134
+ ## Video streaming
135
+
136
+ Two methods depending on whether you control the capture loop or just have a URL.
137
+
138
+ ### `send_video_frame(frame, camera_id)` — send frames you capture yourself
139
+
140
+ 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.
141
+
142
+ ```python
143
+ import cv2
144
+
145
+ cap = cv2.VideoCapture(0)
146
+ while True:
147
+ ok, frame = cap.read()
148
+ if ok:
149
+ px.send_video_frame(frame, camera_id="front")
150
+ ```
151
+
152
+ Accepted frame types:
153
+ - **numpy ndarray** (H × W × C) — from OpenCV or picamera2; requires `opencv-python`
154
+ - **JPEG bytes** — passed through as-is, zero re-encode overhead
155
+ - **Other image bytes** (PNG, BMP, WebP) — decoded and re-encoded as JPEG via Pillow; requires `pip install plexus-python[video]`
156
+
157
+ `camera_id` identifies which camera the frame came from. Use distinct IDs when streaming from multiple cameras simultaneously (`"front"`, `"rear"`, `"cam:0"`).
158
+
159
+ ### `stream_camera(url, camera_id)` — stream from an RTSP URL or file
160
+
161
+ Use this when you have an RTSP stream or video file and don't want to manage the capture loop yourself. The SDK runs FFmpeg internally and handles the rest. Requires FFmpeg on `$PATH`.
162
+
163
+ ```python
164
+ stop = px.stream_camera("rtsp://192.168.1.100/stream", camera_id="front")
165
+ # ... do other work ...
166
+ stop.set() # stop streaming
167
+ ```
168
+
169
+ Returns a `threading.Event` — call `.set()` to stop. Runs in a background thread so it doesn't block your main loop.
170
+
171
+ **Which to use:** if you're piping from `rpicam-vid`, `picamera2`, or your own capture process, use `send_video_frame()`. If you have an RTSP URL or file path, use `stream_camera()`.
172
+
101
173
  ## Bring Your Own Protocol
102
174
 
103
175
  This package ships no adapters, auto-detection, or daemons — just the client. Use whatever library you'd use anyway and pipe values into `px.send()`.
@@ -35,34 +35,108 @@ The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `setup.sh` refuses to run with
35
35
 
36
36
  In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
37
37
 
38
- ## Usage
38
+ ## Core methods
39
39
 
40
- ```python
41
- from plexus import Plexus
40
+ ### `send(metric, value)` — stream a reading
41
+
42
+ The main method. Call it every time you have a new sensor reading.
42
43
 
44
+ ```python
43
45
  px = Plexus(source_id="rig-01") # reads PLEXUS_API_KEY from env
44
46
 
45
- # Numbers
46
47
  px.send("engine.rpm", 3450)
47
- px.send("coolant.temperature", 82.3, tags={"unit": "C"})
48
+ px.send("coolant.temp", 82.3)
49
+ ```
48
50
 
49
- # Strings, bools, objects, arrays all JSON-serializable
50
- px.send("vehicle.state", "RUNNING")
51
- px.send("motor.enabled", True)
52
- px.send("position", {"x": 1.5, "y": 2.3, "z": 0.8})
51
+ `metric` is a dot-namespaced string (`"motor.rpm"`, `"gps.fix_quality"`). `value` accepts any JSON-serializable type:
53
52
 
54
- # Batch
53
+ | Type | Example | When to use |
54
+ |------|---------|-------------|
55
+ | `float` / `int` | `72.5`, `3450` | Sensor readings, counters |
56
+ | `str` | `"RUNNING"`, `"E_STALL"` | State machines, error codes |
57
+ | `bool` | `True` | Binary flags |
58
+ | `dict` | `{"x": 1.5, "y": 2.3}` | Vectors, structured readings |
59
+ | `list` | `[0.5, 1.2, -0.3]` | Waveforms, joint angles |
60
+
61
+ Optional arguments:
62
+ - `tags={"motor_id": "A1"}` — key-value labels for filtering in the dashboard
63
+ - `timestamp=t` — explicit Unix timestamp in seconds; omit to let the SDK pick (see [Timestamps](#timestamps-and-clock-correction))
64
+
65
+ ### `send_batch(points)` — send multiple readings at once
66
+
67
+ Use this when you sample several sensors together and want them to share a timestamp and land in one network call.
68
+
69
+ ```python
55
70
  px.send_batch([
56
- ("temperature", 72.5),
57
- ("pressure", 1013.25),
71
+ ("temperature", 22.4),
72
+ ("humidity", 58.1),
73
+ ("pressure", 1013.2),
58
74
  ])
75
+ ```
76
+
77
+ `points` is a list of `(metric, value)` tuples. All points share the same timestamp (now, unless you pass `timestamp=t`). For independent timestamps per point, call `send()` in a loop instead.
78
+
79
+ ### `event(name, data)` — record a discrete occurrence
80
+
81
+ Use `event()` for things that *happen* rather than things you *measure continuously*. Faults, state transitions, operator actions, log entries — anything you'd put on a timeline as a marker rather than plot as a graph.
82
+
83
+ ```python
84
+ px.event("fault", "E-stop triggered")
85
+ px.event("state_change", {"from": "IDLE", "to": "RUNNING"})
86
+ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
87
+ ```
88
+
89
+ The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
90
+
91
+ ### `run(run_id)` — group data into a named recording
59
92
 
60
- # Named run for grouping related data
93
+ ```python
61
94
  with px.run("thermal-cycle-001"):
62
95
  while running:
63
96
  px.send("temperature", read_temp())
64
97
  ```
65
98
 
99
+ All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
100
+
101
+ ## Video streaming
102
+
103
+ Two methods depending on whether you control the capture loop or just have a URL.
104
+
105
+ ### `send_video_frame(frame, camera_id)` — send frames you capture yourself
106
+
107
+ 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.
108
+
109
+ ```python
110
+ import cv2
111
+
112
+ cap = cv2.VideoCapture(0)
113
+ while True:
114
+ ok, frame = cap.read()
115
+ if ok:
116
+ px.send_video_frame(frame, camera_id="front")
117
+ ```
118
+
119
+ Accepted frame types:
120
+ - **numpy ndarray** (H × W × C) — from OpenCV or picamera2; requires `opencv-python`
121
+ - **JPEG bytes** — passed through as-is, zero re-encode overhead
122
+ - **Other image bytes** (PNG, BMP, WebP) — decoded and re-encoded as JPEG via Pillow; requires `pip install plexus-python[video]`
123
+
124
+ `camera_id` identifies which camera the frame came from. Use distinct IDs when streaming from multiple cameras simultaneously (`"front"`, `"rear"`, `"cam:0"`).
125
+
126
+ ### `stream_camera(url, camera_id)` — stream from an RTSP URL or file
127
+
128
+ Use this when you have an RTSP stream or video file and don't want to manage the capture loop yourself. The SDK runs FFmpeg internally and handles the rest. Requires FFmpeg on `$PATH`.
129
+
130
+ ```python
131
+ stop = px.stream_camera("rtsp://192.168.1.100/stream", camera_id="front")
132
+ # ... do other work ...
133
+ stop.set() # stop streaming
134
+ ```
135
+
136
+ Returns a `threading.Event` — call `.set()` to stop. Runs in a background thread so it doesn't block your main loop.
137
+
138
+ **Which to use:** if you're piping from `rpicam-vid`, `picamera2`, or your own capture process, use `send_video_frame()`. If you have an RTSP URL or file path, use `stream_camera()`.
139
+
66
140
  ## Bring Your Own Protocol
67
141
 
68
142
  This package ships no adapters, auto-detection, or daemons — just the client. Use whatever library you'd use anyway and pipe values into `px.send()`.
@@ -674,11 +674,11 @@ wheels = [
674
674
 
675
675
  [[package]]
676
676
  name = "urllib3"
677
- version = "2.6.3"
677
+ version = "2.7.0"
678
678
  source = { registry = "https://pypi.org/simple" }
679
- sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
679
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
680
680
  wheels = [
681
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
681
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
682
682
  ]
683
683
 
684
684
  [[package]]
@@ -10,5 +10,5 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
10
10
  from plexus.client import Plexus, read_mjpeg_frames
11
11
  from plexus.ws import WebSocketTransport
12
12
 
13
- __version__ = "0.4.9"
13
+ __version__ = "0.5.2"
14
14
  __all__ = ["Plexus", "WebSocketTransport", "read_mjpeg_frames"]
@@ -0,0 +1,15 @@
1
+ import os
2
+ import sys
3
+
4
+ _QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
5
+
6
+
7
+ def _say(line: str) -> None:
8
+ """Single-line status message to stderr. Skipped if PLEXUS_QUIET=1."""
9
+ if _QUIET:
10
+ return
11
+ try:
12
+ sys.stderr.write(f"[plexus] {line}\n")
13
+ sys.stderr.flush()
14
+ except Exception:
15
+ pass