plesty-lib 0.3.4__tar.gz → 0.3.5.dev2__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 (162) hide show
  1. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/CHANGELOG.md +97 -0
  2. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/PKG-INFO +1 -1
  3. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/composite_device.md +51 -1
  4. plesty_lib-0.3.5.dev2/docs/experiment.md +218 -0
  5. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/monitor.md +60 -6
  6. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/gl-secret-detection-report.json +5 -5
  7. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/__init__.py +13 -1
  8. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/io.py +149 -22
  9. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/composite_device.py +400 -26
  10. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/__init__.py +9 -0
  11. plesty_lib-0.3.5.dev2/plesty/lib/experiment/assets/preflight.yaml +20 -0
  12. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/base_experiment.py +129 -27
  13. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/journal.py +2 -2
  14. plesty_lib-0.3.5.dev2/plesty/lib/experiment/preflight.py +168 -0
  15. plesty_lib-0.3.5.dev2/plesty/lib/experiment/runs.py +356 -0
  16. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/__init__.py +2 -0
  17. plesty_lib-0.3.5.dev2/plesty/lib/monitor/assets/viz.yaml +37 -0
  18. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/sources.py +90 -60
  19. plesty_lib-0.3.5.dev2/plesty/lib/monitor/viz.py +261 -0
  20. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/tcp_ip_client.py +25 -4
  21. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/shell.py +7 -2
  22. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/logger.py +54 -20
  23. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_composite_config.py +280 -4
  24. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_base.py +4 -0
  25. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_experiment.py +48 -6
  26. plesty_lib-0.3.5.dev2/tests/test_experiment_preflight.py +114 -0
  27. plesty_lib-0.3.5.dev2/tests/test_experiment_runs.py +218 -0
  28. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_monitor.py +47 -14
  29. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_utils.py +36 -0
  30. plesty_lib-0.3.5.dev2/tests/test_viz.py +151 -0
  31. plesty_lib-0.3.4/docs/experiment.md +0 -119
  32. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/.gitignore +0 -0
  33. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/.gitlab-ci.yml +0 -0
  34. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/LICENSE +0 -0
  35. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  36. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/README.md +0 -0
  37. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/REUSE.toml +0 -0
  38. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/analyzer.md +0 -0
  39. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/api_reference.md +0 -0
  40. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/architecture.md +0 -0
  41. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/contributing.md +0 -0
  42. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/data_schemas.md +0 -0
  43. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/base_device.md +0 -0
  44. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/cmd_solver.md +0 -0
  45. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/error_handling.md +0 -0
  46. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/func_system.md +0 -0
  47. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/index.md +0 -0
  48. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/logging_system.md +0 -0
  49. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/param_system.md +0 -0
  50. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/test_helper.md +0 -0
  51. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/traffic_manager.md +0 -0
  52. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/device_layers.svg +0 -0
  53. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/device_standard.svg +0 -0
  54. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/plesty_framework.svg +0 -0
  55. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/index.md +0 -0
  56. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/quickstart.md +0 -0
  57. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/toc.yaml +0 -0
  58. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/ui.md +0 -0
  59. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/__init__.py +0 -0
  60. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/analyzer/__init__.py +0 -0
  61. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/analyzer/base_analyzer.py +0 -0
  62. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/array.py +0 -0
  63. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/ctype_manager.py +0 -0
  64. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/table.py +0 -0
  65. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/types.py +0 -0
  66. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/units.py +0 -0
  67. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/__init__.py +0 -0
  68. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/async_wrapper.py +0 -0
  69. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_apt_device.py +0 -0
  70. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_device_sync.py +0 -0
  71. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
  72. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_visa_scpi_device.py +0 -0
  73. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/device_utils.py +0 -0
  74. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/doc.py +0 -0
  75. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/funcs.py +0 -0
  76. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/params.py +0 -0
  77. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/telemetry.py +0 -0
  78. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/schedule.py +0 -0
  79. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/base_monitor.py +0 -0
  80. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/__init__.py +0 -0
  81. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/resource_manager.py +0 -0
  82. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/tcp_ip_server.py +0 -0
  83. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/__init__.py +0 -0
  84. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/apt.py +0 -0
  85. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/data_generator.py +0 -0
  86. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/demo_device.py +0 -0
  87. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/__init__.py +0 -0
  88. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/iceblock.py +0 -0
  89. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/scpi.py +0 -0
  90. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/__init__.py +0 -0
  91. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/analyzer_pipeline.py +0 -0
  92. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/client_field_test.py +0 -0
  93. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_func_system.py +0 -0
  94. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_param_system.py +0 -0
  95. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_pipeline.py +0 -0
  96. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/experiment_pipeline.py +0 -0
  97. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/field_test.py +0 -0
  98. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/field_test_concurrency.py +0 -0
  99. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/grouped_param_keys.py +0 -0
  100. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/report_artifact.py +0 -0
  101. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/resource_allocation.py +0 -0
  102. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/schema_params.py +0 -0
  103. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/schema_refresh.py +0 -0
  104. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/__init__.py +0 -0
  105. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/apt.py +0 -0
  106. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/apt_protocol.py +0 -0
  107. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/serial.py +0 -0
  108. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/tcp_ip.py +0 -0
  109. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/usb_utils.py +0 -0
  110. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/utils.py +0 -0
  111. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/visa.py +0 -0
  112. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/__init__.py +0 -0
  113. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/assets/palette.json +0 -0
  114. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/assets/shell.qss +0 -0
  115. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/monitor_panel.py +0 -0
  116. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/panel.py +0 -0
  117. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/__init__.py +0 -0
  118. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/app.py +0 -0
  119. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/plot.py +0 -0
  120. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/recorder.py +0 -0
  121. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/theme.py +0 -0
  122. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/__init__.py +0 -0
  123. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/config.py +0 -0
  124. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/dll_utils.py +0 -0
  125. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/error_utils.py +0 -0
  126. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/registry.py +0 -0
  127. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/settings.py +0 -0
  128. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/pyproject.toml +0 -0
  129. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_analyzer.py +0 -0
  130. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_analyzer_pipeline.py +0 -0
  131. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_async_wrapper_threading.py +0 -0
  132. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_client_field_test.py +0 -0
  133. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_config.py +0 -0
  134. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_array.py +0 -0
  135. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_io_hdf5.py +0 -0
  136. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_types.py +0 -0
  137. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_demo_device.py +0 -0
  138. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_apt.py +0 -0
  139. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_data_path.py +0 -0
  140. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_funcs.py +0 -0
  141. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_params.py +0 -0
  142. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_pipeline.py +0 -0
  143. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_scpi.py +0 -0
  144. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_telemetry.py +0 -0
  145. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_experiment_pipeline.py +0 -0
  146. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_field_test.py +0 -0
  147. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_misc.py +0 -0
  148. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_report_artifact.py +0 -0
  149. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_schema_params.py +0 -0
  150. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_schema_refresh.py +0 -0
  151. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_service_manager.py +0 -0
  152. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_settings.py +0 -0
  153. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_shipped_imports.py +0 -0
  154. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_sim.py +0 -0
  155. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_solver.py +0 -0
  156. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_tcp_ip_resources.py +0 -0
  157. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_drivers.py +0 -0
  158. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_orphan.py +0 -0
  159. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_utils.py +0 -0
  160. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_ui.py +0 -0
  161. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_visa_discovery.py +0 -0
  162. {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/uv.lock +0 -0
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5.dev2 — 2026-08-17
4
+
5
+ Development pre-release for field-testing the rig start-up (#36, #37):
6
+ bounded, visible connects, the preflight, and default logging. Same line as
7
+ 0.3.5.dev1 — the final release is 0.4.0.
8
+
9
+ - **A run starts only on a rig that is really there** (pol-pl field
10
+ finding: a sweep went on with the powermeter server absent).
11
+ `CompositeDevice.preflight()` checks that every remote sub-device exposes
12
+ the methods listed under the new `requires` config key and answers
13
+ `identity()`; `Experiment.setup()` runs it and raises `RigNotReadyError`
14
+ on any problem. `plesty.lib.experiment.connect_rig(build, optional=…)`
15
+ is the start-up for every experiment's `__main__`: build, preflight, and
16
+ on a failure print which device, at which address, from which variable,
17
+ then ask `[r]etry / [a]bort / [s]kip` (skip only for optional
18
+ sub-devices, returned as `skipped`); non-interactive processes get the
19
+ diagnosis logged and `RigNotReadyError`. Text in
20
+ `experiment/assets/preflight.yaml`.
21
+ - **A composite that is given up closes its clients.** New
22
+ `CompositeDevice.release()` closes every remote client on its owning
23
+ thread (server-side disconnect with a 1 s window, linger 0) and stops the
24
+ threads; `disconnect_all()` does the same for remote clients, and a first
25
+ connect that fails closes the clients that did connect. A client left to
26
+ the garbage collector blocked the interpreter in its ZMQ context's
27
+ `__del__` — seen mid-run after `connect_rig` rebuilt a rig without the
28
+ powermeter. `connect_rig` releases a rig it gives up.
29
+ - **A first connect to a dead server now returns.** The client's connect
30
+ failure path closed its DEALER socket with the default infinite LINGER,
31
+ so `ctx.term()` blocked forever on the undelivered handshake — a rig built
32
+ against one absent server hung with no output, and no attempt ladder or
33
+ deadline could bound it. `_DeviceTCPIPClient` now drops undelivered
34
+ messages (linger 0) when a connect fails and on `close()`.
35
+ `CompositeDevice` also resolves every address before starting any connect
36
+ (a half-configured `.env` raises `ValueError` without touching a server)
37
+ and stops its worker threads when a first connect fails.
38
+ - **`CompositeDevice` signals sub-device connection state and bounds the first
39
+ connect** (#36). Each sub-device carries a `ConnectionState`
40
+ (`CONNECTING → CONNECTED → DEGRADED → DISCONNECTED`), read with
41
+ `status()` / `status(dev)` and observed with `on_status_change(callback)`;
42
+ every transition is logged (WARNING for `DEGRADED` / `DISCONNECTED`).
43
+ New config key `connect_deadline_s` (default 10 s): a server that does not
44
+ answer within it raises `DeviceUnreachableError(dev, address, elapsed_s)`
45
+ from the constructor instead of running attempts × timeout × backoff
46
+ (~40 s per dead server before). Under thread affinity the first connects
47
+ run in parallel on the sub-devices' own threads. `reconnect()` reuses the
48
+ sub-device's configured `timeout_ms` / `attempts` / `backoff_s` instead of
49
+ the 50 s defaults; a call timeout marks the sub-device `DEGRADED` before
50
+ the rebuild. `_DeviceTCPIPClient.set_default_timeout(ms)` added.
51
+ - **Default logging for experiments and composites** (#37): nothing is
52
+ silent anymore without module code. `Experiment` logs
53
+ `Run <id>: N step(s) (k already completed), run dir …`, `Setup: …`,
54
+ `Step i/N <id> (<op>)` / `Step i/N done in x s`, `Teardown: …`, and
55
+ `Run … completed (N steps) in x s`. `CompositeDevice` logs
56
+ `Connecting <dev> @ <addr>` / `Connected <dev> @ <addr> in x s`, each failed
57
+ attempt with the retry delay, `Reconnecting <dev> @ <addr>`, and the timeout
58
+ a retried call ran under. New `plesty.lib.utils.logger.ensure_logging()`
59
+ (root level + one console sink, idempotent) is called by both constructors;
60
+ `setup_logging(app)` builds on it and adds the per-process log file — the
61
+ first `app` names the file, so a rig built before its experiment no longer
62
+ steals the file name.
63
+
64
+ ## 0.3.5.dev1 — 2026-08-17
65
+
66
+ Development pre-release for field-testing the run convention (#34); the
67
+ final release of this line is 0.4.0 — the API diff against 0.3.4 (the
68
+ `pattern=` arguments and `data_dir` attributes of the run sources are
69
+ gone, `Experiment(run_root=)` defaults to the mount) is a minor bump.
70
+
71
+ - `plesty.lib.monitor.Viz` (#34): the `monitor` / `render` command line over
72
+ an experiment's panels. The experiment registers one function
73
+ (`@viz.panels`, `Run` → panels) and calls `viz.main()`; run discovery,
74
+ `--run-root/--mount/--data-dir/--run`, live vs `--replay`, `--record`,
75
+ offscreen `render` with self-stop, and the text catalog
76
+ (`monitor/assets/viz.yaml`) are the lib's. `Run(..., data_dir=)` supplies
77
+ the host spelling for runs that did not record it.
78
+ - **Run layout: one `records.jsonl` per run instead of one `data/step_NNNN.json`
79
+ per step** (#34). Each completed step appends one fsynced line (its
80
+ `ResultDocument` + `index`; blobs still go to `data/`), and readers tail
81
+ the file by byte offset — writing and following a long run on a network
82
+ share no longer costs a file open and a directory listing per step. New:
83
+ `plesty.lib.data.append_record` / `read_records` / `record_value`;
84
+ `RunSource`/`ReplaySource` lose their `pattern` argument and gain `unread`
85
+ and `legacy`; the journal's `step_completed` carries `recorded` instead of
86
+ a `result` path; a resumed run also counts steps with a committed record
87
+ as done. Runs in the old layout are still read (`RunSource.legacy`).
88
+ - The run convention is spelled out once and exported (#34):
89
+ `plesty.lib.experiment.runs` — `run_id`/`parse_run_id`, `list_runs`,
90
+ `latest_run`, and `Run` (plan, config, journal status, records, and
91
+ `local()` translation of device-written paths onto this machine).
92
+ - One environment convention for the shared disk: `PLESTY_DATA_DIR` (as the
93
+ acquiring host sees it) and `PLESTY_DATA_MOUNT` (as this machine sees it).
94
+ `Experiment` reads both (or `data_dir=`/`data_mount=`), defaults
95
+ `run_root` to the mount when set, exposes `self.run_id`/`self.run_dir`
96
+ before `setup()` and `self.raw_dir` = `<data_dir>/<run_id>/raw` for the
97
+ writing devices, and journals all three with `run_started`. `run_root`
98
+ still defaults to `runs` without a mount.
99
+
3
100
  ## 0.3.4 — 2026-08-14
4
101
 
5
102
  - 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.dev2
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>
@@ -60,7 +60,9 @@ composite = CompositeDevice(
60
60
  |---|---|
61
61
  | `address` | Explicit ZMQ address, skipping the environment |
62
62
  | `env` | Environment variable to read, overriding the derived name |
63
- | `timeout_ms`, `attempts`, `backoff_s` | Forwarded to `connect_client` |
63
+ | `timeout_ms`, `attempts`, `backoff_s` | Forwarded to `connect_client` (first connect and every reconnect) |
64
+ | `connect_deadline_s` | Budget for the *first* connect (default 10 s); past it the constructor raises `DeviceUnreachableError` |
65
+ | `requires` | Remote methods the server must expose; checked by `preflight()` (a wrong address connects fine but is not this device) |
64
66
 
65
67
  The address comes from `address` if given, else the environment; an unset
66
68
  variable raises a `ValueError` naming the variable to set.
@@ -106,10 +108,48 @@ explicitly.
106
108
  `connect_client(address)` is the module-level builder used for the rebuilds:
107
109
  `build_client` plus a connection-handshake retry with linear backoff, which
108
110
  matters when several clients open back-to-back against the same server.
111
+ `reconnect(dev)` rebuilds with the sub-device's own `timeout_ms` /
112
+ `attempts` / `backoff_s`, not the defaults.
109
113
 
110
114
  Every multi-device composite gets this robustness for free instead of
111
115
  re-implementing it per module.
112
116
 
117
+ ## Connection state and a bounded first connect
118
+
119
+ The composite is never silent about its servers. Constructing it logs one
120
+ line per sub-device (`Connecting hwp @ tcp://… (timeout 5000 ms) …`,
121
+ `Connected hwp @ tcp://… in 0.4 s`, or `Connect to tcp://… failed, attempt
122
+ 1/4: … ; retrying in 2 s`) and a summary (`PolPlRig ready in 1.2 s:
123
+ hwp=tcp://…, spec=tcp://…`) — the console sink is switched on by the
124
+ composite itself (`ensure_logging()`), so this shows even when the composite
125
+ is built before the experiment that would configure logging.
126
+
127
+ The **first connect is bounded**: each configured sub-device gets
128
+ `connect_deadline_s` (default 10 s); a server that does not answer within it
129
+ raises `DeviceUnreachableError(dev, address, elapsed_s)` from the constructor
130
+ instead of grinding through attempts × timeout × backoff. Under thread
131
+ affinity the first connects run in parallel on the sub-devices' own threads,
132
+ so three dead servers cost one deadline, not three. Reconnects during a run
133
+ keep the attempt ladder — a server busy finishing a long move must be waited
134
+ for, not declared dead.
135
+
136
+ Each sub-device carries a `ConnectionState` — `CONNECTING → CONNECTED →
137
+ DEGRADED` (a call timed out, the client is being rebuilt) `→ DISCONNECTED`
138
+ (connect/reconnect failed, or `disconnect_all()`):
139
+
140
+ ```python
141
+ composite.status() # {"hwp": ConnectionState.CONNECTED, "spec": ...}
142
+ composite.status("hwp") # ConnectionState.CONNECTED
143
+
144
+ @composite.on_status_change
145
+ def _show(dev, old, new, detail):
146
+ ui.set_led(dev, new) # runs on the thread that changed the state
147
+ ```
148
+
149
+ Every transition is logged (INFO, or WARNING for `DEGRADED` /
150
+ `DISCONNECTED`) whether or not anyone subscribes; a subscriber that raises
151
+ is logged and ignored — a broken UI hook must not break the rig.
152
+
113
153
  ## One thread per sub-device
114
154
 
115
155
  ZMQ sockets are not thread-safe, and even migrating one between threads is
@@ -161,6 +201,16 @@ for a data directory either fails or means nothing:
161
201
  composite.set_data_path(frames_dir, devices=["spec"])
162
202
  ```
163
203
 
204
+ ## Preflight
205
+
206
+ `preflight()` proves each sub-device is the device it is supposed to be and
207
+ answers: a remote sub-device must expose every method in its `requires`
208
+ list and its `identity()` must reply; a local one must have the required
209
+ attributes. It returns `{name: problem}` for the failures (empty when the
210
+ rig is ready) and logs one line per sub-device either way. `Experiment`
211
+ runs it in `setup()`; `plesty.lib.experiment.connect_rig` runs it at
212
+ start-up and asks the operator what to do (see the experiment guide).
213
+
164
214
  ## Interplay with experiments
165
215
 
166
216
  `Experiment` accepts a `CompositeDevice` at construction; its default
@@ -0,0 +1,218 @@
1
+ # Experiment Framework
2
+
3
+ `plesty.lib.experiment` turns a measurement routine into a **reproducible,
4
+ crash-tolerant run**. The mental model is checkpointed training in deep
5
+ learning: the *plan* is the training schedule, the *journal* is the training
6
+ log, and the completed-step set plus persisted step results form the
7
+ checkpoint.
8
+
9
+ An `Experiment` orchestrates devices through a
10
+ [`CompositeDevice`](device/composite_device.md) — it receives Device
11
+ instances, never raw hardware.
12
+
13
+ ## The pieces
14
+
15
+ | Piece | Role |
16
+ |---|---|
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
+ | `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
+ | `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. |
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`). |
23
+
24
+ Every run owns a directory:
25
+
26
+ ```text
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
33
+ ```
34
+
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`.
43
+
44
+ ## Writing an experiment
45
+
46
+ Subclasses override exactly three lifecycle hooks — `build_plan()`
47
+ (required), `setup()`/`teardown()` (optional, extend the defaults which
48
+ connect/disconnect all devices) — plus one public method per step operation:
49
+
50
+ ```python
51
+ import asyncio
52
+ from plesty.lib.data import PlestyArray
53
+ from plesty.lib.experiment import Experiment, Plan, Step
54
+
55
+ class LineScan(Experiment):
56
+ def build_plan(self) -> Plan:
57
+ steps = [
58
+ Step(id=f"scan[x={x}]", op="scan_point", params={"x": x})
59
+ for x in range(5)
60
+ ]
61
+ return Plan(steps, config={"points": 5})
62
+
63
+ def scan_point(self, x: int) -> PlestyArray:
64
+ # drive devices via self.devices, return the measurement
65
+ return PlestyArray([float(x)], name="signal", unit="a.u.")
66
+
67
+ run_id = asyncio.run(LineScan(run_root="runs").run())
68
+ ```
69
+
70
+ Rules that keep runs reproducible:
71
+
72
+ - **`build_plan()` must be deterministic** for a given configuration —
73
+ resume compares the regenerated plan against the persisted one by content
74
+ hash.
75
+ - **Step ids must be stable** (e.g. `"scan[x=3,y=5]"`) — never derived from
76
+ run time or randomness; resume matches completed steps by id.
77
+ - **Step operations are validated before anything runs**: every `Step.op`
78
+ must name an existing public method and may not be a reserved lifecycle
79
+ method (`build_plan`, `run`, `setup`, `teardown`). Violations raise
80
+ `InvalidOperationError` listing all offending steps — before any device is
81
+ touched or journal written.
82
+
83
+ Step methods may be sync or async; results are awaited automatically. Each
84
+ step is retried (`max_retries`, `retry_sleep` constructor arguments) before
85
+ the run aborts.
86
+
87
+ ## Starting on a rig that is really there
88
+
89
+ An experiment's `__main__` builds the rig with `connect_rig` — the same
90
+ start for every experiment: connect the servers, run the composite's
91
+ `preflight()` (each sub-device exposes the methods its configuration
92
+ `requires` and answers `identity()`), and if anything is missing, tell the
93
+ operator which device, at which address, from which variable — and ask:
94
+
95
+ ```python
96
+ from plesty.lib.experiment import connect_rig
97
+
98
+ rig, skipped = connect_rig(
99
+ lambda skip: PolPlRig(with_powermeter="pm" not in skip),
100
+ optional={"pm"}, # the experiment can run without it
101
+ )
102
+ ```
103
+
104
+ ```text
105
+ The rig is not ready — 1 device(s) failed the preflight:
106
+ - pm: no answer from tcp://127.0.0.1:5557: TimeoutError('Server did not respond in time')
107
+ address tcp://127.0.0.1:5557 (from PM_ADDRESS in .env / the environment)
108
+ Check that the pm server(s) are running and that PM_ADDRESS point at them
109
+ (a wrong port connects fine but is not the expected device).
110
+ [r] retry (after fixing .env or starting the server) [a] abort [s] continue without pm
111
+ >
112
+ ```
113
+
114
+ `r` rebuilds the rig (an edited `.env` or a freshly started server is picked
115
+ up), `a` raises `RigNotReadyError`, `s` — offered only when every failing
116
+ sub-device is in `optional` — returns the rig without them and names them in
117
+ `skipped`, so the experiment can drop the matching part of its plan. Without
118
+ a terminal on stdin nothing waits: the diagnosis is logged and
119
+ `RigNotReadyError` raised.
120
+
121
+ The check also runs inside `Experiment.setup()`: a run never starts on a
122
+ composite whose `preflight()` reports a problem, whether or not `connect_rig`
123
+ was used. Declare what each sub-device must be able to do in its
124
+ configuration — that is what turns "the port answers" into "it is the
125
+ stage":
126
+
127
+ ```yaml
128
+ devices:
129
+ hwp: {timeout_ms: 5000, requires: [home_stage, move_absolute, get_position]}
130
+ spec: {timeout_ms: 5000, requires: [acquire, get_recent_file, write, query]}
131
+ pm: {timeout_ms: 5000, requires: [measure_power, write]}
132
+ ```
133
+
134
+ ## Data on a shared disk
135
+
136
+ Blobs a device writes itself (spectrometer frames, camera images) never
137
+ cross the network: the acquiring host saves them to a shared disk and the
138
+ step record carries only the path. Two machines spell that share
139
+ differently, so it is named once, by environment:
140
+
141
+ | Variable | Meaning |
142
+ |---|---|
143
+ | `PLESTY_DATA_DIR` | the share as the acquiring host sees it (e.g. `G:\RAWDATA`) |
144
+ | `PLESTY_DATA_MOUNT` | the same share as this machine sees it (e.g. `/mnt/group/RAWDATA`) |
145
+
146
+ The experiment reads both (or takes `data_dir=` / `data_mount=` at
147
+ construction). With the mount set, `run_root` defaults to it, so runs land
148
+ on the share as well. Before `setup()` is awaited the run id is known and
149
+ `self.raw_dir` names `<data_dir>/<run_id>/raw` in the host's spelling — one
150
+ folder per run, the same one on resume — which `setup()` routes the writing
151
+ devices to:
152
+
153
+ ```python
154
+ async def setup(self) -> None:
155
+ await super().setup()
156
+ if self.raw_dir is not None:
157
+ self.devices.set_data_path(self.raw_dir, devices=["spec"])
158
+ ```
159
+
160
+ `run_started` journals `data_dir`, `data_mount`, and `raw_dir`, so a reader
161
+ needs at most its own mount:
162
+
163
+ ```python
164
+ from plesty.lib.experiment import Run, latest_run
165
+
166
+ run = Run(latest_run("/mnt/group/RAWDATA", name="pol_pl"))
167
+ run.config["powermeter"] # the frozen configuration
168
+ run.status()["state"] # "running", "completed", ...
169
+ for record in run.records():
170
+ run.local(record["data_file"]) # G:\RAWDATA\... → /mnt/group/RAWDATA/...
171
+ ```
172
+
173
+ `Run.local` uses, in order, an explicit `mount=`, `PLESTY_DATA_MOUNT`, and
174
+ the mount journaled by the machine that ran the experiment (only when that
175
+ directory exists here); with neither a data root nor a mount the path is
176
+ returned as recorded — the case of reading on the acquiring host itself.
177
+ `run.source(mapper)` returns a `RunSource` (live) or, with
178
+ `Run(..., replay_rows=n)`, a `ReplaySource` — see [Monitors](monitor.md).
179
+
180
+ ## Crash, cancel, resume
181
+
182
+ `run()` returns the run id. If the run is interrupted — `Ctrl-C`, a crash,
183
+ an aborting step — pass the id back to continue:
184
+
185
+ ```python
186
+ run_id = asyncio.run(LineScan(run_root="runs").run())
187
+ # ... interrupted?
188
+ asyncio.run(LineScan(run_root="runs").run(resume=run_id))
189
+ ```
190
+
191
+ On resume the framework:
192
+
193
+ 1. regenerates the plan and **refuses to continue if its content hash
194
+ differs** from the stored one (`PlanMismatchError`) — a silently changed
195
+ schedule or configuration cannot corrupt a run;
196
+ 2. replays the journal to find completed steps and skips them;
197
+ 3. journals the resume (`run_started` with `resumed: true`) and continues.
198
+
199
+ `teardown()` is always awaited — on completion, failure, and cancellation —
200
+ so devices are released no matter how the run ends.
201
+
202
+ ## Contract tests without hardware
203
+
204
+ `plesty.lib.test.experiment_pipeline.ExperimentPipeline` verifies the
205
+ plan/checkpoint contract of an experiment module with **no instrument
206
+ attached** (it never calls `run()`). Five gates:
207
+
208
+ | Gate | Checks |
209
+ |---|---|
210
+ | 1 `test_experiment_subclass` | public `Experiment` subclass, instantiable without devices |
211
+ | 2 `test_plan_deterministic` | `build_plan()` twice → identical `content_hash()` |
212
+ | 3 `test_plan_ops_resolve` | every `Step.op` resolves via the framework's own plan validation |
213
+ | 4 `test_plan_serializable` | `Plan.save`/`load` round-trip preserves the hash |
214
+ | 5 `test_lifecycle_hooks` | `setup`/`teardown` overrides are async |
215
+
216
+ Experiment HUB modules expose one pytest function per gate (SDK gate E1
217
+ verifies their presence and passage — the experiment-tier mirror of device
218
+ gate d1); see [Test Helpers](device/test_helper.md) for the pattern.
@@ -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-17T15:29:32",
23
+ "end_time": "2026-08-17T15:29:32",
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.45769703,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 662,
32
+ "repo_size_kb": 708,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 24,
34
+ "bytes_scanned": 220,
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",