plexus-python 0.7.1__py3-none-any.whl → 0.8.0__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/client.py +11 -1
- plexus/config.py +2 -2
- plexus/ws.py +60 -7
- {plexus_python-0.7.1.dist-info → plexus_python-0.8.0.dist-info}/METADATA +6 -6
- plexus_python-0.8.0.dist-info/RECORD +14 -0
- plexus_python-0.7.1.dist-info/RECORD +0 -14
- {plexus_python-0.7.1.dist-info → plexus_python-0.8.0.dist-info}/WHEEL +0 -0
- {plexus_python-0.7.1.dist-info → plexus_python-0.8.0.dist-info}/entry_points.txt +0 -0
- {plexus_python-0.7.1.dist-info → plexus_python-0.8.0.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, PlexusError, AuthenticationError, read_mjpeg_frames
|
|
11
11
|
from plexus.config import RetryConfig
|
|
12
12
|
|
|
13
|
-
__version__ = "0.
|
|
13
|
+
__version__ = "0.8.0"
|
|
14
14
|
__all__ = ["Plexus", "PlexusError", "AuthenticationError", "RetryConfig", "read_mjpeg_frames"]
|
plexus/client.py
CHANGED
|
@@ -686,6 +686,7 @@ class Plexus:
|
|
|
686
686
|
*,
|
|
687
687
|
description: Optional[str] = None,
|
|
688
688
|
params: Optional[List[Dict[str, Any]]] = None,
|
|
689
|
+
concurrency: str = "accept",
|
|
689
690
|
) -> None:
|
|
690
691
|
"""Register a command handler (WebSocket transport only).
|
|
691
692
|
|
|
@@ -695,6 +696,12 @@ class Plexus:
|
|
|
695
696
|
|
|
696
697
|
Must be called before the first send() so the command is advertised
|
|
697
698
|
in the auth frame.
|
|
699
|
+
|
|
700
|
+
concurrency: "accept" (default) runs overlapping invocations of the
|
|
701
|
+
same command concurrently; "reject" refuses a new invocation with
|
|
702
|
+
an error result while a previous one is still running. Use
|
|
703
|
+
"reject" for handlers that drive exclusive hardware (e.g. a pump
|
|
704
|
+
init) so a retry or double-click can't start two at once.
|
|
698
705
|
"""
|
|
699
706
|
ws = self._ensure_ws()
|
|
700
707
|
if ws.is_authenticated:
|
|
@@ -704,7 +711,10 @@ class Plexus:
|
|
|
704
711
|
"Call on_command() before the first send().",
|
|
705
712
|
name,
|
|
706
713
|
)
|
|
707
|
-
ws.register_command(
|
|
714
|
+
ws.register_command(
|
|
715
|
+
name, handler, description=description, params=params,
|
|
716
|
+
concurrency=concurrency,
|
|
717
|
+
)
|
|
708
718
|
|
|
709
719
|
def _send_points(self, points: List[Dict[str, Any]]) -> bool:
|
|
710
720
|
"""Send data points to the gateway with retry and buffering.
|
plexus/config.py
CHANGED
|
@@ -49,8 +49,8 @@ CONFIG_DIR = Path.home() / ".plexus"
|
|
|
49
49
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
50
50
|
|
|
51
51
|
PLEXUS_ENDPOINT = "https://app.plexus.company"
|
|
52
|
-
PLEXUS_GATEWAY_URL = "https://
|
|
53
|
-
PLEXUS_GATEWAY_WS_URL = "wss://
|
|
52
|
+
PLEXUS_GATEWAY_URL = "https://gateway.plexus.company"
|
|
53
|
+
PLEXUS_GATEWAY_WS_URL = "wss://gateway.plexus.company"
|
|
54
54
|
|
|
55
55
|
DEFAULT_CONFIG = {
|
|
56
56
|
"api_key": None,
|
plexus/ws.py
CHANGED
|
@@ -62,6 +62,11 @@ class _RegisteredCommand:
|
|
|
62
62
|
handler: CommandHandler
|
|
63
63
|
description: Optional[str] = None
|
|
64
64
|
params: List[Dict[str, Any]] = field(default_factory=list)
|
|
65
|
+
# "accept" (default): run overlapping invocations concurrently.
|
|
66
|
+
# "reject": refuse a new invocation with an error result while a
|
|
67
|
+
# previous one of the same command is still running.
|
|
68
|
+
concurrency: str = "accept"
|
|
69
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
65
70
|
|
|
66
71
|
def to_manifest(self) -> Dict[str, Any]:
|
|
67
72
|
m: Dict[str, Any] = {"name": self.name}
|
|
@@ -127,11 +132,24 @@ class WebSocketTransport:
|
|
|
127
132
|
*,
|
|
128
133
|
description: Optional[str] = None,
|
|
129
134
|
params: Optional[List[Dict[str, Any]]] = None,
|
|
135
|
+
concurrency: str = "accept",
|
|
130
136
|
) -> None:
|
|
131
137
|
"""Register a command handler. Must be called before start() to be
|
|
132
|
-
advertised in the auth frame.
|
|
138
|
+
advertised in the auth frame.
|
|
139
|
+
|
|
140
|
+
concurrency controls what happens when a command arrives while a
|
|
141
|
+
previous invocation of the *same* command is still running:
|
|
142
|
+
"accept" (default) — run it concurrently on a new thread.
|
|
143
|
+
"reject" — refuse it with an error result until the
|
|
144
|
+
in-flight invocation finishes.
|
|
145
|
+
"""
|
|
146
|
+
if concurrency not in ("accept", "reject"):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"concurrency must be 'accept' or 'reject', got {concurrency!r}"
|
|
149
|
+
)
|
|
133
150
|
self._commands[name] = _RegisteredCommand(
|
|
134
|
-
name=name, handler=handler, description=description,
|
|
151
|
+
name=name, handler=handler, description=description,
|
|
152
|
+
params=params or [], concurrency=concurrency,
|
|
135
153
|
)
|
|
136
154
|
|
|
137
155
|
def start(self) -> None:
|
|
@@ -371,13 +389,42 @@ class WebSocketTransport:
|
|
|
371
389
|
})
|
|
372
390
|
return
|
|
373
391
|
|
|
392
|
+
# concurrency="reject": if an invocation is already running, refuse
|
|
393
|
+
# this one immediately rather than starting a second in parallel.
|
|
394
|
+
holds_lock = False
|
|
395
|
+
if reg.concurrency == "reject":
|
|
396
|
+
if not reg._lock.acquire(blocking=False):
|
|
397
|
+
self._send_frame({
|
|
398
|
+
"type": "command_result",
|
|
399
|
+
"id": cmd_id,
|
|
400
|
+
"command": command,
|
|
401
|
+
"event": "error",
|
|
402
|
+
"error": f"command already in progress: {command}",
|
|
403
|
+
})
|
|
404
|
+
return
|
|
405
|
+
holds_lock = True
|
|
406
|
+
|
|
374
407
|
# Run the handler off the read-loop thread so a slow handler doesn't
|
|
375
408
|
# block heartbeats or other inbound frames.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
409
|
+
try:
|
|
410
|
+
threading.Thread(
|
|
411
|
+
target=self._run_handler,
|
|
412
|
+
args=(reg, cmd_id, command, params, holds_lock),
|
|
413
|
+
daemon=True,
|
|
414
|
+
).start()
|
|
415
|
+
except Exception as e:
|
|
416
|
+
# Thread creation failed (e.g. resource exhaustion). Release the
|
|
417
|
+
# concurrency lock so the command isn't wedged as "in progress",
|
|
418
|
+
# and report the failure rather than leaving it silently un-acked.
|
|
419
|
+
if holds_lock:
|
|
420
|
+
reg._lock.release()
|
|
421
|
+
self._send_frame({
|
|
422
|
+
"type": "command_result",
|
|
423
|
+
"id": cmd_id,
|
|
424
|
+
"command": command,
|
|
425
|
+
"event": "error",
|
|
426
|
+
"error": f"failed to start command handler: {e}",
|
|
427
|
+
})
|
|
381
428
|
|
|
382
429
|
def _run_handler(
|
|
383
430
|
self,
|
|
@@ -385,6 +432,7 @@ class WebSocketTransport:
|
|
|
385
432
|
cmd_id: str,
|
|
386
433
|
command: str,
|
|
387
434
|
params: Dict[str, Any],
|
|
435
|
+
holds_lock: bool = False,
|
|
388
436
|
) -> None:
|
|
389
437
|
try:
|
|
390
438
|
result = reg.handler(command, params)
|
|
@@ -397,6 +445,11 @@ class WebSocketTransport:
|
|
|
397
445
|
"error": str(e),
|
|
398
446
|
})
|
|
399
447
|
return
|
|
448
|
+
finally:
|
|
449
|
+
# Release the concurrency lock (if held) as soon as the handler
|
|
450
|
+
# returns, regardless of success or failure.
|
|
451
|
+
if holds_lock:
|
|
452
|
+
reg._lock.release()
|
|
400
453
|
self._send_frame({
|
|
401
454
|
"type": "command_result",
|
|
402
455
|
"id": cmd_id,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Thin Python SDK for Plexus — send telemetry in one line
|
|
5
|
-
Project-URL: Homepage, https://plexus.
|
|
6
|
-
Project-URL: Documentation, https://docs.plexus.
|
|
5
|
+
Project-URL: Homepage, https://plexus.company
|
|
6
|
+
Project-URL: Documentation, https://docs.plexus.company
|
|
7
7
|
Project-URL: Repository, https://github.com/plexus-oss/plexus-python
|
|
8
8
|
Project-URL: Issues, https://github.com/plexus-oss/plexus-python/issues
|
|
9
|
-
Author-email: Plexus <
|
|
9
|
+
Author-email: Plexus <info@plexus.company>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: fleet,hardware,iot,monitoring,observability,telemetry
|
|
@@ -270,8 +270,8 @@ The SDK sends an `ack` frame before invoking the handler, then a `result` frame
|
|
|
270
270
|
| Variable | Description | Default |
|
|
271
271
|
| ----------------------- | ---------------------------- | -------------------------------- |
|
|
272
272
|
| `PLEXUS_API_KEY` | API key (required) | none |
|
|
273
|
-
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://
|
|
274
|
-
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://
|
|
273
|
+
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://gateway.plexus.company` |
|
|
274
|
+
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://gateway.plexus.company` |
|
|
275
275
|
|
|
276
276
|
## Architecture
|
|
277
277
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
plexus/__init__.py,sha256=O9GpL-CqlDKRoEfQq5ojYNsIMa7CtXuYvdw3zCrC8pg,447
|
|
2
|
+
plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
|
|
3
|
+
plexus/buffer.py,sha256=0i6PLgoj904jFNv9RCrlskvPQsPSu_KNdYWMasFOvsg,9596
|
|
4
|
+
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
5
|
+
plexus/client.py,sha256=12qnUgmZbTEXuSFtMCAXqosh-V84odSiqagbhtfjAc8,36280
|
|
6
|
+
plexus/config.py,sha256=Y4rPo1zeqAOnz-pQWrDBAXZ1gjfr0UD5Q6HHTMmbKew,4450
|
|
7
|
+
plexus/ws.py,sha256=PSVVrS1vpGi20qGdRfTi4LWCXzt3Dx3hR09FuoT-QIs,18643
|
|
8
|
+
plexus/cameras/__init__.py,sha256=OvnU9KGKxkVtFLlk56H9x-ATa6UvpLI7PANa0HQO2cc,490
|
|
9
|
+
plexus/cameras/thermal.py,sha256=7o33QsF1RiZLManTxZ2E36nO8lRAHppCDkS3zXBHCxs,12047
|
|
10
|
+
plexus_python-0.8.0.dist-info/METADATA,sha256=MfH0vB7R8uBYeJhTTY8Xbd_Qi3qCYZ3jzSIS8cPFNmg,11750
|
|
11
|
+
plexus_python-0.8.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
+
plexus_python-0.8.0.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
13
|
+
plexus_python-0.8.0.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
14
|
+
plexus_python-0.8.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
plexus/__init__.py,sha256=ZzZDGRCztNWGUtN-YF1EwwYzngxsZkU9zb_rW1Tfalg,447
|
|
2
|
-
plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
|
|
3
|
-
plexus/buffer.py,sha256=0i6PLgoj904jFNv9RCrlskvPQsPSu_KNdYWMasFOvsg,9596
|
|
4
|
-
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
5
|
-
plexus/client.py,sha256=SZ4V9FN-bt3QKqVgp9S5QKdRXw934WXZxKlYUb7bWHw,35810
|
|
6
|
-
plexus/config.py,sha256=RNym2Fon6JOCVi1rXPSRWjPFAdT8DSmokY5JPEljQOc,4450
|
|
7
|
-
plexus/ws.py,sha256=9DiQchqCQU7O8r8-FuqotJh8vYAQBO7npJn4BFNzLAE,16242
|
|
8
|
-
plexus/cameras/__init__.py,sha256=OvnU9KGKxkVtFLlk56H9x-ATa6UvpLI7PANa0HQO2cc,490
|
|
9
|
-
plexus/cameras/thermal.py,sha256=7o33QsF1RiZLManTxZ2E36nO8lRAHppCDkS3zXBHCxs,12047
|
|
10
|
-
plexus_python-0.7.1.dist-info/METADATA,sha256=OUDqqIIcHUaZms1QCwL7XdaFo6j11LeN0HZ3EipZEIM,11739
|
|
11
|
-
plexus_python-0.7.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
-
plexus_python-0.7.1.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
13
|
-
plexus_python-0.7.1.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
14
|
-
plexus_python-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|