plexus-python 0.7.0__tar.gz → 0.7.1__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.
- {plexus_python-0.7.0 → plexus_python-0.7.1}/CHANGELOG.md +8 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/PKG-INFO +1 -1
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/__init__.py +1 -1
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/client.py +0 -14
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/config.py +0 -48
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/ws.py +1 -24
- {plexus_python-0.7.0 → plexus_python-0.7.1}/pyproject.toml +1 -1
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_ws.py +1 -74
- {plexus_python-0.7.0 → plexus_python-0.7.1}/uv.lock +1 -1
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/.gitignore +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/AGENTS.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/API.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/CONTRIBUTING.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/LICENSE +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/README.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/SECURITY.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/TODO.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/.python-version +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/README.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/basic.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/can.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/mac_metrics.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/mavlink.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/mqtt.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/pyproject.toml +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/thermal_camera.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/examples/uv.lock +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/_log.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/buffer.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/cameras/__init__.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/cameras/thermal.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/plexus/cli.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/scripts/plexus.service +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/scripts/release.sh +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/scripts/scan_buses.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/scripts/setup.sh +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/skills/plexus/SKILL.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/skills/plexus/references/api.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/skills/plexus/references/sdk.md +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_basic.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_buffer.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_config.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_retry.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_thermal.py +0 -0
- {plexus_python-0.7.0 → plexus_python-0.7.1}/tests/test_video.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] - 2026-06-02 - Remove install_id / source_id auto-suffix
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Removed `install_id` from the device auth frame and `WebSocketTransport`.
|
|
8
|
+
- Removed server-side source_id auto-suffix handling (`on_source_id_assigned` callback, `set_source_id` persistence).
|
|
9
|
+
- Removed `get_install_id` and `set_source_id` from `plexus.config`.
|
|
10
|
+
|
|
3
11
|
## [0.7.0] - 2026-05-29 - SDK hardening
|
|
4
12
|
|
|
5
13
|
### Fixed
|
|
@@ -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.7.
|
|
13
|
+
__version__ = "0.7.1"
|
|
14
14
|
__all__ = ["Plexus", "PlexusError", "AuthenticationError", "RetryConfig", "read_mjpeg_frames"]
|
|
@@ -55,9 +55,7 @@ from plexus.config import (
|
|
|
55
55
|
get_endpoint,
|
|
56
56
|
get_gateway_url,
|
|
57
57
|
get_gateway_ws_url,
|
|
58
|
-
get_install_id,
|
|
59
58
|
get_source_id,
|
|
60
|
-
set_source_id,
|
|
61
59
|
)
|
|
62
60
|
|
|
63
61
|
logger = logging.getLogger(__name__)
|
|
@@ -414,9 +412,7 @@ class Plexus:
|
|
|
414
412
|
api_key=self.api_key,
|
|
415
413
|
source_id=self.source_id,
|
|
416
414
|
ws_url=self._ws_url,
|
|
417
|
-
install_id=get_install_id(),
|
|
418
415
|
agent_version=__version__,
|
|
419
|
-
on_source_id_assigned=self._on_source_id_assigned,
|
|
420
416
|
on_clock_synced=self._on_clock_synced,
|
|
421
417
|
)
|
|
422
418
|
self._ws.start()
|
|
@@ -428,16 +424,6 @@ class Plexus:
|
|
|
428
424
|
def _on_clock_synced(self, offset_ms: int) -> None:
|
|
429
425
|
self._clock_offset_ms = offset_ms
|
|
430
426
|
|
|
431
|
-
def _on_source_id_assigned(self, assigned: str) -> None:
|
|
432
|
-
"""Callback from WebSocketTransport when the gateway returns an
|
|
433
|
-
auto-suffixed source_id. Persists it so subsequent runs (and the HTTP
|
|
434
|
-
fallback path in this process) use the assigned name directly."""
|
|
435
|
-
self.source_id = assigned
|
|
436
|
-
try:
|
|
437
|
-
set_source_id(assigned)
|
|
438
|
-
except Exception as e: # pragma: no cover - persistence failure is non-fatal
|
|
439
|
-
logger.debug("failed to persist assigned source_id: %s", e)
|
|
440
|
-
|
|
441
427
|
def _encode_frame(self, frame, quality: int) -> Tuple[bytes, int, int]:
|
|
442
428
|
"""Normalize any supported frame type to (jpeg_bytes, width, height).
|
|
443
429
|
|
|
@@ -145,54 +145,6 @@ def get_source_id() -> Optional[str]:
|
|
|
145
145
|
return source_id
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def get_install_id() -> str:
|
|
149
|
-
"""Get the device install ID, generating one if not set.
|
|
150
|
-
|
|
151
|
-
The install_id is a stable per-installation UUID. It is generated lazily
|
|
152
|
-
on first run (NOT at image-build time) so that cloned SD-card images
|
|
153
|
-
naturally get distinct install_ids on their first boot. The gateway uses
|
|
154
|
-
it to tell "same device reconnecting" from "different device claiming the
|
|
155
|
-
same name" when resolving source_id collisions.
|
|
156
|
-
|
|
157
|
-
Resolution order:
|
|
158
|
-
1. ``PLEXUS_INSTALL_ID`` env var — lets ephemeral containers (Fly
|
|
159
|
-
machines, CI runners, Kubernetes pods) pin a stable identity
|
|
160
|
-
across restarts when the config filesystem is ephemeral. Without
|
|
161
|
-
this, every redeploy generates a new install_id and the gateway
|
|
162
|
-
auto-suffixes the source_id to avoid a collision with the prior
|
|
163
|
-
install ("gw-001" → "gw-001_2" → "gw-001_3"…).
|
|
164
|
-
2. ``install_id`` in the on-disk config.
|
|
165
|
-
3. Newly-generated UUID, persisted to config.
|
|
166
|
-
"""
|
|
167
|
-
env_id = os.environ.get("PLEXUS_INSTALL_ID", "").strip()
|
|
168
|
-
if env_id:
|
|
169
|
-
return env_id
|
|
170
|
-
|
|
171
|
-
config = load_config()
|
|
172
|
-
install_id = config.get("install_id")
|
|
173
|
-
|
|
174
|
-
if not install_id:
|
|
175
|
-
import uuid
|
|
176
|
-
install_id = uuid.uuid4().hex
|
|
177
|
-
config["install_id"] = install_id
|
|
178
|
-
save_config(config)
|
|
179
|
-
|
|
180
|
-
return install_id
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def set_source_id(source_id: str) -> None:
|
|
184
|
-
"""Persist an updated source_id to the config file.
|
|
185
|
-
|
|
186
|
-
Called by the SDK when the gateway returns an auto-suffixed name so the
|
|
187
|
-
assigned name is stable across reconnects.
|
|
188
|
-
"""
|
|
189
|
-
config = load_config()
|
|
190
|
-
if config.get("source_id") == source_id:
|
|
191
|
-
return
|
|
192
|
-
config["source_id"] = source_id
|
|
193
|
-
save_config(config)
|
|
194
|
-
|
|
195
|
-
|
|
196
148
|
def get_persistent_buffer() -> bool:
|
|
197
149
|
"""Get persistent buffer setting. Default True (store-and-forward enabled)."""
|
|
198
150
|
config = load_config()
|
|
@@ -89,11 +89,9 @@ class WebSocketTransport:
|
|
|
89
89
|
source_id: str,
|
|
90
90
|
ws_url: str,
|
|
91
91
|
*,
|
|
92
|
-
install_id: str = "",
|
|
93
92
|
agent_version: str = "0.0.0",
|
|
94
93
|
platform: str = "python-sdk",
|
|
95
94
|
auto_reconnect: bool = True,
|
|
96
|
-
on_source_id_assigned: Optional[Callable[[str], None]] = None,
|
|
97
95
|
on_clock_synced: Optional[Callable[[int], None]] = None,
|
|
98
96
|
):
|
|
99
97
|
if not api_key:
|
|
@@ -103,12 +101,10 @@ class WebSocketTransport:
|
|
|
103
101
|
|
|
104
102
|
self.api_key = api_key
|
|
105
103
|
self.source_id = source_id
|
|
106
|
-
self.install_id = install_id
|
|
107
104
|
self.ws_url = _ensure_device_path(ws_url)
|
|
108
105
|
self.agent_version = agent_version
|
|
109
106
|
self.platform = platform
|
|
110
107
|
self.auto_reconnect = auto_reconnect
|
|
111
|
-
self._on_source_id_assigned = on_source_id_assigned
|
|
112
108
|
self._on_clock_synced = on_clock_synced
|
|
113
109
|
|
|
114
110
|
self._commands: Dict[str, _RegisteredCommand] = {}
|
|
@@ -278,16 +274,13 @@ class WebSocketTransport:
|
|
|
278
274
|
self._ws = ws
|
|
279
275
|
|
|
280
276
|
# 1. Send device_auth
|
|
281
|
-
desired_source_id = self.source_id
|
|
282
277
|
auth = {
|
|
283
278
|
"type": "device_auth",
|
|
284
279
|
"api_key": self.api_key,
|
|
285
|
-
"source_id":
|
|
280
|
+
"source_id": self.source_id,
|
|
286
281
|
"platform": self.platform,
|
|
287
282
|
"agent_version": self.agent_version,
|
|
288
283
|
}
|
|
289
|
-
if self.install_id:
|
|
290
|
-
auth["install_id"] = self.install_id
|
|
291
284
|
if self._commands:
|
|
292
285
|
auth["commands"] = [c.to_manifest() for c in self._commands.values()]
|
|
293
286
|
ws.send(json.dumps(auth))
|
|
@@ -312,22 +305,6 @@ class WebSocketTransport:
|
|
|
312
305
|
except Exception as e:
|
|
313
306
|
logger.debug("on_clock_synced callback raised: %s", e)
|
|
314
307
|
|
|
315
|
-
# The gateway may return a different source_id if the desired name
|
|
316
|
-
# was already claimed by another install — adopt the assigned value
|
|
317
|
-
# so all subsequent frames (heartbeats, future reconnects) use it.
|
|
318
|
-
assigned = msg.get("source_id")
|
|
319
|
-
if isinstance(assigned, str) and assigned and assigned != self.source_id:
|
|
320
|
-
logger.info(
|
|
321
|
-
"plexus ws source_id auto-suffixed: requested=%s assigned=%s",
|
|
322
|
-
desired_source_id, assigned,
|
|
323
|
-
)
|
|
324
|
-
self.source_id = assigned
|
|
325
|
-
if self._on_source_id_assigned is not None:
|
|
326
|
-
try:
|
|
327
|
-
self._on_source_id_assigned(assigned)
|
|
328
|
-
except Exception as e: # pragma: no cover - callback errors must not break auth
|
|
329
|
-
logger.debug("on_source_id_assigned callback raised: %s", e)
|
|
330
|
-
|
|
331
308
|
was_reconnect = self._backoff_attempt > 0
|
|
332
309
|
self._authenticated.set()
|
|
333
310
|
self._backoff_attempt = 0
|
|
@@ -126,7 +126,6 @@ def test_auth_handshake_and_telemetry(gateway):
|
|
|
126
126
|
api_key="plx_test_abc",
|
|
127
127
|
source_id="drone-001",
|
|
128
128
|
ws_url=_url(gateway.port),
|
|
129
|
-
install_id="install-A",
|
|
130
129
|
agent_version="9.9.9",
|
|
131
130
|
)
|
|
132
131
|
t.start()
|
|
@@ -137,7 +136,7 @@ def test_auth_handshake_and_telemetry(gateway):
|
|
|
137
136
|
assert gateway.auth_frame["type"] == "device_auth"
|
|
138
137
|
assert gateway.auth_frame["api_key"] == "plx_test_abc"
|
|
139
138
|
assert gateway.auth_frame["source_id"] == "drone-001"
|
|
140
|
-
assert
|
|
139
|
+
assert "install_id" not in gateway.auth_frame
|
|
141
140
|
assert gateway.auth_frame["platform"] == "python-sdk"
|
|
142
141
|
assert gateway.auth_frame["agent_version"] == "9.9.9"
|
|
143
142
|
# commands is omitted when none registered
|
|
@@ -256,78 +255,6 @@ def test_handler_exception_returns_error(gateway):
|
|
|
256
255
|
t.stop()
|
|
257
256
|
|
|
258
257
|
|
|
259
|
-
def test_install_id_omitted_when_empty():
|
|
260
|
-
# Default install_id="" should not leak an empty install_id field into
|
|
261
|
-
# the auth frame — that keeps the wire shape identical for legacy SDK
|
|
262
|
-
# builds that don't set one.
|
|
263
|
-
g = _StubGateway()
|
|
264
|
-
g.start()
|
|
265
|
-
try:
|
|
266
|
-
t = WebSocketTransport(
|
|
267
|
-
api_key="plx_test_abc",
|
|
268
|
-
source_id="drone-001",
|
|
269
|
-
ws_url=_url(g.port),
|
|
270
|
-
)
|
|
271
|
-
t.start()
|
|
272
|
-
try:
|
|
273
|
-
assert t.wait_authenticated(timeout=3)
|
|
274
|
-
assert "install_id" not in g.auth_frame
|
|
275
|
-
finally:
|
|
276
|
-
t.stop()
|
|
277
|
-
finally:
|
|
278
|
-
g.stop()
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def test_server_assigned_source_id_is_adopted():
|
|
282
|
-
# Simulate the auto-suffix path: SDK asks for "drone-01", the gateway
|
|
283
|
-
# returns "drone-01_2" in the authenticated frame. The transport must
|
|
284
|
-
# adopt the assigned name and fire the on_source_id_assigned callback.
|
|
285
|
-
g = _StubGateway(assigned_source_id="drone-01_2")
|
|
286
|
-
g.start()
|
|
287
|
-
try:
|
|
288
|
-
seen: List[str] = []
|
|
289
|
-
t = WebSocketTransport(
|
|
290
|
-
api_key="plx_test_abc",
|
|
291
|
-
source_id="drone-01",
|
|
292
|
-
ws_url=_url(g.port),
|
|
293
|
-
install_id="install-B",
|
|
294
|
-
on_source_id_assigned=lambda s: seen.append(s),
|
|
295
|
-
)
|
|
296
|
-
t.start()
|
|
297
|
-
try:
|
|
298
|
-
assert t.wait_authenticated(timeout=3)
|
|
299
|
-
assert t.source_id == "drone-01_2"
|
|
300
|
-
assert seen == ["drone-01_2"]
|
|
301
|
-
finally:
|
|
302
|
-
t.stop()
|
|
303
|
-
finally:
|
|
304
|
-
g.stop()
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def test_same_assigned_source_id_does_not_fire_callback():
|
|
308
|
-
# Happy path — gateway returns the same name. No callback, source_id unchanged.
|
|
309
|
-
g = _StubGateway() # echoes whatever was sent
|
|
310
|
-
g.start()
|
|
311
|
-
try:
|
|
312
|
-
seen: List[str] = []
|
|
313
|
-
t = WebSocketTransport(
|
|
314
|
-
api_key="plx_test_abc",
|
|
315
|
-
source_id="drone-01",
|
|
316
|
-
ws_url=_url(g.port),
|
|
317
|
-
install_id="install-A",
|
|
318
|
-
on_source_id_assigned=lambda s: seen.append(s),
|
|
319
|
-
)
|
|
320
|
-
t.start()
|
|
321
|
-
try:
|
|
322
|
-
assert t.wait_authenticated(timeout=3)
|
|
323
|
-
assert t.source_id == "drone-01"
|
|
324
|
-
assert seen == []
|
|
325
|
-
finally:
|
|
326
|
-
t.stop()
|
|
327
|
-
finally:
|
|
328
|
-
g.stop()
|
|
329
|
-
|
|
330
|
-
|
|
331
258
|
def test_ensure_device_path():
|
|
332
259
|
from plexus.ws import _ensure_device_path
|
|
333
260
|
assert _ensure_device_path("wss://foo") == "wss://foo/ws/device"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|