plesty-lib 0.3.3__tar.gz → 0.3.5.dev1__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.
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/CHANGELOG.md +59 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/PKG-INFO +1 -1
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/experiment.md +62 -10
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/monitor.md +60 -6
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/gl-secret-detection-report.json +5 -5
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/__init__.py +13 -1
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/io.py +149 -22
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_device_sync.py +50 -10
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/composite_device.py +7 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/__init__.py +6 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/base_experiment.py +86 -24
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/journal.py +2 -2
- plesty_lib-0.3.5.dev1/plesty/lib/experiment/runs.py +356 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/__init__.py +2 -0
- plesty_lib-0.3.5.dev1/plesty/lib/monitor/assets/viz.yaml +37 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/sources.py +90 -60
- plesty_lib-0.3.5.dev1/plesty/lib/monitor/viz.py +261 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/service/tcp_ip_server.py +25 -7
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/shell.py +7 -2
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_composite_config.py +22 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_data_path.py +46 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_experiment.py +18 -6
- plesty_lib-0.3.5.dev1/tests/test_experiment_runs.py +218 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_monitor.py +47 -14
- plesty_lib-0.3.5.dev1/tests/test_viz.py +151 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/.gitignore +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/.gitlab-ci.yml +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/LICENSE +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/LICENSES/LGPL-3.0-or-later.txt +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/README.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/REUSE.toml +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/analyzer.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/api_reference.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/architecture.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/contributing.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/data_schemas.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/base_device.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/cmd_solver.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/composite_device.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/error_handling.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/func_system.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/index.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/logging_system.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/param_system.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/test_helper.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/device/traffic_manager.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/imgs/device_layers.svg +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/imgs/device_standard.svg +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/imgs/plesty_framework.svg +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/index.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/quickstart.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/toc.yaml +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/docs/ui.md +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/analyzer/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/analyzer/base_analyzer.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/array.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/ctype_manager.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/table.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/types.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/data/units.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/async_wrapper.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_apt_device.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_visa_scpi_device.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/device_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/doc.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/funcs.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/params.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/device/telemetry.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/schedule.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/base_monitor.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/service/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/service/resource_manager.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/service/tcp_ip_client.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/apt.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/data_generator.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/demo_device.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/iceblock.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/scpi.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/client_field_test.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_func_system.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_param_system.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/experiment_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/field_test.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/field_test_concurrency.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/grouped_param_keys.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/report_artifact.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/resource_allocation.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/schema_params.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/test/schema_refresh.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/apt.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/apt_protocol.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/serial.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/tcp_ip.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/usb_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/visa.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/assets/palette.json +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/assets/shell.qss +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/monitor_panel.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/panel.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/app.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/plot.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/recorder.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/theme.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/__init__.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/config.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/dll_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/error_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/logger.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/registry.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/settings.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/pyproject.toml +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_analyzer.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_async_wrapper_threading.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_client_field_test.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_config.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_data_array.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_data_io_hdf5.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_data_types.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_demo_device.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_apt.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_base.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_funcs.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_params.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_scpi.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_device_telemetry.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_experiment_pipeline.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_field_test.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_misc.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_report_artifact.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_schema_params.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_schema_refresh.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_service_manager.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_settings.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_shipped_imports.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_sim.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_solver.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_tcp_ip_resources.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_traffic_drivers.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_traffic_orphan.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_traffic_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_ui.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_utils.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/tests/test_visa_discovery.py +0 -0
- {plesty_lib-0.3.3 → plesty_lib-0.3.5.dev1}/uv.lock +0 -0
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5.dev1 — 2026-08-17
|
|
4
|
+
|
|
5
|
+
Development pre-release for field-testing the run convention (#34); the
|
|
6
|
+
final release of this line is 0.4.0 — the API diff against 0.3.4 (the
|
|
7
|
+
`pattern=` arguments and `data_dir` attributes of the run sources are
|
|
8
|
+
gone, `Experiment(run_root=)` defaults to the mount) is a minor bump.
|
|
9
|
+
|
|
10
|
+
- `plesty.lib.monitor.Viz` (#34): the `monitor` / `render` command line over
|
|
11
|
+
an experiment's panels. The experiment registers one function
|
|
12
|
+
(`@viz.panels`, `Run` → panels) and calls `viz.main()`; run discovery,
|
|
13
|
+
`--run-root/--mount/--data-dir/--run`, live vs `--replay`, `--record`,
|
|
14
|
+
offscreen `render` with self-stop, and the text catalog
|
|
15
|
+
(`monitor/assets/viz.yaml`) are the lib's. `Run(..., data_dir=)` supplies
|
|
16
|
+
the host spelling for runs that did not record it.
|
|
17
|
+
- **Run layout: one `records.jsonl` per run instead of one `data/step_NNNN.json`
|
|
18
|
+
per step** (#34). Each completed step appends one fsynced line (its
|
|
19
|
+
`ResultDocument` + `index`; blobs still go to `data/`), and readers tail
|
|
20
|
+
the file by byte offset — writing and following a long run on a network
|
|
21
|
+
share no longer costs a file open and a directory listing per step. New:
|
|
22
|
+
`plesty.lib.data.append_record` / `read_records` / `record_value`;
|
|
23
|
+
`RunSource`/`ReplaySource` lose their `pattern` argument and gain `unread`
|
|
24
|
+
and `legacy`; the journal's `step_completed` carries `recorded` instead of
|
|
25
|
+
a `result` path; a resumed run also counts steps with a committed record
|
|
26
|
+
as done. Runs in the old layout are still read (`RunSource.legacy`).
|
|
27
|
+
- The run convention is spelled out once and exported (#34):
|
|
28
|
+
`plesty.lib.experiment.runs` — `run_id`/`parse_run_id`, `list_runs`,
|
|
29
|
+
`latest_run`, and `Run` (plan, config, journal status, records, and
|
|
30
|
+
`local()` translation of device-written paths onto this machine).
|
|
31
|
+
- One environment convention for the shared disk: `PLESTY_DATA_DIR` (as the
|
|
32
|
+
acquiring host sees it) and `PLESTY_DATA_MOUNT` (as this machine sees it).
|
|
33
|
+
`Experiment` reads both (or `data_dir=`/`data_mount=`), defaults
|
|
34
|
+
`run_root` to the mount when set, exposes `self.run_id`/`self.run_dir`
|
|
35
|
+
before `setup()` and `self.raw_dir` = `<data_dir>/<run_id>/raw` for the
|
|
36
|
+
writing devices, and journals all three with `run_started`. `run_root`
|
|
37
|
+
still defaults to `runs` without a mount.
|
|
38
|
+
|
|
39
|
+
## 0.3.4 — 2026-08-14
|
|
40
|
+
|
|
41
|
+
- The server never cancels a receive (#33): the old
|
|
42
|
+
`wait_for(recv_multipart, 0.2)` pattern cancelled the ZMQ receive five
|
|
43
|
+
times a second, and a cancel racing an arriving message dropped it — the
|
|
44
|
+
request never happened as far as the server knew, and the client waited
|
|
45
|
+
out its full timeout (confirmed by a lost acquire that left no frame on
|
|
46
|
+
disk). The loop polls first and only then receives; Ctrl+C stays prompt.
|
|
47
|
+
Replies are mandatory-routed: one sent to a client that already timed out
|
|
48
|
+
and reconnected is now a warning in the server log instead of a silent
|
|
49
|
+
drop.
|
|
50
|
+
- `CompositeDevice.reconnect()` closes the stale socket with linger 0 (#32):
|
|
51
|
+
`ctx.term()` otherwise blocks until the very message the reconnect exists
|
|
52
|
+
to abandon is flushed — measured at ~45 s per reconnect against an
|
|
53
|
+
unhealthy peer, turning the recovery ladder into the delay.
|
|
54
|
+
- Data paths keep their configured spelling (#31): `Path.resolve()` rewrites
|
|
55
|
+
a mapped Windows drive to its UNC form, which the operator never wrote and
|
|
56
|
+
vendor software rejects. Resolution still guards the containment checks;
|
|
57
|
+
`set_data_path`/`get_data_path` return paths re-based onto the configured
|
|
58
|
+
root spelling. `get_data_path` also creates the directory — the configured
|
|
59
|
+
default was otherwise never created, and vendor software validating it
|
|
60
|
+
rejects a directory that does not exist.
|
|
61
|
+
|
|
3
62
|
## 0.3.3 — 2026-08-14
|
|
4
63
|
|
|
5
64
|
- `CompositeDevice(..., thread_affinity=True)` gives each sub-device its own
|
|
@@ -17,23 +17,29 @@ instances, never raw hardware.
|
|
|
17
17
|
| `Step` | The smallest unit of work: a stable `id`, an operation name `op`, and its `params`. A step either completes (result persisted and journaled) or is re-run on resume. |
|
|
18
18
|
| `Plan` | The frozen, ordered schedule of steps plus the run configuration. Written to disk at run start, never mutated; a SHA-256 `content_hash()` identifies the schedule. |
|
|
19
19
|
| `Journal` | A crash-safe, append-only JSONL event log (`run_started`, `step_started`, `step_completed`, `step_failed`, `run_completed`/`_canceled`/`_aborted`). Replaying it derives the run state and the completed-step set. |
|
|
20
|
+
| `records.jsonl` | One line per completed step: its result document (`plesty.lib.data.append_record` / `read_records`). One file, appended and fsynced per step, tailed by offset — not one small file per step, which is slow to write and slow to follow on a network share. |
|
|
20
21
|
| `Experiment` | The async ABC tying it together: validate → journal → execute with retries → persist results → resume. |
|
|
22
|
+
| `Run` | Read access to a stored run — plan, config, journal status, records, and the translation of device-written paths onto this machine (`plesty.lib.experiment.runs`, which also spells out the naming below and finds runs: `list_runs`, `latest_run`). |
|
|
21
23
|
|
|
22
24
|
Every run owns a directory:
|
|
23
25
|
|
|
24
26
|
```text
|
|
25
|
-
<run_root>/<run_id>/
|
|
26
|
-
├── plan.json
|
|
27
|
-
├── journal.jsonl
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
└──
|
|
27
|
+
<run_root>/<run_id>/ # run_id = <name>_<YYYYmmdd-HHMMSS>
|
|
28
|
+
├── plan.json # frozen schedule + config + content hash
|
|
29
|
+
├── journal.jsonl # append-only event log
|
|
30
|
+
├── records.jsonl # one line per completed step: its result document
|
|
31
|
+
└── data/ # blobs the records reference (arrays, images), if any
|
|
32
|
+
└── step_0003.npy
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Each record line is a typed [`ResultDocument`](data_schemas.md) with
|
|
36
|
+
provenance (`step_id`, `op`, `params`) plus the step `index`; a JSON value
|
|
37
|
+
result is inlined, an array or image stays a raw blob in `data/` that the
|
|
38
|
+
line references. The line is the commit — it is appended (and fsynced) only
|
|
39
|
+
after the blob is on disk, and a resumed run treats a committed record as a
|
|
40
|
+
completed step even if the crash came before its journal event. Runs
|
|
41
|
+
written by plesty-lib ≤ 0.3.4 (one `data/step_*.json` per step) are still
|
|
42
|
+
read by `RunSource`, `records()`, and `Run`.
|
|
37
43
|
|
|
38
44
|
## Writing an experiment
|
|
39
45
|
|
|
@@ -78,6 +84,52 @@ Step methods may be sync or async; results are awaited automatically. Each
|
|
|
78
84
|
step is retried (`max_retries`, `retry_sleep` constructor arguments) before
|
|
79
85
|
the run aborts.
|
|
80
86
|
|
|
87
|
+
## Data on a shared disk
|
|
88
|
+
|
|
89
|
+
Blobs a device writes itself (spectrometer frames, camera images) never
|
|
90
|
+
cross the network: the acquiring host saves them to a shared disk and the
|
|
91
|
+
step record carries only the path. Two machines spell that share
|
|
92
|
+
differently, so it is named once, by environment:
|
|
93
|
+
|
|
94
|
+
| Variable | Meaning |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `PLESTY_DATA_DIR` | the share as the acquiring host sees it (e.g. `G:\RAWDATA`) |
|
|
97
|
+
| `PLESTY_DATA_MOUNT` | the same share as this machine sees it (e.g. `/mnt/group/RAWDATA`) |
|
|
98
|
+
|
|
99
|
+
The experiment reads both (or takes `data_dir=` / `data_mount=` at
|
|
100
|
+
construction). With the mount set, `run_root` defaults to it, so runs land
|
|
101
|
+
on the share as well. Before `setup()` is awaited the run id is known and
|
|
102
|
+
`self.raw_dir` names `<data_dir>/<run_id>/raw` in the host's spelling — one
|
|
103
|
+
folder per run, the same one on resume — which `setup()` routes the writing
|
|
104
|
+
devices to:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
async def setup(self) -> None:
|
|
108
|
+
await super().setup()
|
|
109
|
+
if self.raw_dir is not None:
|
|
110
|
+
self.devices.set_data_path(self.raw_dir, devices=["spec"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`run_started` journals `data_dir`, `data_mount`, and `raw_dir`, so a reader
|
|
114
|
+
needs at most its own mount:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from plesty.lib.experiment import Run, latest_run
|
|
118
|
+
|
|
119
|
+
run = Run(latest_run("/mnt/group/RAWDATA", name="pol_pl"))
|
|
120
|
+
run.config["powermeter"] # the frozen configuration
|
|
121
|
+
run.status()["state"] # "running", "completed", ...
|
|
122
|
+
for record in run.records():
|
|
123
|
+
run.local(record["data_file"]) # G:\RAWDATA\... → /mnt/group/RAWDATA/...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`Run.local` uses, in order, an explicit `mount=`, `PLESTY_DATA_MOUNT`, and
|
|
127
|
+
the mount journaled by the machine that ran the experiment (only when that
|
|
128
|
+
directory exists here); with neither a data root nor a mount the path is
|
|
129
|
+
returned as recorded — the case of reading on the acquiring host itself.
|
|
130
|
+
`run.source(mapper)` returns a `RunSource` (live) or, with
|
|
131
|
+
`Run(..., replay_rows=n)`, a `ReplaySource` — see [Monitors](monitor.md).
|
|
132
|
+
|
|
81
133
|
## Crash, cancel, resume
|
|
82
134
|
|
|
83
135
|
`run()` returns the run id. If the run is interrupted — `Ctrl-C`, a crash,
|
|
@@ -82,16 +82,19 @@ clock belongs to the caller: a GUI timer, a test loop, a notebook cell.
|
|
|
82
82
|
|
|
83
83
|
| Source | Follows | Typical use |
|
|
84
84
|
|---|---|---|
|
|
85
|
-
| `RunSource` | `<run_dir>/
|
|
85
|
+
| `RunSource` | `<run_dir>/records.jsonl`, tailed by offset | any running experiment |
|
|
86
86
|
| `TelemetrySource` | `TelemetryEvent`s of devices | live device readings |
|
|
87
87
|
| `PushSource` | whatever the producer emits | tests, notebooks |
|
|
88
88
|
|
|
89
89
|
`RunSource` is the generic live view of *any* PLESTY experiment: the
|
|
90
|
-
[experiment framework](experiment.md)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
[experiment framework](experiment.md) appends one record line per completed
|
|
91
|
+
step, and that line is the commit record of the write, so following the file
|
|
92
|
+
needs no cooperation from the experiment and cannot disturb it. Each poll
|
|
93
|
+
reads only the bytes appended since the last one, so following a run of
|
|
94
|
+
thousands of steps on a network share stays cheap. It also means a viewer
|
|
95
|
+
started mid-run still sees every earlier row. A `Run` from
|
|
96
|
+
`plesty.lib.experiment` builds the source for you (`run.source(mapper)`),
|
|
97
|
+
live or replayed.
|
|
95
98
|
|
|
96
99
|
```python
|
|
97
100
|
from plesty.lib.monitor import RunSource
|
|
@@ -137,6 +140,57 @@ infrastructure. Reusable views live in their own package
|
|
|
137
140
|
(`plesty-common-monitors`); rig-specific ones live next to the experiment
|
|
138
141
|
that needs them.
|
|
139
142
|
|
|
143
|
+
## Watching a run: `Viz`
|
|
144
|
+
|
|
145
|
+
Everything about *watching* is the same for every experiment — which run
|
|
146
|
+
(the newest, or `--run`), where the share is mounted, live or replayed, in a
|
|
147
|
+
window or rendered offscreen to a video, recorded or not. `Viz` owns all of
|
|
148
|
+
it; the experiment writes one function that says which record key means
|
|
149
|
+
what, and that is its whole viz module:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# plesty/pol_pl/viz.py
|
|
153
|
+
import sys
|
|
154
|
+
|
|
155
|
+
from plesty.common_monitors import SeriesMonitor, SpectrumMonitor, WaterfallMonitor
|
|
156
|
+
from plesty.common_monitors.series import series_mapper
|
|
157
|
+
from plesty.common_monitors.spectrum import spectrum_mapper
|
|
158
|
+
from plesty.lib.monitor import Viz
|
|
159
|
+
from plesty.lib.ui import MonitorPanel
|
|
160
|
+
|
|
161
|
+
viz = Viz("Polarization PL", experiment="pol_pl")
|
|
162
|
+
|
|
163
|
+
@viz.panels
|
|
164
|
+
def panels(run):
|
|
165
|
+
spectra = spectrum_mapper("data_file", locate=run.local, row_key="hwp_deg")
|
|
166
|
+
yield MonitorPanel(SpectrumMonitor(run.source(spectra), name="spectrum"), weight=3)
|
|
167
|
+
yield MonitorPanel(WaterfallMonitor(run.source(spectra), name="map", y_label="HWP angle (deg)"), weight=3)
|
|
168
|
+
if run.config.get("powermeter"):
|
|
169
|
+
power = run.source(series_mapper("hwp_deg", "power_w", y_scale=1e6))
|
|
170
|
+
yield MonitorPanel(SeriesMonitor(power, name="power", y_label="Power (uW)"), weight=2)
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
sys.exit(viz.main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m plesty.pol_pl.viz monitor # follow the newest run live
|
|
178
|
+
python -m plesty.pol_pl.viz monitor --replay 2 --record demo.mp4
|
|
179
|
+
python -m plesty.pol_pl.viz render --run pol_pl_20260805-091909 # <run-id>.mp4, no window
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The function receives a [`Run`](experiment.md#data-on-a-shared-disk):
|
|
183
|
+
`run.source(mapper)` is already a `RunSource` or a `ReplaySource` as the
|
|
184
|
+
subcommand decided — call it **once per view**: a source hands each record
|
|
185
|
+
out once, so two views polling one source would split the rows between them
|
|
186
|
+
(a mapper, by contrast, is freely shared); `run.local` translates the acquiring host's paths onto
|
|
187
|
+
this machine, `run.config` is the frozen configuration. Runs are looked for
|
|
188
|
+
under `--run-root` (default `PLESTY_DATA_MOUNT`, else `runs`); `--mount`
|
|
189
|
+
and `--data-dir` override the share's two spellings for runs that did not
|
|
190
|
+
record them. `render` sets Qt's offscreen platform before the toolkit is
|
|
191
|
+
imported and stops by itself once every source is exhausted; neither
|
|
192
|
+
`--help` nor `resolve_run` needs the `gui` extra.
|
|
193
|
+
|
|
140
194
|
## Testing a monitor
|
|
141
195
|
|
|
142
196
|
`NullRenderer` records draw calls instead of drawing them, so the whole path
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
"version": "8.30.1"
|
|
20
20
|
},
|
|
21
21
|
"type": "secret_detection",
|
|
22
|
-
"start_time": "2026-08-
|
|
23
|
-
"end_time": "2026-08-
|
|
22
|
+
"start_time": "2026-08-17T12:45:23",
|
|
23
|
+
"end_time": "2026-08-17T12:45:24",
|
|
24
24
|
"status": "success",
|
|
25
25
|
"observability": {
|
|
26
26
|
"events": [
|
|
27
27
|
{
|
|
28
28
|
"event": "collect_secrets_analyzer_scan_metrics_from_pipeline",
|
|
29
|
-
"time_s": 0.
|
|
29
|
+
"time_s": 0.469797198,
|
|
30
30
|
"exit_code": 0,
|
|
31
31
|
"git_strategy": "FetchShallow",
|
|
32
|
-
"repo_size_kb":
|
|
32
|
+
"repo_size_kb": 685,
|
|
33
33
|
"commit_count": 1,
|
|
34
|
-
"bytes_scanned":
|
|
34
|
+
"bytes_scanned": 314,
|
|
35
35
|
"pipeline_type": "Tag"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
@@ -15,7 +15,16 @@ from .types import (
|
|
|
15
15
|
cast_basic_type,
|
|
16
16
|
)
|
|
17
17
|
from .table import TableHeader, PlestyTable2D, PlestyTable3D
|
|
18
|
-
from .io import
|
|
18
|
+
from .io import (
|
|
19
|
+
ResultDocument,
|
|
20
|
+
append_record,
|
|
21
|
+
convert_to_hdf5,
|
|
22
|
+
load_document,
|
|
23
|
+
load_result,
|
|
24
|
+
read_records,
|
|
25
|
+
record_value,
|
|
26
|
+
save_result,
|
|
27
|
+
)
|
|
19
28
|
|
|
20
29
|
__all__ = [
|
|
21
30
|
"PlestyArray",
|
|
@@ -24,6 +33,9 @@ __all__ = [
|
|
|
24
33
|
"load_result",
|
|
25
34
|
"load_document",
|
|
26
35
|
"convert_to_hdf5",
|
|
36
|
+
"append_record",
|
|
37
|
+
"read_records",
|
|
38
|
+
"record_value",
|
|
27
39
|
"Units",
|
|
28
40
|
"TableHeader",
|
|
29
41
|
"PlestyTable2D",
|
|
@@ -21,7 +21,15 @@ reference link to the blob:
|
|
|
21
21
|
|
|
22
22
|
Writes are crash-safe: the blob is written first (atomically, via a temporary
|
|
23
23
|
sibling file and :func:`os.replace`), then the JSON document. The JSON
|
|
24
|
-
document is the commit record — a result exists once its
|
|
24
|
+
document is the commit record — a result exists once its document does.
|
|
25
|
+
|
|
26
|
+
Two layouts carry the documents. :func:`save_result` writes one ``.json``
|
|
27
|
+
file per result — the right shape for standalone results. An experiment run
|
|
28
|
+
writes all of its step documents to **one** append-only ``records.jsonl``
|
|
29
|
+
(:func:`append_record` / :func:`read_records`): opening, syncing, and later
|
|
30
|
+
listing thousands of small files on a network share is what made a run slow
|
|
31
|
+
to write and slow to follow, whereas one line appended and fsynced costs one
|
|
32
|
+
write and is read back by offset. Blobs still go to ``data/`` beside it.
|
|
25
33
|
"""
|
|
26
34
|
|
|
27
35
|
import json
|
|
@@ -160,32 +168,20 @@ def _sniff_suffix(payload: bytes) -> str:
|
|
|
160
168
|
return "bin"
|
|
161
169
|
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
) -> Path:
|
|
168
|
-
"""Persist a measurement result as raw blob + JSON metadata document.
|
|
171
|
+
#: Name of a run's single record file (one result document per line).
|
|
172
|
+
RECORDS_FILE = "records.jsonl"
|
|
173
|
+
#: Directory under a run holding the blobs its records reference.
|
|
174
|
+
BLOB_SUBDIR = "data"
|
|
169
175
|
|
|
170
|
-
Args:
|
|
171
|
-
result: The value to persist — a :class:`PlestyArray`, encoded
|
|
172
|
-
``bytes`` (e.g. an image), or any JSON-serializable value.
|
|
173
|
-
path_stem: Destination path without suffix; the JSON document is
|
|
174
|
-
written to ``<stem>.json`` and any blob next to it.
|
|
175
|
-
provenance: Optional context (step id, operation, parameters,
|
|
176
|
-
device identity) recorded verbatim in the document.
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
def _persist(result: Any, stem: Path, provenance: Optional[dict[str, Any]]) -> ResultDocument:
|
|
178
|
+
"""Write *result*'s blob (if it has one) next to *stem*; return its document.
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
JSON-serializable.
|
|
180
|
+
The document's ``blob`` is the bare file name; callers writing the
|
|
181
|
+
document elsewhere than beside the blob re-base it.
|
|
184
182
|
"""
|
|
185
|
-
stem = Path(path_stem)
|
|
186
|
-
stem.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
-
|
|
188
183
|
if isinstance(result, PlestyArray):
|
|
184
|
+
stem.parent.mkdir(parents=True, exist_ok=True)
|
|
189
185
|
blob_path = stem.with_suffix(".npy")
|
|
190
186
|
buffer = BytesIO()
|
|
191
187
|
np.save(buffer, np.asarray(result))
|
|
@@ -200,6 +196,7 @@ def save_result(
|
|
|
200
196
|
meta={k: getattr(result, k, None) for k in PlestyArray._META_KEYS},
|
|
201
197
|
)
|
|
202
198
|
elif isinstance(result, (bytes, bytearray)):
|
|
199
|
+
stem.parent.mkdir(parents=True, exist_ok=True)
|
|
203
200
|
payload = bytes(result)
|
|
204
201
|
suffix = _sniff_suffix(payload)
|
|
205
202
|
blob_path = stem.with_suffix(f".{suffix}")
|
|
@@ -209,12 +206,142 @@ def save_result(
|
|
|
209
206
|
)
|
|
210
207
|
else:
|
|
211
208
|
document = ResultDocument(type="value", provenance=provenance or {}, value=result)
|
|
209
|
+
return document
|
|
212
210
|
|
|
211
|
+
|
|
212
|
+
def save_result(
|
|
213
|
+
result: Any,
|
|
214
|
+
path_stem: str | Path,
|
|
215
|
+
provenance: Optional[dict[str, Any]] = None,
|
|
216
|
+
) -> Path:
|
|
217
|
+
"""Persist a measurement result as raw blob + JSON metadata document.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
result: The value to persist — a :class:`PlestyArray`, encoded
|
|
221
|
+
``bytes`` (e.g. an image), or any JSON-serializable value.
|
|
222
|
+
path_stem: Destination path without suffix; the JSON document is
|
|
223
|
+
written to ``<stem>.json`` and any blob next to it.
|
|
224
|
+
provenance: Optional context (step id, operation, parameters,
|
|
225
|
+
device identity) recorded verbatim in the document.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The path of the JSON metadata document.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
TypeError: If the result is neither a PlestyArray, bytes, nor
|
|
232
|
+
JSON-serializable.
|
|
233
|
+
"""
|
|
234
|
+
stem = Path(path_stem)
|
|
235
|
+
document = _persist(result, stem, provenance)
|
|
236
|
+
stem.parent.mkdir(parents=True, exist_ok=True)
|
|
213
237
|
target = stem.with_suffix(".json")
|
|
214
238
|
_atomic_write_bytes(target, json.dumps(document.to_dict(), default=str, indent=2).encode())
|
|
215
239
|
return target
|
|
216
240
|
|
|
217
241
|
|
|
242
|
+
def append_record(
|
|
243
|
+
run_dir: str | Path,
|
|
244
|
+
index: int,
|
|
245
|
+
result: Any,
|
|
246
|
+
provenance: Optional[dict[str, Any]] = None,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Append one step result to the run's ``records.jsonl``.
|
|
249
|
+
|
|
250
|
+
A blob-backed result (array, bytes) is written to
|
|
251
|
+
``<run_dir>/data/step_<index>.<fmt>`` first, atomically; the document
|
|
252
|
+
line — the commit record — is appended and fsynced afterwards, so a
|
|
253
|
+
reader never sees a document whose blob is missing.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
run_dir: The run directory.
|
|
257
|
+
index: Position of the step in the plan; stamped onto the line
|
|
258
|
+
and used for the blob file name.
|
|
259
|
+
result: The value to persist (see :func:`save_result`).
|
|
260
|
+
provenance: Context recorded verbatim in the document.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The line that was written, as a dictionary — the document fields
|
|
264
|
+
plus ``index``; ``blob`` (when present) is relative to *run_dir*.
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
TypeError: If the result is neither a PlestyArray, bytes, nor
|
|
268
|
+
JSON-serializable.
|
|
269
|
+
"""
|
|
270
|
+
root = Path(run_dir)
|
|
271
|
+
document = _persist(result, root / BLOB_SUBDIR / f"step_{index:04d}", provenance)
|
|
272
|
+
line: dict[str, Any] = {"index": index, **document.to_dict()}
|
|
273
|
+
if document.blob is not None:
|
|
274
|
+
line["blob"] = f"{BLOB_SUBDIR}/{document.blob}"
|
|
275
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
with open(root / RECORDS_FILE, "a", encoding="utf-8") as stream:
|
|
277
|
+
stream.write(json.dumps(line, default=str) + "\n")
|
|
278
|
+
stream.flush()
|
|
279
|
+
os.fsync(stream.fileno())
|
|
280
|
+
return line
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def read_records(run_dir: str | Path, offset: int = 0) -> tuple[list[dict[str, Any]], int]:
|
|
284
|
+
"""Read the record lines appended to ``records.jsonl`` since *offset*.
|
|
285
|
+
|
|
286
|
+
Only complete lines are returned; a line still being written (no
|
|
287
|
+
trailing newline yet) is left for the next call, which is what makes
|
|
288
|
+
tailing a run in progress safe. Lines that fail to parse are skipped.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
run_dir: The run directory.
|
|
292
|
+
offset: Byte position to read from — pass back the returned offset
|
|
293
|
+
to read only what appeared since.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
The parsed lines (see :func:`append_record`) and the offset just
|
|
297
|
+
after the last complete line; ``([], offset)`` while the file does
|
|
298
|
+
not exist yet.
|
|
299
|
+
"""
|
|
300
|
+
path = Path(run_dir) / RECORDS_FILE
|
|
301
|
+
try:
|
|
302
|
+
with open(path, "rb") as stream:
|
|
303
|
+
stream.seek(offset)
|
|
304
|
+
chunk = stream.read()
|
|
305
|
+
except FileNotFoundError:
|
|
306
|
+
return [], offset
|
|
307
|
+
end = chunk.rfind(b"\n")
|
|
308
|
+
if end < 0:
|
|
309
|
+
return [], offset
|
|
310
|
+
lines: list[dict[str, Any]] = []
|
|
311
|
+
for raw in chunk[: end + 1].splitlines():
|
|
312
|
+
try:
|
|
313
|
+
lines.append(json.loads(raw))
|
|
314
|
+
except json.JSONDecodeError:
|
|
315
|
+
continue
|
|
316
|
+
return lines, offset + end + 1
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def record_value(line: dict[str, Any], run_dir: str | Path) -> Any:
|
|
320
|
+
"""Return the value a ``records.jsonl`` line stands for.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
line: A line as returned by :func:`read_records`.
|
|
324
|
+
run_dir: The run directory the line's ``blob`` is relative to.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The inlined value, or the :class:`PlestyArray` / raw ``bytes`` loaded
|
|
328
|
+
from the referenced blob.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If the line declares an unknown result type.
|
|
332
|
+
"""
|
|
333
|
+
document = ResultDocument.from_dict(line)
|
|
334
|
+
if document.type == "value":
|
|
335
|
+
return document.value
|
|
336
|
+
blob = Path(run_dir) / (document.blob or "")
|
|
337
|
+
if document.type == "array":
|
|
338
|
+
meta = dict(document.meta or {})
|
|
339
|
+
if meta.get("range") is not None:
|
|
340
|
+
meta["range"] = tuple(meta["range"])
|
|
341
|
+
return PlestyArray(np.load(blob), **meta)
|
|
342
|
+
return blob.read_bytes()
|
|
343
|
+
|
|
344
|
+
|
|
218
345
|
def load_document(path: str | Path) -> ResultDocument:
|
|
219
346
|
"""Load only the metadata document of a persisted result — no blob I/O.
|
|
220
347
|
|
|
@@ -87,13 +87,27 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
87
87
|
# channel: the device writes it to a shared disk and returns only the
|
|
88
88
|
# path, so the directory has to be steerable by the client that owns the
|
|
89
89
|
# run. Resolving here (no mkdir) keeps construction side-effect-free.
|
|
90
|
+
#
|
|
91
|
+
# Two spellings are kept per root: the resolved one guards the
|
|
92
|
+
# containment checks (resolve() collapses '..' and follows links, so
|
|
93
|
+
# they cannot be defeated), while the configured one is what callers
|
|
94
|
+
# and the instrument see. On Windows, resolve() rewrites a mapped
|
|
95
|
+
# network drive to its UNC form (Z:\... -> \\server\...) — a spelling
|
|
96
|
+
# the operator never wrote and some vendor software rejects
|
|
97
|
+
# (plesty-lib#31).
|
|
90
98
|
self._allowed_data_roots: list[Path] = [
|
|
91
99
|
Path(root).expanduser().resolve() for root in (allowed_data_roots or [])
|
|
92
100
|
]
|
|
93
|
-
self.
|
|
94
|
-
Path(
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
self._root_spellings: list[tuple[Path, Path]] = [
|
|
102
|
+
(Path(root).expanduser().resolve(), Path(root).expanduser().absolute())
|
|
103
|
+
for root in (allowed_data_roots or [])
|
|
104
|
+
]
|
|
105
|
+
self._default_data_path: Path | None = None
|
|
106
|
+
if data_path is not None:
|
|
107
|
+
self._default_data_path = Path(data_path).expanduser().resolve()
|
|
108
|
+
self._root_spellings.append(
|
|
109
|
+
(self._default_data_path, Path(data_path).expanduser().absolute())
|
|
110
|
+
)
|
|
97
111
|
if not self._allowed_data_roots:
|
|
98
112
|
self._allowed_data_roots = [self._default_data_path]
|
|
99
113
|
elif not self._is_allowed_data_path(self._default_data_path):
|
|
@@ -102,6 +116,11 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
102
116
|
f"roots {[str(root) for root in self._allowed_data_roots]}."
|
|
103
117
|
)
|
|
104
118
|
self._data_path: Path | None = self._default_data_path
|
|
119
|
+
self._data_path_display: Path | None = (
|
|
120
|
+
self._display_path(self._default_data_path)
|
|
121
|
+
if self._default_data_path is not None
|
|
122
|
+
else None
|
|
123
|
+
)
|
|
105
124
|
|
|
106
125
|
def register_resources(self, resources: list[Any]) -> None:
|
|
107
126
|
"""Declare the physical resources this device owns.
|
|
@@ -160,6 +179,19 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
160
179
|
"""Return whether *path* resolves under one of the allowed data roots."""
|
|
161
180
|
return any(path == root or path.is_relative_to(root) for root in self._allowed_data_roots)
|
|
162
181
|
|
|
182
|
+
def _display_path(self, resolved: Path) -> Path:
|
|
183
|
+
"""Re-base *resolved* onto the configured spelling of its data root.
|
|
184
|
+
|
|
185
|
+
``Path.resolve()`` rewrites a mapped Windows drive to its UNC form,
|
|
186
|
+
which the operator never configured and some vendor software rejects.
|
|
187
|
+
A path under a known root is expressed under that root's configured
|
|
188
|
+
spelling; anything else is returned as it came.
|
|
189
|
+
"""
|
|
190
|
+
for root_resolved, root_display in self._root_spellings:
|
|
191
|
+
if resolved == root_resolved or resolved.is_relative_to(root_resolved):
|
|
192
|
+
return root_display / resolved.relative_to(root_resolved)
|
|
193
|
+
return resolved
|
|
194
|
+
|
|
163
195
|
def _resolve_data_path(self, path: str | os.PathLike[str]) -> Path:
|
|
164
196
|
"""Resolve *path* to an absolute directory, expanding ``~`` and following links.
|
|
165
197
|
|
|
@@ -204,7 +236,9 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
204
236
|
**kwargs: Optional extras such as ``resources``, ignored here.
|
|
205
237
|
|
|
206
238
|
Returns:
|
|
207
|
-
The
|
|
239
|
+
The directory now in effect, in the configured spelling of its data
|
|
240
|
+
root — a mapped drive stays a mapped drive; resolution is used for
|
|
241
|
+
the containment checks only.
|
|
208
242
|
|
|
209
243
|
Raises:
|
|
210
244
|
ValueError: If the resolved path is outside the allowed roots, or the
|
|
@@ -223,8 +257,9 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
223
257
|
)
|
|
224
258
|
resolved.mkdir(parents=True, exist_ok=True)
|
|
225
259
|
self._data_path = resolved
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
self._data_path_display = self._display_path(resolved)
|
|
261
|
+
logging.info(f"Data path set to '{self._data_path_display}'")
|
|
262
|
+
return str(self._data_path_display)
|
|
228
263
|
|
|
229
264
|
@expose_to_api
|
|
230
265
|
def get_data_path(self, **kwargs) -> str:
|
|
@@ -238,8 +273,12 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
238
273
|
**kwargs: Optional extras such as ``resources``, ignored here.
|
|
239
274
|
|
|
240
275
|
Returns:
|
|
241
|
-
The directory in effect: the last one
|
|
242
|
-
device's configured default.
|
|
276
|
+
The directory in effect, in its configured spelling: the last one
|
|
277
|
+
set by a client, or the device's configured default. The directory
|
|
278
|
+
is created if missing — the configured default is otherwise never
|
|
279
|
+
created anywhere (construction is side-effect-free), and vendor
|
|
280
|
+
software validating the directory rejects one that does not exist
|
|
281
|
+
(plesty-lib#31).
|
|
243
282
|
|
|
244
283
|
Raises:
|
|
245
284
|
RuntimeError: If the device has no data path configured.
|
|
@@ -249,7 +288,8 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem, TelemetrySystem):
|
|
|
249
288
|
"This device has no data path configured; pass 'data_path' when "
|
|
250
289
|
"constructing it or call 'set_data_path' first."
|
|
251
290
|
)
|
|
252
|
-
|
|
291
|
+
self._data_path.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
return str(self._data_path_display or self._data_path)
|
|
253
293
|
|
|
254
294
|
def init(self, main=None) -> None:
|
|
255
295
|
"""Initialize the device. This should be called before any other operations."""
|
|
@@ -12,6 +12,8 @@ from collections.abc import Iterable, Mapping
|
|
|
12
12
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
import zmq
|
|
16
|
+
|
|
15
17
|
from plesty.lib.device.base_device_sync import BaseDeviceSyncModel
|
|
16
18
|
from plesty.lib.service import _DeviceTCPIPClient, build_client
|
|
17
19
|
from plesty.lib.utils.settings import EnvSettings
|
|
@@ -383,6 +385,11 @@ class CompositeDevice:
|
|
|
383
385
|
raise RuntimeError(f"Cannot reconnect '{dev}': no address was provided for it.")
|
|
384
386
|
client = getattr(self, dev)
|
|
385
387
|
try:
|
|
388
|
+
# Linger 0, or ctx.term() blocks until the undelivered message is
|
|
389
|
+
# flushed — and a reconnect happens precisely because a message
|
|
390
|
+
# went undelivered. Measured on the rig: ~45 s per reconnect
|
|
391
|
+
# (plesty-lib#32), turning the recovery ladder into the delay.
|
|
392
|
+
client.socket.setsockopt(zmq.LINGER, 0)
|
|
386
393
|
client.socket.close()
|
|
387
394
|
client.ctx.term()
|
|
388
395
|
except Exception: # noqa: BLE001 - best-effort teardown of a dead client
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
from .base_experiment import Experiment, InvalidOperationError, PlanMismatchError
|
|
9
9
|
from .journal import Journal
|
|
10
|
+
from .runs import Run, latest_run, list_runs, parse_run_id, run_id
|
|
10
11
|
from .schedule import Plan, Step
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
@@ -15,5 +16,10 @@ __all__ = [
|
|
|
15
16
|
"PlanMismatchError",
|
|
16
17
|
"Journal",
|
|
17
18
|
"Plan",
|
|
19
|
+
"Run",
|
|
18
20
|
"Step",
|
|
21
|
+
"latest_run",
|
|
22
|
+
"list_runs",
|
|
23
|
+
"parse_run_id",
|
|
24
|
+
"run_id",
|
|
19
25
|
]
|