plesty-lib 0.3.4__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.
Files changed (158) hide show
  1. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/CHANGELOG.md +36 -0
  2. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/PKG-INFO +1 -1
  3. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/experiment.md +62 -10
  4. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/monitor.md +60 -6
  5. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/gl-secret-detection-report.json +5 -5
  6. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/__init__.py +13 -1
  7. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/io.py +149 -22
  8. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/__init__.py +6 -0
  9. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/base_experiment.py +86 -24
  10. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/journal.py +2 -2
  11. plesty_lib-0.3.5.dev1/plesty/lib/experiment/runs.py +356 -0
  12. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/__init__.py +2 -0
  13. plesty_lib-0.3.5.dev1/plesty/lib/monitor/assets/viz.yaml +37 -0
  14. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/sources.py +90 -60
  15. plesty_lib-0.3.5.dev1/plesty/lib/monitor/viz.py +261 -0
  16. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/shell.py +7 -2
  17. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_experiment.py +18 -6
  18. plesty_lib-0.3.5.dev1/tests/test_experiment_runs.py +218 -0
  19. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_monitor.py +47 -14
  20. plesty_lib-0.3.5.dev1/tests/test_viz.py +151 -0
  21. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/.gitignore +0 -0
  22. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/.gitlab-ci.yml +0 -0
  23. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/LICENSE +0 -0
  24. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  25. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/README.md +0 -0
  26. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/REUSE.toml +0 -0
  27. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/analyzer.md +0 -0
  28. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/api_reference.md +0 -0
  29. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/architecture.md +0 -0
  30. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/contributing.md +0 -0
  31. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/data_schemas.md +0 -0
  32. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/base_device.md +0 -0
  33. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/cmd_solver.md +0 -0
  34. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/composite_device.md +0 -0
  35. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/error_handling.md +0 -0
  36. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/func_system.md +0 -0
  37. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/index.md +0 -0
  38. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/logging_system.md +0 -0
  39. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/param_system.md +0 -0
  40. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/test_helper.md +0 -0
  41. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/device/traffic_manager.md +0 -0
  42. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/imgs/device_layers.svg +0 -0
  43. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/imgs/device_standard.svg +0 -0
  44. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/imgs/plesty_framework.svg +0 -0
  45. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/index.md +0 -0
  46. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/quickstart.md +0 -0
  47. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/toc.yaml +0 -0
  48. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/docs/ui.md +0 -0
  49. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/__init__.py +0 -0
  50. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/analyzer/__init__.py +0 -0
  51. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/analyzer/base_analyzer.py +0 -0
  52. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/array.py +0 -0
  53. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/ctype_manager.py +0 -0
  54. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/table.py +0 -0
  55. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/types.py +0 -0
  56. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/data/units.py +0 -0
  57. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/__init__.py +0 -0
  58. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/async_wrapper.py +0 -0
  59. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_apt_device.py +0 -0
  60. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_device_sync.py +0 -0
  61. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
  62. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/base_visa_scpi_device.py +0 -0
  63. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/composite_device.py +0 -0
  64. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/device_utils.py +0 -0
  65. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/doc.py +0 -0
  66. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/funcs.py +0 -0
  67. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/params.py +0 -0
  68. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/device/telemetry.py +0 -0
  69. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/experiment/schedule.py +0 -0
  70. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/monitor/base_monitor.py +0 -0
  71. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/service/__init__.py +0 -0
  72. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/service/resource_manager.py +0 -0
  73. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/service/tcp_ip_client.py +0 -0
  74. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/service/tcp_ip_server.py +0 -0
  75. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/__init__.py +0 -0
  76. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/apt.py +0 -0
  77. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/data_generator.py +0 -0
  78. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/sim/demo_device.py +0 -0
  79. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/__init__.py +0 -0
  80. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/iceblock.py +0 -0
  81. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/solver/scpi.py +0 -0
  82. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/__init__.py +0 -0
  83. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/analyzer_pipeline.py +0 -0
  84. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/client_field_test.py +0 -0
  85. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_func_system.py +0 -0
  86. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_param_system.py +0 -0
  87. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/device_pipeline.py +0 -0
  88. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/experiment_pipeline.py +0 -0
  89. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/field_test.py +0 -0
  90. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/field_test_concurrency.py +0 -0
  91. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/grouped_param_keys.py +0 -0
  92. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/report_artifact.py +0 -0
  93. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/resource_allocation.py +0 -0
  94. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/schema_params.py +0 -0
  95. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/test/schema_refresh.py +0 -0
  96. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/__init__.py +0 -0
  97. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/apt.py +0 -0
  98. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/apt_protocol.py +0 -0
  99. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/serial.py +0 -0
  100. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/tcp_ip.py +0 -0
  101. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/usb_utils.py +0 -0
  102. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/utils.py +0 -0
  103. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/traffic/visa.py +0 -0
  104. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/__init__.py +0 -0
  105. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/assets/palette.json +0 -0
  106. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/assets/shell.qss +0 -0
  107. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/monitor_panel.py +0 -0
  108. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/panel.py +0 -0
  109. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/__init__.py +0 -0
  110. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/app.py +0 -0
  111. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/plot.py +0 -0
  112. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/qt/recorder.py +0 -0
  113. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/ui/theme.py +0 -0
  114. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/__init__.py +0 -0
  115. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/config.py +0 -0
  116. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/dll_utils.py +0 -0
  117. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/error_utils.py +0 -0
  118. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/logger.py +0 -0
  119. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/registry.py +0 -0
  120. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/plesty/lib/utils/settings.py +0 -0
  121. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/pyproject.toml +0 -0
  122. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_analyzer.py +0 -0
  123. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_analyzer_pipeline.py +0 -0
  124. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_async_wrapper_threading.py +0 -0
  125. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_client_field_test.py +0 -0
  126. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_composite_config.py +0 -0
  127. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_config.py +0 -0
  128. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_data_array.py +0 -0
  129. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_data_io_hdf5.py +0 -0
  130. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_data_types.py +0 -0
  131. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_demo_device.py +0 -0
  132. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_apt.py +0 -0
  133. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_base.py +0 -0
  134. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_data_path.py +0 -0
  135. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_funcs.py +0 -0
  136. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_params.py +0 -0
  137. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_pipeline.py +0 -0
  138. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_scpi.py +0 -0
  139. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_device_telemetry.py +0 -0
  140. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_experiment_pipeline.py +0 -0
  141. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_field_test.py +0 -0
  142. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_misc.py +0 -0
  143. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_report_artifact.py +0 -0
  144. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_schema_params.py +0 -0
  145. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_schema_refresh.py +0 -0
  146. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_service_manager.py +0 -0
  147. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_settings.py +0 -0
  148. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_shipped_imports.py +0 -0
  149. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_sim.py +0 -0
  150. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_solver.py +0 -0
  151. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_tcp_ip_resources.py +0 -0
  152. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_traffic_drivers.py +0 -0
  153. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_traffic_orphan.py +0 -0
  154. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_traffic_utils.py +0 -0
  155. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_ui.py +0 -0
  156. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_utils.py +0 -0
  157. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/tests/test_visa_discovery.py +0 -0
  158. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev1}/uv.lock +0 -0
@@ -1,5 +1,41 @@
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
+
3
39
  ## 0.3.4 — 2026-08-14
4
40
 
5
41
  - The server never cancels a receive (#33): the old
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plesty-lib
3
- Version: 0.3.4
3
+ Version: 0.3.5.dev1
4
4
  Summary: A library for the Plesty ecosystem.
5
5
  Author: Plesty Development Team
6
6
  Author-email: Yunshuang Yuan <yunshuang.yuan@fkp.uni-hannover.de>
@@ -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 # frozen schedule + config + content hash
27
- ├── journal.jsonl # append-only event log
28
- └── data/ # per-step results: raw blob + JSON metadata
29
- ├── step_0000.json
30
- └── step_0000.npy
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
- Step results are persisted with
34
- [`save_result`](data_schemas.md) — the raw blob stays in its native format,
35
- described by a typed `ResultDocument` with provenance (`step_id`, `op`,
36
- `params`).
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>/data/step_*.json` | any running experiment |
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) writes one result document per
91
- completed step, and that document is the commit record of the write, so
92
- following the directory needs no cooperation from the experiment and cannot
93
- disturb it. It also means a viewer started mid-run still sees every earlier
94
- row.
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-14T16:45:05",
23
- "end_time": "2026-08-14T16:45:05",
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.490144528,
29
+ "time_s": 0.469797198,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 662,
32
+ "repo_size_kb": 685,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 24,
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 ResultDocument, save_result, load_result, load_document, convert_to_hdf5
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 ``.json`` file does.
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
- def save_result(
164
- result: Any,
165
- path_stem: str | Path,
166
- provenance: Optional[dict[str, Any]] = None,
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
- Returns:
179
- The path of the JSON metadata document.
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
- Raises:
182
- TypeError: If the result is neither a PlestyArray, bytes, nor
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
 
@@ -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
  ]