plexus-python 0.11.1__tar.gz → 0.11.2__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.11.1 → plexus_python-0.11.2}/CHANGELOG.md +19 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/PKG-INFO +1 -1
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/__init__.py +1 -1
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/client.py +27 -6
- {plexus_python-0.11.1 → plexus_python-0.11.2}/pyproject.toml +1 -1
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_runs.py +42 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/.gitignore +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/AGENTS.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/API.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/CONTRIBUTING.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/LICENSE +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/README.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/SECURITY.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/TODO.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/.python-version +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/README.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/basic.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/can.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/mac_metrics.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/mavlink.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/mqtt.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/pyproject.toml +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/thermal_camera.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/examples/uv.lock +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/_log.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/batching.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/buffer.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/cameras/__init__.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/cameras/thermal.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/cli.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/config.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/plexus/ws.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/scripts/plexus.service +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/scripts/release.sh +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/scripts/scan_buses.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/scripts/setup.sh +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/scripts/verify_skills.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/skills/README.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/skills/plexus/SKILL.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/skills/plexus-dashboard/SKILL.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/skills/plexus-firmware/SKILL.md +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/conftest.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_basic.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_batching.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_batching_wire.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_buffer.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_config.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_isolation.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_rate_limit.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_retry.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_skills.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_thermal.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_video.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/tests/test_ws.py +0 -0
- {plexus_python-0.11.1 → plexus_python-0.11.2}/uv.lock +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.2] - 2026-09-02 - Stamp run boundaries on the server's clock
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A run's window was measured by a different clock than the points inside
|
|
10
|
+
it.** `_normalize_ts_ms` corrects generated timestamps by the offset the
|
|
11
|
+
gateway reports on connect; `start_run` / `end_run` read the local clock
|
|
12
|
+
instead. Every point taken in the final `offset` milliseconds of a run
|
|
13
|
+
therefore landed *after* `ended_at` and fell outside the window its own run
|
|
14
|
+
was evaluated over.
|
|
15
|
+
|
|
16
|
+
Measured on production: a device running 58ms behind the server sent 300
|
|
17
|
+
points and its run was judged on 284 of them. The shortfall reads exactly
|
|
18
|
+
like ingest lag, which is what it was mistaken for — a pass criterion can
|
|
19
|
+
come back green because the excursion that would have failed it was outside
|
|
20
|
+
the window by 58 milliseconds. Run boundaries now carry the same correction
|
|
21
|
+
as the telemetry. An explicitly passed `started_at` / `ended_at` is still
|
|
22
|
+
left alone.
|
|
23
|
+
|
|
5
24
|
## [0.11.1] - 2026-09-02 - Correct what the agent skills teach
|
|
6
25
|
|
|
7
26
|
### Fixed
|
|
@@ -60,10 +60,12 @@ if TYPE_CHECKING: # pragma: no cover - typing only
|
|
|
60
60
|
logger = logging.getLogger(__name__)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
64
|
-
"""
|
|
65
|
-
return
|
|
66
|
-
|
|
63
|
+
def _iso_from_ms(ms: int) -> str:
|
|
64
|
+
"""Epoch milliseconds as ISO-8601 with a Z offset — /api/runs validates the shape."""
|
|
65
|
+
return (
|
|
66
|
+
datetime.fromtimestamp(ms / 1000, timezone.utc)
|
|
67
|
+
.isoformat(timespec="milliseconds")
|
|
68
|
+
.replace("+00:00", "Z")
|
|
67
69
|
)
|
|
68
70
|
|
|
69
71
|
|
|
@@ -350,6 +352,22 @@ class Plexus:
|
|
|
350
352
|
return int(timestamp * 1000)
|
|
351
353
|
return int(timestamp)
|
|
352
354
|
|
|
355
|
+
def _now_iso(self) -> str:
|
|
356
|
+
"""Now on the SERVER's clock, as ISO-8601.
|
|
357
|
+
|
|
358
|
+
Run boundaries have to be stamped on the same clock as the telemetry
|
|
359
|
+
they bound, and `_normalize_ts_ms` corrects generated timestamps by the
|
|
360
|
+
offset the gateway reports on connect. Reading the local clock here
|
|
361
|
+
instead — which this did — leaves `ended_at` behind every point taken
|
|
362
|
+
in the final `offset` milliseconds of the run, and those points then
|
|
363
|
+
fall outside the window their own run is evaluated over.
|
|
364
|
+
|
|
365
|
+
The failure is quiet and looks like something else entirely: on a
|
|
366
|
+
device running 58ms behind the server, a 300-point run was evaluated
|
|
367
|
+
over 284 of them, and the shortfall reads exactly like ingest lag.
|
|
368
|
+
"""
|
|
369
|
+
return _iso_from_ms(int(time.time() * 1000) + self._clock_offset_ms)
|
|
370
|
+
|
|
353
371
|
@staticmethod
|
|
354
372
|
def _infer_class(value: FlexValue) -> str:
|
|
355
373
|
"""Numbers are metrics; everything else (str/bool/dict/list) is an event.
|
|
@@ -584,7 +602,10 @@ class Plexus:
|
|
|
584
602
|
Returns:
|
|
585
603
|
The created run dict, including its "id".
|
|
586
604
|
"""
|
|
587
|
-
body: dict[str, Any] = {
|
|
605
|
+
body: dict[str, Any] = {
|
|
606
|
+
"name": name,
|
|
607
|
+
"started_at": started_at or self._now_iso(),
|
|
608
|
+
}
|
|
588
609
|
if source_id is not _USE_CLIENT_SOURCE:
|
|
589
610
|
body["source_id"] = source_id
|
|
590
611
|
else:
|
|
@@ -664,7 +685,7 @@ class Plexus:
|
|
|
664
685
|
return self._api(
|
|
665
686
|
"PATCH",
|
|
666
687
|
f"/api/runs/{run_id}",
|
|
667
|
-
{"status": status, "ended_at": ended_at or
|
|
688
|
+
{"status": status, "ended_at": ended_at or self._now_iso()},
|
|
668
689
|
)["run"]
|
|
669
690
|
|
|
670
691
|
@contextmanager
|
|
@@ -10,6 +10,7 @@ The HTTP layer is stubbed at `_Session.request`, so no socket is opened.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import time
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
@@ -72,6 +73,47 @@ def test_started_at_matches_the_shape_the_route_validates():
|
|
|
72
73
|
datetime.fromisoformat(started.replace("Z", "+00:00")) # parses or raises
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
def test_run_boundaries_are_stamped_on_the_server_clock():
|
|
77
|
+
"""The window must be measured by the same clock as the points inside it.
|
|
78
|
+
|
|
79
|
+
`_normalize_ts_ms` corrects generated timestamps by the offset the gateway
|
|
80
|
+
reports on connect. Stamping run boundaries from the local clock instead
|
|
81
|
+
leaves `ended_at` behind every point taken in the final `offset`
|
|
82
|
+
milliseconds — and those points then fall outside the window their own run
|
|
83
|
+
is evaluated over. Measured on production before the fix: a 300-point run
|
|
84
|
+
on a device 58ms behind the server was judged on 284 of them, and the
|
|
85
|
+
shortfall reads exactly like ingest lag.
|
|
86
|
+
"""
|
|
87
|
+
from datetime import datetime
|
|
88
|
+
|
|
89
|
+
session = _StubSession()
|
|
90
|
+
px = _client(session)
|
|
91
|
+
px._clock_offset_ms = 5_000 # device 5s behind the server
|
|
92
|
+
|
|
93
|
+
px.start_run("run")
|
|
94
|
+
px.end_run({"id": "run_1"})
|
|
95
|
+
|
|
96
|
+
def as_ms(iso: str) -> int:
|
|
97
|
+
return int(datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp() * 1000)
|
|
98
|
+
|
|
99
|
+
local_ms = int(time.time() * 1000)
|
|
100
|
+
started = as_ms(session.calls[0][2]["started_at"])
|
|
101
|
+
ended = as_ms(session.calls[1][2]["ended_at"])
|
|
102
|
+
|
|
103
|
+
# Both carry the offset, so they bound points that also carry it.
|
|
104
|
+
assert started - local_ms > 4_000
|
|
105
|
+
assert ended - local_ms > 4_000
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_an_explicit_boundary_is_left_alone():
|
|
109
|
+
"""A caller passing its own timestamp owns it — we do not shift it."""
|
|
110
|
+
session = _StubSession()
|
|
111
|
+
px = _client(session)
|
|
112
|
+
px._clock_offset_ms = 5_000
|
|
113
|
+
px.start_run("run", started_at="2026-09-01T10:00:00.000Z")
|
|
114
|
+
assert session.calls[0][2]["started_at"] == "2026-09-01T10:00:00.000Z"
|
|
115
|
+
|
|
116
|
+
|
|
75
117
|
def test_run_defaults_to_the_clients_own_source():
|
|
76
118
|
session = _StubSession()
|
|
77
119
|
px = _client(session)
|
|
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
|
|
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
|