plexus-python 0.4.3__py3-none-any.whl → 0.4.5__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 +56 -0
- plexus/ws.py +38 -1
- {plexus_python-0.4.3.dist-info → plexus_python-0.4.5.dist-info}/METADATA +1 -1
- plexus_python-0.4.5.dist-info/RECORD +11 -0
- plexus_python-0.4.3.dist-info/RECORD +0 -11
- {plexus_python-0.4.3.dist-info → plexus_python-0.4.5.dist-info}/WHEEL +0 -0
- {plexus_python-0.4.3.dist-info → plexus_python-0.4.5.dist-info}/entry_points.txt +0 -0
- {plexus_python-0.4.3.dist-info → plexus_python-0.4.5.dist-info}/licenses/LICENSE +0 -0
plexus/__init__.py
CHANGED
plexus/client.py
CHANGED
|
@@ -36,6 +36,8 @@ Note: Requires authentication. Run 'plexus start' or set PLEXUS_API_KEY.
|
|
|
36
36
|
import gzip
|
|
37
37
|
import json
|
|
38
38
|
import logging
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
39
41
|
import time
|
|
40
42
|
from contextlib import contextmanager
|
|
41
43
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
@@ -55,6 +57,20 @@ from plexus.config import (
|
|
|
55
57
|
)
|
|
56
58
|
logger = logging.getLogger(__name__)
|
|
57
59
|
|
|
60
|
+
# Status messages to stderr so users running `python my_script.py` see what's
|
|
61
|
+
# happening without having to configure logging. Set PLEXUS_QUIET=1 to disable.
|
|
62
|
+
_QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _say(line: str) -> None:
|
|
66
|
+
if _QUIET:
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
sys.stderr.write(f"[plexus] {line}\n")
|
|
70
|
+
sys.stderr.flush()
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
58
74
|
# Flexible value type - supports any JSON-serializable value
|
|
59
75
|
FlexValue = Union[int, float, str, bool, Dict[str, Any], List[Any]]
|
|
60
76
|
|
|
@@ -133,6 +149,12 @@ class Plexus:
|
|
|
133
149
|
else:
|
|
134
150
|
self._buffer: BufferBackend = MemoryBuffer(max_size=max_buffer_size)
|
|
135
151
|
|
|
152
|
+
# State that drives the [plexus] stderr status line.
|
|
153
|
+
self._announced_first_send = False
|
|
154
|
+
self._announced_http_fallback = False
|
|
155
|
+
self._announced_buffering = False
|
|
156
|
+
self._send_count = 0
|
|
157
|
+
|
|
136
158
|
@property
|
|
137
159
|
def max_buffer_size(self):
|
|
138
160
|
return self._max_buffer_size
|
|
@@ -344,8 +366,14 @@ class Plexus:
|
|
|
344
366
|
ws.wait_authenticated(timeout=min(self.timeout, 5.0))
|
|
345
367
|
if ws.send_points(all_points):
|
|
346
368
|
self._clear_buffer()
|
|
369
|
+
self._note_send(len(all_points), via="ws")
|
|
347
370
|
return True
|
|
348
371
|
# Socket unavailable → fall through to HTTP.
|
|
372
|
+
if not self._announced_http_fallback:
|
|
373
|
+
_say(
|
|
374
|
+
f"⚠ WebSocket unavailable, falling back to POST {self.gateway_url}/ingest"
|
|
375
|
+
)
|
|
376
|
+
self._announced_http_fallback = True
|
|
349
377
|
|
|
350
378
|
url = f"{self.gateway_url}/ingest"
|
|
351
379
|
last_error: Optional[Exception] = None
|
|
@@ -372,8 +400,11 @@ class Plexus:
|
|
|
372
400
|
|
|
373
401
|
# Auth errors - don't retry, raise immediately
|
|
374
402
|
if response.status_code == 401:
|
|
403
|
+
_say("✗ Gateway rejected the API key (401).")
|
|
404
|
+
_say(" Run `plexus whoami` to confirm what's on disk.")
|
|
375
405
|
raise AuthenticationError("Invalid API key")
|
|
376
406
|
elif response.status_code == 403:
|
|
407
|
+
_say("✗ API key lacks write scope (403).")
|
|
377
408
|
raise AuthenticationError("API key doesn't have write permissions")
|
|
378
409
|
|
|
379
410
|
# Bad request errors - don't retry (client error)
|
|
@@ -403,6 +434,7 @@ class Plexus:
|
|
|
403
434
|
# Success - clear the buffer and return
|
|
404
435
|
elif response.status_code < 400:
|
|
405
436
|
self._clear_buffer()
|
|
437
|
+
self._note_send(len(all_points), via="http")
|
|
406
438
|
return True
|
|
407
439
|
|
|
408
440
|
# Other 4xx errors - don't retry
|
|
@@ -427,11 +459,35 @@ class Plexus:
|
|
|
427
459
|
|
|
428
460
|
# All retries failed - buffer the points for later
|
|
429
461
|
self._add_to_buffer(points)
|
|
462
|
+
if not self._announced_buffering:
|
|
463
|
+
_say(
|
|
464
|
+
f"⏸ Send failed, buffering points locally ({self.buffer_size()} queued). "
|
|
465
|
+
f"Will retry on next call."
|
|
466
|
+
)
|
|
467
|
+
self._announced_buffering = True
|
|
430
468
|
|
|
431
469
|
if last_error:
|
|
432
470
|
raise last_error
|
|
433
471
|
raise PlexusError("Send failed after all retries")
|
|
434
472
|
|
|
473
|
+
def _note_send(self, count: int, via: str) -> None:
|
|
474
|
+
"""Bookkeeping so the user sees the moment data starts flowing.
|
|
475
|
+
|
|
476
|
+
First successful send → "✓ First N points landed (via WS/HTTP)".
|
|
477
|
+
Recovery from a buffering state → "✓ Sending again (was buffered)".
|
|
478
|
+
Otherwise silent — every-send chatter would be unbearable at 100 Hz.
|
|
479
|
+
"""
|
|
480
|
+
self._send_count += count
|
|
481
|
+
if not self._announced_first_send:
|
|
482
|
+
_say(
|
|
483
|
+
f"✓ First {count} point{'s' if count != 1 else ''} landed "
|
|
484
|
+
f"(via {via}). source_id={self.source_id!r}"
|
|
485
|
+
)
|
|
486
|
+
self._announced_first_send = True
|
|
487
|
+
elif self._announced_buffering:
|
|
488
|
+
_say("✓ Sending again (drained the local buffer).")
|
|
489
|
+
self._announced_buffering = False
|
|
490
|
+
|
|
435
491
|
def _add_to_buffer(self, points: List[Dict[str, Any]]) -> None:
|
|
436
492
|
"""Add points to the local buffer for later retry."""
|
|
437
493
|
self._buffer.add(points)
|
plexus/ws.py
CHANGED
|
@@ -27,7 +27,9 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
import json
|
|
29
29
|
import logging
|
|
30
|
+
import os
|
|
30
31
|
import random
|
|
32
|
+
import sys
|
|
31
33
|
import threading
|
|
32
34
|
import time
|
|
33
35
|
from dataclasses import dataclass, field
|
|
@@ -43,6 +45,23 @@ except ImportError as e: # pragma: no cover - import-time failure is obvious
|
|
|
43
45
|
|
|
44
46
|
logger = logging.getLogger(__name__)
|
|
45
47
|
|
|
48
|
+
# By default, print connection status to stderr so users running
|
|
49
|
+
# `python my_script.py` can see what's happening without having to
|
|
50
|
+
# configure the logging module. Set PLEXUS_QUIET=1 to disable.
|
|
51
|
+
_QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _say(line: str) -> None:
|
|
55
|
+
"""Single-line status message to stderr. Skipped if PLEXUS_QUIET=1."""
|
|
56
|
+
if _QUIET:
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
sys.stderr.write(f"[plexus] {line}\n")
|
|
60
|
+
sys.stderr.flush()
|
|
61
|
+
except Exception:
|
|
62
|
+
# Stderr blew up — don't take the whole client down with it.
|
|
63
|
+
pass
|
|
64
|
+
|
|
46
65
|
AUTH_TIMEOUT_S = 10.0
|
|
47
66
|
HEARTBEAT_INTERVAL_S = 30.0
|
|
48
67
|
BACKOFF_BASE_S = 1.0
|
|
@@ -169,11 +188,22 @@ class WebSocketTransport:
|
|
|
169
188
|
# ------------------------------------------------------------------ thread
|
|
170
189
|
|
|
171
190
|
def _run(self) -> None:
|
|
191
|
+
first_attempt = True
|
|
172
192
|
while not self._stop.is_set():
|
|
173
193
|
try:
|
|
174
194
|
self._connect_and_serve()
|
|
175
195
|
except Exception as e:
|
|
176
|
-
|
|
196
|
+
msg = str(e)
|
|
197
|
+
logger.warning("plexus ws loop error: %s", msg)
|
|
198
|
+
# Loud the first time so users running a script see it,
|
|
199
|
+
# quieter on subsequent retries to avoid log spam.
|
|
200
|
+
if first_attempt:
|
|
201
|
+
if "auth failed" in msg.lower() or "invalid api key" in msg.lower():
|
|
202
|
+
_say(f"✗ Auth rejected by gateway: {msg}")
|
|
203
|
+
_say(" Check your key — `plexus whoami` shows what's on disk.")
|
|
204
|
+
else:
|
|
205
|
+
_say(f"✗ Connection failed: {msg}")
|
|
206
|
+
_say(" SDK will keep retrying with backoff.")
|
|
177
207
|
finally:
|
|
178
208
|
self._authenticated.clear()
|
|
179
209
|
with self._ws_lock:
|
|
@@ -185,6 +215,7 @@ class WebSocketTransport:
|
|
|
185
215
|
delay = _backoff_delay(self._backoff_attempt)
|
|
186
216
|
self._backoff_attempt = min(self._backoff_attempt + 1, 10)
|
|
187
217
|
logger.info("plexus ws reconnect in %.1fs", delay)
|
|
218
|
+
first_attempt = False
|
|
188
219
|
if self._stop.wait(timeout=delay):
|
|
189
220
|
break
|
|
190
221
|
|
|
@@ -235,9 +266,15 @@ class WebSocketTransport:
|
|
|
235
266
|
except Exception as e: # pragma: no cover - callback errors must not break auth
|
|
236
267
|
logger.debug("on_source_id_assigned callback raised: %s", e)
|
|
237
268
|
|
|
269
|
+
was_reconnect = self._backoff_attempt > 0
|
|
238
270
|
self._authenticated.set()
|
|
239
271
|
self._backoff_attempt = 0
|
|
240
272
|
logger.info("plexus ws authenticated as %s", self.source_id)
|
|
273
|
+
if was_reconnect:
|
|
274
|
+
_say(f"✓ Reconnected as {self.source_id}")
|
|
275
|
+
else:
|
|
276
|
+
_say(f"✓ Connected to gateway as {self.source_id}")
|
|
277
|
+
_say(f" endpoint: {self.ws_url}")
|
|
241
278
|
|
|
242
279
|
# 3. Read loop with heartbeat pump
|
|
243
280
|
ws.settimeout(1.0)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
plexus/__init__.py,sha256=9USZDYoueDQVWKI7HREgzoDRIaPaxNCYmf8ng-CcDn0,345
|
|
2
|
+
plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
|
|
3
|
+
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
4
|
+
plexus/client.py,sha256=YtPa7lT5gC0etglIJEs1DInsE3qtPBaMZDZ552DKgS4,21706
|
|
5
|
+
plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
|
|
6
|
+
plexus/ws.py,sha256=qGZL9TsYUpzUwEew1tgXIvQdYm1GNxnFu-pLqxfAulA,14134
|
|
7
|
+
plexus_python-0.4.5.dist-info/METADATA,sha256=SLGrAflShkZdfXBbyjZwJIsFUt63tGnd38Hd0ZrwOI0,6800
|
|
8
|
+
plexus_python-0.4.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
plexus_python-0.4.5.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
10
|
+
plexus_python-0.4.5.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
11
|
+
plexus_python-0.4.5.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
plexus/__init__.py,sha256=5og5cc398OVi85SxHJzxsQk7aZqLNC-MJYPnkBDdmYc,345
|
|
2
|
-
plexus/buffer.py,sha256=3ykybqLs7yMXxQWFajAT8nGe3cs_lW8_6Xvn0vQ69dE,9262
|
|
3
|
-
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
4
|
-
plexus/client.py,sha256=Hp-qUdLkZ83OQeF_3d2FH5kCZXK9iJOSmO7o0opOR8U,19395
|
|
5
|
-
plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
|
|
6
|
-
plexus/ws.py,sha256=upQ9SpekDYa7MltUW5ZDEuCm_E8hEVpxC0QFNP_jT1g,12581
|
|
7
|
-
plexus_python-0.4.3.dist-info/METADATA,sha256=2GlnmRZqlVdpbSlhFr9jDaKEC370iCktE9KFaLQqUEs,6800
|
|
8
|
-
plexus_python-0.4.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
-
plexus_python-0.4.3.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
10
|
-
plexus_python-0.4.3.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
11
|
-
plexus_python-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|