plexus-python 0.5.1__py3-none-any.whl → 0.5.2__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, read_mjpeg_frames
11
11
  from plexus.ws import WebSocketTransport
12
12
 
13
- __version__ = "0.5.1"
13
+ __version__ = "0.5.2"
14
14
  __all__ = ["Plexus", "WebSocketTransport", "read_mjpeg_frames"]
plexus/_log.py ADDED
@@ -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
plexus/client.py CHANGED
@@ -30,17 +30,16 @@ Usage:
30
30
  px.send("temperature", read_temp())
31
31
  time.sleep(0.01)
32
32
 
33
- Note: Requires authentication. Run 'plexus start' or set PLEXUS_API_KEY.
33
+ Note: Requires authentication. Run 'plexus init' or set PLEXUS_API_KEY.
34
34
  """
35
35
 
36
36
  import gzip
37
37
  import json
38
38
  import logging
39
- import os
39
+ import re
40
40
  import shutil
41
41
  import socket
42
42
  import subprocess
43
- import sys
44
43
  import threading
45
44
  import time
46
45
  import urllib.error
@@ -48,6 +47,7 @@ import urllib.request
48
47
  from contextlib import contextmanager
49
48
  from typing import Any, Dict, Generator, List, Optional, Tuple, Union
50
49
 
50
+ from plexus._log import _say
51
51
  from plexus.buffer import BufferBackend, MemoryBuffer, SqliteBuffer
52
52
  from plexus.config import (
53
53
  RetryConfig,
@@ -59,6 +59,7 @@ from plexus.config import (
59
59
  get_source_id,
60
60
  set_source_id,
61
61
  )
62
+
62
63
  logger = logging.getLogger(__name__)
63
64
 
64
65
 
@@ -101,20 +102,6 @@ class _ConnError(OSError):
101
102
  pass
102
103
 
103
104
 
104
- # Status messages to stderr so users running `python my_script.py` see what's
105
- # happening without having to configure logging. Set PLEXUS_QUIET=1 to disable.
106
- _QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
107
-
108
-
109
- def _say(line: str) -> None:
110
- if _QUIET:
111
- return
112
- try:
113
- sys.stderr.write(f"[plexus] {line}\n")
114
- sys.stderr.flush()
115
- except Exception:
116
- pass
117
-
118
105
  # Flexible value type - supports any JSON-serializable value
119
106
  FlexValue = Union[int, float, str, bool, Dict[str, Any], List[Any]]
120
107
 
@@ -161,6 +148,18 @@ class AuthenticationError(PlexusError):
161
148
  pass
162
149
 
163
150
 
151
+ _SOURCE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9_-]{1,62}$')
152
+
153
+
154
+ def _validate_source_id(source_id: str) -> None:
155
+ if not _SOURCE_ID_RE.match(source_id):
156
+ raise ValueError(
157
+ f"Invalid source_id {source_id!r}. "
158
+ "Must match ^[a-z0-9][a-z0-9_-]{1,62}$ "
159
+ "(lowercase letters, digits, hyphens, underscores; start with letter or digit)."
160
+ )
161
+
162
+
164
163
  class Plexus:
165
164
  """
166
165
  Client for sending sensor data to Plexus.
@@ -186,7 +185,7 @@ class Plexus:
186
185
  timeout: float = 10.0,
187
186
  retry_config: Optional[RetryConfig] = None,
188
187
  max_buffer_size: int = 10000,
189
- persistent_buffer: bool = False,
188
+ persistent_buffer: bool = True,
190
189
  buffer_path: Optional[str] = None,
191
190
  transport: str = "ws",
192
191
  ws_url: Optional[str] = None,
@@ -201,6 +200,7 @@ class Plexus:
201
200
  self.endpoint = (endpoint or get_endpoint()).rstrip("/")
202
201
  self.gateway_url = get_gateway_url()
203
202
  self.source_id = source_id or get_source_id()
203
+ _validate_source_id(self.source_id)
204
204
  self.timeout = timeout
205
205
  self.retry_config = retry_config or RetryConfig()
206
206
  self._max_buffer_size = max_buffer_size
@@ -360,7 +360,7 @@ class Plexus:
360
360
 
361
361
  def send_batch(
362
362
  self,
363
- points: List[Tuple[str, FlexValue]],
363
+ points: List[Union[Tuple[str, FlexValue], Tuple[str, FlexValue, float]]],
364
364
  timestamp: Optional[float] = None,
365
365
  tags: Optional[Dict[str, str]] = None,
366
366
  ) -> bool:
@@ -368,8 +368,11 @@ class Plexus:
368
368
  Send multiple metrics at once.
369
369
 
370
370
  Args:
371
- points: List of (metric, value) tuples. Values can be any FlexValue type.
372
- timestamp: Shared timestamp for all points. If not provided, uses current time.
371
+ points: List of (metric, value) or (metric, value, timestamp) tuples.
372
+ Values can be any FlexValue type. Per-point timestamps override
373
+ the shared timestamp argument.
374
+ timestamp: Shared timestamp for points that don't supply their own.
375
+ If not provided, uses current time.
373
376
  tags: Shared tags for all points
374
377
 
375
378
  Returns:
@@ -382,9 +385,23 @@ class Plexus:
382
385
  ("robot.state", "RUNNING"),
383
386
  ("position", {"x": 1.0, "y": 2.0}),
384
387
  ])
388
+
389
+ # Per-point timestamps (e.g. sensors on different interrupt timers):
390
+ px.send_batch([
391
+ ("imu.accel_x", 0.12, t_imu),
392
+ ("pressure", 1013.2, t_baro),
393
+ ("temperature", 22.4), # uses shared timestamp
394
+ ])
385
395
  """
386
- ts_ms = self._normalize_ts_ms(timestamp)
387
- data_points = [self._make_point(m, v, ts_ms, tags) for m, v in points]
396
+ default_ts_ms = self._normalize_ts_ms(timestamp)
397
+ data_points = []
398
+ for p in points:
399
+ if len(p) == 3:
400
+ m, v, t = p
401
+ data_points.append(self._make_point(m, v, self._normalize_ts_ms(t), tags))
402
+ else:
403
+ m, v = p
404
+ data_points.append(self._make_point(m, v, default_ts_ms, tags))
388
405
  return self._send_points(data_points)
389
406
 
390
407
  def _ensure_ws(self):
@@ -656,6 +673,12 @@ class Plexus:
656
673
  if self.transport != "ws":
657
674
  raise PlexusError("on_command requires transport='ws'")
658
675
  ws = self._ensure_ws()
676
+ if ws.is_authenticated:
677
+ _say(
678
+ f"⚠ on_command('{name}') called after connection is already authenticated — "
679
+ "command will not be advertised to the dashboard until next reconnect. "
680
+ "Call on_command() before the first send()."
681
+ )
659
682
  ws.register_command(name, handler, description=description, params=params)
660
683
 
661
684
  def _send_points(self, points: List[Dict[str, Any]]) -> bool:
@@ -673,7 +696,7 @@ class Plexus:
673
696
  """
674
697
  if not self.api_key:
675
698
  raise AuthenticationError(
676
- "No API key configured. Run 'plexus start' or set PLEXUS_API_KEY"
699
+ "No API key configured. Run 'plexus init' or set PLEXUS_API_KEY"
677
700
  )
678
701
 
679
702
  # Include any previously buffered points
@@ -905,7 +928,12 @@ class Plexus:
905
928
  self._store_frames = False
906
929
 
907
930
  def close(self):
908
- """Close the client and release resources."""
931
+ """Close the client, flush any buffered points, and release resources."""
932
+ if self.buffer_size() > 0:
933
+ try:
934
+ self.flush_buffer()
935
+ except Exception as e:
936
+ logger.debug("flush on close failed: %s", e)
909
937
  if self._ws is not None:
910
938
  self._ws.stop()
911
939
  self._ws = None
plexus/ws.py CHANGED
@@ -28,11 +28,9 @@ from __future__ import annotations
28
28
  import atexit
29
29
  import json
30
30
  import logging
31
- import os
32
31
  import queue
33
32
  import random
34
33
  import struct
35
- import sys
36
34
  import threading
37
35
  import time
38
36
  from dataclasses import dataclass, field
@@ -46,24 +44,9 @@ except ImportError as e: # pragma: no cover - import-time failure is obvious
46
44
  "Install with: pip install websocket-client"
47
45
  ) from e
48
46
 
49
- logger = logging.getLogger(__name__)
50
-
51
- # By default, print connection status to stderr so users running
52
- # `python my_script.py` can see what's happening without having to
53
- # configure the logging module. Set PLEXUS_QUIET=1 to disable.
54
- _QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
55
-
47
+ from plexus._log import _say
56
48
 
57
- def _say(line: str) -> None:
58
- """Single-line status message to stderr. Skipped if PLEXUS_QUIET=1."""
59
- if _QUIET:
60
- return
61
- try:
62
- sys.stderr.write(f"[plexus] {line}\n")
63
- sys.stderr.flush()
64
- except Exception:
65
- # Stderr blew up — don't take the whole client down with it.
66
- pass
49
+ logger = logging.getLogger(__name__)
67
50
 
68
51
  AUTH_TIMEOUT_S = 10.0
69
52
  HEARTBEAT_INTERVAL_S = 30.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.5.1
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
@@ -0,0 +1,12 @@
1
+ plexus/__init__.py,sha256=2Q3D7gMSFmMpFzcc_ywnCMH_1zgysy-PH6jW21bONgc,385
2
+ plexus/_log.py,sha256=y1A9ahcPuCefTQ8xsZtOJTb6iT0pP9LAlkeP7U-yvGE,352
3
+ plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
4
+ plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
5
+ plexus/client.py,sha256=S0AVpT73sKjMvDENZsWovk47ZuxZjT_vXQCAdNUVfG0,35338
6
+ plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
7
+ plexus/ws.py,sha256=yTvxcloHOqiXkLtPQ4Vquv8KSrGoluOEkYIDp7MFImo,17095
8
+ plexus_python-0.5.2.dist-info/METADATA,sha256=L87GlCwfwMIjXmetRak1E2vjkmdXyYj5v3nlzsFHtSU,11531
9
+ plexus_python-0.5.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ plexus_python-0.5.2.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
11
+ plexus_python-0.5.2.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
12
+ plexus_python-0.5.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- plexus/__init__.py,sha256=mNRP4PCXuMoMvaKh05M-1u3GrJL4TAhqA8nEY1xdiSY,385
2
- plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
3
- plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
4
- plexus/client.py,sha256=z8CNy0EFA0Ol3If1lIKgyndYYaqD67lvB0iuGV62Nsc,34034
5
- plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
6
- plexus/ws.py,sha256=lSVv-Yf4ODZ0TaziKEF9pEmgVOLAJHMlluULqVydePs,17683
7
- plexus_python-0.5.1.dist-info/METADATA,sha256=HHSsxh19qh99fkDFErlFzdCqxtT3zj0q4VCj8jre72Y,11531
8
- plexus_python-0.5.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
- plexus_python-0.5.1.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
10
- plexus_python-0.5.1.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
11
- plexus_python-0.5.1.dist-info/RECORD,,