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 +1 -1
- plexus/_log.py +15 -0
- plexus/client.py +53 -25
- plexus/ws.py +2 -19
- {plexus_python-0.5.1.dist-info → plexus_python-0.5.2.dist-info}/METADATA +1 -1
- plexus_python-0.5.2.dist-info/RECORD +12 -0
- plexus_python-0.5.1.dist-info/RECORD +0 -11
- {plexus_python-0.5.1.dist-info → plexus_python-0.5.2.dist-info}/WHEEL +0 -0
- {plexus_python-0.5.1.dist-info → plexus_python-0.5.2.dist-info}/entry_points.txt +0 -0
- {plexus_python-0.5.1.dist-info → plexus_python-0.5.2.dist-info}/licenses/LICENSE +0 -0
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.
|
|
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
|
|
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
|
|
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 =
|
|
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)
|
|
372
|
-
|
|
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
|
-
|
|
387
|
-
data_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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|