plexus-python 0.4.3__tar.gz → 0.4.5__tar.gz

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.
Files changed (36) hide show
  1. {plexus_python-0.4.3 → plexus_python-0.4.5}/CHANGELOG.md +30 -0
  2. {plexus_python-0.4.3 → plexus_python-0.4.5}/PKG-INFO +1 -1
  3. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/__init__.py +1 -1
  4. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/client.py +56 -0
  5. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/ws.py +38 -1
  6. {plexus_python-0.4.3 → plexus_python-0.4.5}/pyproject.toml +1 -1
  7. {plexus_python-0.4.3 → plexus_python-0.4.5}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  8. {plexus_python-0.4.3 → plexus_python-0.4.5}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  9. {plexus_python-0.4.3 → plexus_python-0.4.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  10. {plexus_python-0.4.3 → plexus_python-0.4.5}/.github/workflows/ci.yml +0 -0
  11. {plexus_python-0.4.3 → plexus_python-0.4.5}/.github/workflows/publish.yml +0 -0
  12. {plexus_python-0.4.3 → plexus_python-0.4.5}/.gitignore +0 -0
  13. {plexus_python-0.4.3 → plexus_python-0.4.5}/AGENTS.md +0 -0
  14. {plexus_python-0.4.3 → plexus_python-0.4.5}/API.md +0 -0
  15. {plexus_python-0.4.3 → plexus_python-0.4.5}/CODE_OF_CONDUCT.md +0 -0
  16. {plexus_python-0.4.3 → plexus_python-0.4.5}/CONTRIBUTING.md +0 -0
  17. {plexus_python-0.4.3 → plexus_python-0.4.5}/LICENSE +0 -0
  18. {plexus_python-0.4.3 → plexus_python-0.4.5}/README.md +0 -0
  19. {plexus_python-0.4.3 → plexus_python-0.4.5}/SECURITY.md +0 -0
  20. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/README.md +0 -0
  21. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/basic.py +0 -0
  22. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/can.py +0 -0
  23. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/i2c_bme280.py +0 -0
  24. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/mavlink.py +0 -0
  25. {plexus_python-0.4.3 → plexus_python-0.4.5}/examples/mqtt.py +0 -0
  26. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/buffer.py +0 -0
  27. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/cli.py +0 -0
  28. {plexus_python-0.4.3 → plexus_python-0.4.5}/plexus/config.py +0 -0
  29. {plexus_python-0.4.3 → plexus_python-0.4.5}/scripts/plexus.service +0 -0
  30. {plexus_python-0.4.3 → plexus_python-0.4.5}/scripts/scan_buses.py +0 -0
  31. {plexus_python-0.4.3 → plexus_python-0.4.5}/scripts/setup.sh +0 -0
  32. {plexus_python-0.4.3 → plexus_python-0.4.5}/tests/test_basic.py +0 -0
  33. {plexus_python-0.4.3 → plexus_python-0.4.5}/tests/test_buffer.py +0 -0
  34. {plexus_python-0.4.3 → plexus_python-0.4.5}/tests/test_config.py +0 -0
  35. {plexus_python-0.4.3 → plexus_python-0.4.5}/tests/test_retry.py +0 -0
  36. {plexus_python-0.4.3 → plexus_python-0.4.5}/tests/test_ws.py +0 -0
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.5] - 2026-04-27 - Stderr status output (re-release of 0.4.4)
4
+
5
+ Same code as 0.4.4 — the 0.4.4 publish workflow failed lint on a stray
6
+ `f`-prefix in `plexus/client.py:488`. PyPI doesn't allow re-uploading a
7
+ version, so 0.4.5 is the corrected re-release.
8
+
9
+ ## [0.4.4] - 2026-04-27 - Stderr status output
10
+
11
+ ### Added
12
+
13
+ - `[plexus] …` status lines on stderr at every meaningful state change so
14
+ scripts that don't configure the `logging` module still tell the user
15
+ what's going on. Set `PLEXUS_QUIET=1` to suppress.
16
+ - `✓ Connected to gateway as <source_id>` on first WS auth
17
+ - `✓ Reconnected as <source_id>` after a drop
18
+ - `✓ First N points landed (via ws|http)` on first successful send
19
+ - `⚠ WebSocket unavailable, falling back to POST /ingest` on WS failure
20
+ - `✗ Auth rejected by gateway: …` / `✗ Gateway rejected the API key (401)`
21
+ on auth failures, with a `plexus whoami` hint
22
+ - `⏸ Send failed, buffering points locally (N queued)` when offline
23
+ - `✓ Sending again (drained the local buffer)` on recovery
24
+
25
+ ### Why
26
+
27
+ Users running `python my_script.py` saw nothing — by default Python's
28
+ `logging` module emits at WARNING and above only on the console, so a
29
+ silent SDK was indistinguishable from "everything's working" until they
30
+ checked the dashboard. This makes the trip from `python my_script.py` to
31
+ "first row visible in the UI" auditable in one terminal.
32
+
3
33
  ## [0.4.3] - 2026-04-27 - Re-release of 0.4.2 with correct __version__
4
34
 
5
35
  The 0.4.2 wheel shipped with `plexus.__version__ == "0.4.1"` because the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -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.3"
13
+ __version__ = "0.4.5"
14
14
  __all__ = ["Plexus", "WebSocketTransport"]
@@ -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)
@@ -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
- logger.warning("plexus ws loop error: %s", e)
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)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plexus-python"
7
- version = "0.4.3"
7
+ version = "0.4.5"
8
8
  description = "Thin Python SDK for Plexus — send telemetry in one line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes