plexus-python 0.4.6__tar.gz → 0.4.8__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 (42) hide show
  1. {plexus_python-0.4.6 → plexus_python-0.4.8}/CHANGELOG.md +47 -0
  2. {plexus_python-0.4.6 → plexus_python-0.4.8}/PKG-INFO +3 -1
  3. plexus_python-0.4.8/examples/.python-version +1 -0
  4. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/README.md +1 -0
  5. plexus_python-0.4.8/examples/mac_metrics.py +81 -0
  6. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/mqtt.py +1 -1
  7. plexus_python-0.4.8/examples/pyproject.toml +15 -0
  8. plexus_python-0.4.8/examples/uv.lock +740 -0
  9. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/__init__.py +3 -3
  10. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/client.py +262 -1
  11. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/ws.py +0 -1
  12. {plexus_python-0.4.6 → plexus_python-0.4.8}/pyproject.toml +2 -1
  13. plexus_python-0.4.8/tests/test_video.py +208 -0
  14. {plexus_python-0.4.6 → plexus_python-0.4.8}/uv.lock +315 -2
  15. {plexus_python-0.4.6 → plexus_python-0.4.8}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {plexus_python-0.4.6 → plexus_python-0.4.8}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  17. {plexus_python-0.4.6 → plexus_python-0.4.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {plexus_python-0.4.6 → plexus_python-0.4.8}/.github/workflows/ci.yml +0 -0
  19. {plexus_python-0.4.6 → plexus_python-0.4.8}/.github/workflows/publish.yml +0 -0
  20. {plexus_python-0.4.6 → plexus_python-0.4.8}/.gitignore +0 -0
  21. {plexus_python-0.4.6 → plexus_python-0.4.8}/AGENTS.md +0 -0
  22. {plexus_python-0.4.6 → plexus_python-0.4.8}/API.md +0 -0
  23. {plexus_python-0.4.6 → plexus_python-0.4.8}/CODE_OF_CONDUCT.md +0 -0
  24. {plexus_python-0.4.6 → plexus_python-0.4.8}/CONTRIBUTING.md +0 -0
  25. {plexus_python-0.4.6 → plexus_python-0.4.8}/LICENSE +0 -0
  26. {plexus_python-0.4.6 → plexus_python-0.4.8}/README.md +0 -0
  27. {plexus_python-0.4.6 → plexus_python-0.4.8}/SECURITY.md +0 -0
  28. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/basic.py +0 -0
  29. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/can.py +0 -0
  30. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/i2c_bme280.py +0 -0
  31. {plexus_python-0.4.6 → plexus_python-0.4.8}/examples/mavlink.py +0 -0
  32. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/buffer.py +0 -0
  33. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/cli.py +0 -0
  34. {plexus_python-0.4.6 → plexus_python-0.4.8}/plexus/config.py +0 -0
  35. {plexus_python-0.4.6 → plexus_python-0.4.8}/scripts/plexus.service +0 -0
  36. {plexus_python-0.4.6 → plexus_python-0.4.8}/scripts/scan_buses.py +0 -0
  37. {plexus_python-0.4.6 → plexus_python-0.4.8}/scripts/setup.sh +0 -0
  38. {plexus_python-0.4.6 → plexus_python-0.4.8}/tests/test_basic.py +0 -0
  39. {plexus_python-0.4.6 → plexus_python-0.4.8}/tests/test_buffer.py +0 -0
  40. {plexus_python-0.4.6 → plexus_python-0.4.8}/tests/test_config.py +0 -0
  41. {plexus_python-0.4.6 → plexus_python-0.4.8}/tests/test_retry.py +0 -0
  42. {plexus_python-0.4.6 → plexus_python-0.4.8}/tests/test_ws.py +0 -0
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.8] - 2026-05-19 - Video input broadening and wire safety
4
+
5
+ ### Added
6
+
7
+ - `send_video_frame` now accepts raw bytes/bytearray: JPEG bytes are passed
8
+ through without re-encoding (zero CPU cost on hardware that outputs JPEG
9
+ natively); other formats (PNG, BMP, WebP) are decoded via Pillow and
10
+ re-encoded as JPEG. Install `plexus-python[video]` for Pillow support.
11
+ - `stream_camera(url, camera_id, fps, quality)` — streams from any
12
+ FFmpeg-supported source (RTSP, video file, capture device). Requires FFmpeg
13
+ on `$PATH`. Returns a `threading.Event`; call `.set()` to stop.
14
+ - `read_mjpeg_frames(pipe)` — public generator that parses raw MJPEG byte
15
+ streams (e.g. FFmpeg stdout) into individual JPEG frames by SOI/EOI markers.
16
+ Useful for custom FFmpeg pipelines before handing off to `send_video_frame`.
17
+ - Optional `video` extras group: `pip install plexus-python[video]` installs
18
+ Pillow for non-JPEG input decoding and automatic oversized-frame downsampling.
19
+
20
+ ### Changed
21
+
22
+ - Frames that would exceed the gateway's 1 MB wire limit are automatically
23
+ re-encoded at a proportionally lower quality. A one-time warning is printed
24
+ to stderr; subsequent frames are silently clamped.
25
+ - `stream_camera` raises `PlexusError` synchronously (before spawning a thread)
26
+ when FFmpeg is not found, rather than silently dying in the background.
27
+
28
+ ## [0.4.7] - 2026-05-14 - Video streaming API
29
+
30
+ ### Added
31
+
32
+ - `Plexus.send_video_frame(frame, camera_id, quality, timestamp)` — high-level
33
+ API for streaming camera frames. Accepts a numpy array (e.g. from
34
+ `cv2.VideoCapture.read()`), handles JPEG encoding, base64, dimensions, and
35
+ auth wait internally. Requires `transport="ws"` and `opencv-python`.
36
+
37
+ ### Changed
38
+
39
+ - Gateway WebSocket URL (`wss://plexus-gateway.fly.dev`) is now the SDK
40
+ default — no need to pass `ws_url` explicitly.
41
+ - Removed the `[plexus] endpoint: …` line from the connection printout.
42
+
43
+ ### Performance
44
+
45
+ - Eliminated per-frame `buf.tobytes()` copy in `send_video_frame` by passing
46
+ the numpy buffer directly to `base64.b64encode` (buffer protocol).
47
+ - `base64` imported at module level; `cv2` imported once on first call and
48
+ cached, removing repeated import overhead from the hot path.
49
+
3
50
  ## [0.4.5] - 2026-04-27 - Stderr status output (re-release of 0.4.4)
4
51
 
5
52
  Same code as 0.4.4 — the 0.4.4 publish workflow failed lint on a stray
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.4.6
3
+ Version: 0.4.8
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
@@ -30,6 +30,8 @@ Requires-Dist: pytest; extra == 'dev'
30
30
  Requires-Dist: pytest-cov; extra == 'dev'
31
31
  Requires-Dist: ruff; extra == 'dev'
32
32
  Requires-Dist: websockets>=12; extra == 'dev'
33
+ Provides-Extra: video
34
+ Requires-Dist: pillow>=9.0; extra == 'video'
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
  # plexus-python
@@ -0,0 +1 @@
1
+ 3.12
@@ -9,5 +9,6 @@ Each script is standalone — copy into your project, adjust the `source_id`, an
9
9
  | `can.py` | Vehicle CAN bus (with optional DBC decode) | `python-can`, `cantools` |
10
10
  | `mqtt.py` | MQTT broker → Plexus bridge | `paho-mqtt` |
11
11
  | `i2c_bme280.py` | Raspberry Pi environmental sensor | `adafruit-circuitpython-bme280` |
12
+ | `mac_metrics.py` | Mac system metrics + spike/pressure events | `psutil` |
12
13
 
13
14
  The pattern is always the same: use whatever library you'd use anyway, then call `px.send(metric, value)`. Plexus stays out of your decode path.
@@ -0,0 +1,81 @@
1
+ """
2
+ Mac system metrics with structured event logs.
3
+
4
+ Install deps:
5
+ pip install plexus-python psutil
6
+
7
+ Run:
8
+ python examples/mac_metrics.py --api-key plx_xxx
9
+ python examples/mac_metrics.py --api-key plx_xxx --interval 10
10
+ """
11
+
12
+ import argparse
13
+ import time
14
+ import psutil
15
+ from plexus import Plexus
16
+
17
+ parser = argparse.ArgumentParser(description="Stream Mac system metrics to Plexus.")
18
+ parser.add_argument("--api-key", required=True, help="Plexus API key (plx_xxx)")
19
+ parser.add_argument("--interval", type=float, default=5.0, metavar="SECONDS",
20
+ help="Sampling interval in seconds (default: 5)")
21
+ parser.add_argument("--source-id", default="macbook", help="Device source ID (default: macbook)")
22
+ args = parser.parse_args()
23
+
24
+ px = Plexus(api_key=args.api_key, source_id=args.source_id)
25
+
26
+ CPU_SPIKE_THRESHOLD = 80.0 # %
27
+ MEM_PRESSURE_THRESHOLD = 85.0 # %
28
+
29
+ prev_net = psutil.net_io_counters()
30
+ prev_disk = psutil.disk_io_counters()
31
+ prev_charging = None
32
+
33
+ while True:
34
+ net = psutil.net_io_counters()
35
+ disk = psutil.disk_io_counters()
36
+
37
+ cpu = psutil.cpu_percent(interval=None)
38
+ mem = psutil.virtual_memory()
39
+
40
+ # Numeric metrics
41
+ px.send("cpu.percent", cpu)
42
+ px.send("memory.used_gb", mem.used / 1e9)
43
+ px.send("memory.percent", mem.percent)
44
+ px.send("net.rx_mbps", (net.bytes_recv - prev_net.bytes_recv) / 1e6)
45
+ px.send("net.tx_mbps", (net.bytes_sent - prev_net.bytes_sent) / 1e6)
46
+ px.send("disk.read_mbps", (disk.read_bytes - prev_disk.read_bytes) / 1e6)
47
+ px.send("disk.write_mbps", (disk.write_bytes - prev_disk.write_bytes) / 1e6)
48
+ px.send("disk.free_gb", psutil.disk_usage("/").free / 1e9)
49
+
50
+ # Battery metrics + charge-state change event
51
+ battery = psutil.sensors_battery()
52
+ if battery:
53
+ px.send("battery.percent", battery.percent)
54
+ charging = battery.power_plugged
55
+ if charging != prev_charging and prev_charging is not None:
56
+ px.event("battery.state_change", {
57
+ "charging": charging,
58
+ "percent": battery.percent,
59
+ })
60
+ prev_charging = charging
61
+
62
+ # CPU spike event — fires once per interval when threshold is crossed
63
+ if cpu >= CPU_SPIKE_THRESHOLD:
64
+ top = max(psutil.process_iter(["name", "cpu_percent"]), key=lambda p: p.info["cpu_percent"] or 0)
65
+ px.event("cpu.spike", {
66
+ "cpu_percent": cpu,
67
+ "top_process": top.info["name"],
68
+ "top_process_percent": top.info["cpu_percent"],
69
+ })
70
+
71
+ # Memory pressure event
72
+ if mem.percent >= MEM_PRESSURE_THRESHOLD:
73
+ top = max(psutil.process_iter(["name", "memory_percent"]), key=lambda p: p.info["memory_percent"] or 0)
74
+ px.event("memory.pressure", {
75
+ "memory_percent": mem.percent,
76
+ "top_process": top.info["name"],
77
+ "top_process_percent": round(top.info["memory_percent"], 1),
78
+ })
79
+
80
+ prev_net, prev_disk = net, disk
81
+ time.sleep(args.interval)
@@ -41,7 +41,7 @@ def on_message(_client, _userdata, msg):
41
41
  px.send(name, data)
42
42
 
43
43
 
44
- client = mqtt.Client()
44
+ client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
45
45
  client.on_message = on_message
46
46
  client.connect(broker)
47
47
  client.subscribe(topic)
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "examples"
3
+ version = "0.1.0"
4
+ description = "Standalone example scripts for plexus-python"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "adafruit-circuitpython-bme280>=2.6.32",
9
+ "cantools>=41.3.1",
10
+ "paho-mqtt>=2.1.0",
11
+ "plexus-python>=0.4.6",
12
+ "psutil>=7.2.2",
13
+ "pymavlink>=2.4.49",
14
+ "python-can>=4.6.1",
15
+ ]