plexus-python 0.4.6__py3-none-any.whl → 0.4.7__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
@@ -10,5 +10,5 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
10
10
  from plexus.client import Plexus
11
11
  from plexus.ws import WebSocketTransport
12
12
 
13
- __version__ = "0.4.6"
13
+ __version__ = "0.4.7"
14
14
  __all__ = ["Plexus", "WebSocketTransport"]
plexus/client.py CHANGED
@@ -33,6 +33,7 @@ Usage:
33
33
  Note: Requires authentication. Run 'plexus start' or set PLEXUS_API_KEY.
34
34
  """
35
35
 
36
+ import base64
36
37
  import gzip
37
38
  import json
38
39
  import logging
@@ -134,6 +135,7 @@ class Plexus:
134
135
  self._run_id: Optional[str] = None
135
136
  self._session: Optional[requests.Session] = None
136
137
  self._store_frames: bool = False
138
+ self._cv2 = None
137
139
 
138
140
  if transport not in ("ws", "http"):
139
141
  raise ValueError(f"transport must be 'ws' or 'http', got {transport!r}")
@@ -342,6 +344,65 @@ class Plexus:
342
344
  except Exception as e: # pragma: no cover - persistence failure is non-fatal
343
345
  logger.debug("failed to persist assigned source_id: %s", e)
344
346
 
347
+ def send_video_frame(
348
+ self,
349
+ frame,
350
+ camera_id: str = "camera:0",
351
+ quality: int = 85,
352
+ timestamp: Optional[float] = None,
353
+ ) -> bool:
354
+ """
355
+ Send a single video frame to Plexus (WebSocket transport only).
356
+
357
+ Args:
358
+ frame: A numpy array (H, W, C) as returned by cv2.VideoCapture.read()
359
+ camera_id: Logical camera identifier (e.g. "picam:0", "usb:1")
360
+ quality: JPEG compression quality, 1-100. Default 85.
361
+ timestamp: Unix timestamp. If not provided, uses current time.
362
+
363
+ Returns:
364
+ True if the frame was sent successfully.
365
+
366
+ Raises:
367
+ PlexusError: If transport is not 'ws', or if the send fails.
368
+ ImportError: If opencv-python is not installed.
369
+
370
+ Example:
371
+ cap = cv2.VideoCapture(0)
372
+ ret, frame = cap.read()
373
+ px.send_video_frame(frame, camera_id="picam:0")
374
+ """
375
+ if self.transport != "ws":
376
+ raise PlexusError("send_video_frame requires transport='ws'")
377
+
378
+ if self._cv2 is None:
379
+ try:
380
+ import cv2 as _cv2
381
+ self._cv2 = _cv2
382
+ except ImportError as e:
383
+ raise ImportError(
384
+ "send_video_frame requires opencv-python. "
385
+ "Install with: pip install opencv-python"
386
+ ) from e
387
+
388
+ height, width = frame.shape[:2]
389
+ _, buf = self._cv2.imencode(".jpg", frame, [self._cv2.IMWRITE_JPEG_QUALITY, quality])
390
+ b64 = base64.b64encode(buf).decode()
391
+
392
+ ws = self._ensure_ws()
393
+ if not ws.is_authenticated:
394
+ ws.wait_authenticated(timeout=min(self.timeout, 5.0))
395
+
396
+ return ws._send_frame({
397
+ "type": "video_frame",
398
+ "source_id": self.source_id,
399
+ "camera_id": camera_id,
400
+ "frame": b64,
401
+ "width": width,
402
+ "height": height,
403
+ "timestamp": self._normalize_ts_ms(timestamp),
404
+ })
405
+
345
406
  def on_command(
346
407
  self,
347
408
  name: str,
plexus/ws.py CHANGED
@@ -292,7 +292,6 @@ class WebSocketTransport:
292
292
  _say(f"✓ Reconnected as {self.source_id}")
293
293
  else:
294
294
  _say(f"✓ Connected to gateway as {self.source_id}")
295
- _say(f" endpoint: {self.ws_url}")
296
295
 
297
296
  # 3. Read loop with heartbeat pump
298
297
  ws.settimeout(1.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.4.6
3
+ Version: 0.4.7
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
@@ -0,0 +1,11 @@
1
+ plexus/__init__.py,sha256=5Ef1BoSacfFvLTXaSV84bJWMSelmuX43AzO7QRphoys,345
2
+ plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
3
+ plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
4
+ plexus/client.py,sha256=uSmgxeGs3BOFfhtnTwH2L-uia-zyRuJTtTYEjyCMl0g,25016
5
+ plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
6
+ plexus/ws.py,sha256=4nmNganwS_1BujdFkH3u3lLBB7rEkPYd_oYrbdYkdY4,14818
7
+ plexus_python-0.4.7.dist-info/METADATA,sha256=4tdaBPCjJn2jWzk52vQLSg6zGtq2RrvSU5eF6yanqbI,8121
8
+ plexus_python-0.4.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ plexus_python-0.4.7.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
10
+ plexus_python-0.4.7.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
11
+ plexus_python-0.4.7.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- plexus/__init__.py,sha256=upqHP1A19Sp_CnCeCBHHJ1GUKKzeCJiJw5w3wD2K4cE,345
2
- plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
3
- plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
4
- plexus/client.py,sha256=JmEKKRu4ik548uBPMEOCcB_r_asRmuNORZF4snjFosQ,22956
5
- plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
6
- plexus/ws.py,sha256=aynJTH8LHRA4_U1OfIu-2aaOg1tLniUcWbzB2yuiQvU,14865
7
- plexus_python-0.4.6.dist-info/METADATA,sha256=d6Vp27f4EsfSHeS0NT6z9oWF85UIsLskMzzborehyQQ,8121
8
- plexus_python-0.4.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
- plexus_python-0.4.6.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
10
- plexus_python-0.4.6.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
11
- plexus_python-0.4.6.dist-info/RECORD,,