plesty-lib 0.2.6__tar.gz → 0.2.8__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 (103) hide show
  1. plesty_lib-0.2.8/CHANGELOG.md +27 -0
  2. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/PKG-INFO +3 -2
  3. plesty_lib-0.2.8/REUSE.toml +21 -0
  4. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/api_reference.md +1 -1
  5. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/quickstart.md +33 -0
  6. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/gl-secret-detection-report.json +7 -7
  7. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/__init__.py +3 -4
  8. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/__init__.py +7 -4
  9. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/array.py +3 -4
  10. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/ctype_manager.py +3 -4
  11. plesty_lib-0.2.8/plesty/lib/data/io.py +210 -0
  12. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/table.py +3 -4
  13. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/types.py +3 -4
  14. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/data/units.py +3 -4
  15. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/__init__.py +3 -4
  16. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/async_wrapper.py +3 -4
  17. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/base_device_sync.py +8 -8
  18. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/base_tcp_scpi_device.py +3 -4
  19. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/base_visa_scpi_device.py +3 -4
  20. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/composite_device.py +5 -0
  21. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/device_utils.py +3 -4
  22. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/funcs.py +3 -5
  23. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/device/params.py +3 -4
  24. plesty_lib-0.2.8/plesty/lib/experiment/__init__.py +19 -0
  25. plesty_lib-0.2.8/plesty/lib/experiment/base_experiment.py +296 -0
  26. plesty_lib-0.2.8/plesty/lib/experiment/journal.py +113 -0
  27. plesty_lib-0.2.8/plesty/lib/experiment/schedule.py +123 -0
  28. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/service/__init__.py +3 -5
  29. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/service/resource_manager.py +5 -0
  30. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/service/tcp_ip_client.py +12 -4
  31. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/service/tcp_ip_server.py +11 -4
  32. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/sim/__init__.py +3 -4
  33. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/sim/data_generator.py +3 -4
  34. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/solver/__init__.py +3 -4
  35. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/solver/iceblock.py +3 -4
  36. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/solver/scpi.py +3 -4
  37. plesty_lib-0.2.8/plesty/lib/test/__init__.py +6 -0
  38. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/device_func_system.py +3 -4
  39. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/device_param_system.py +3 -4
  40. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/device_pipeline.py +3 -4
  41. plesty_lib-0.2.8/plesty/lib/test/experiment_pipeline.py +228 -0
  42. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/grouped_param_keys.py +3 -4
  43. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/resource_allocation.py +5 -0
  44. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/test/schema_params.py +5 -0
  45. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/__init__.py +3 -4
  46. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/serial.py +3 -5
  47. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/tcp_ip.py +4 -5
  48. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/usb_utils.py +3 -4
  49. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/utils.py +6 -7
  50. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/traffic/visa.py +3 -4
  51. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/utils/__init__.py +9 -4
  52. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/utils/dll_utils.py +3 -4
  53. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/utils/error_utils.py +3 -4
  54. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/utils/logger.py +3 -4
  55. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plesty/lib/utils/registry.py +3 -4
  56. plesty_lib-0.2.8/plesty/lib/utils/settings.py +155 -0
  57. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/plestylib/__init__.py +3 -5
  58. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/pyproject.toml +2 -1
  59. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_data_array.py +3 -4
  60. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_data_types.py +3 -2
  61. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_device_base.py +3 -3
  62. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_device_funcs.py +3 -3
  63. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_device_params.py +3 -3
  64. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_device_pipeline.py +3 -3
  65. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_device_scpi.py +3 -3
  66. plesty_lib-0.2.8/tests/test_experiment.py +276 -0
  67. plesty_lib-0.2.8/tests/test_experiment_pipeline.py +115 -0
  68. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_misc.py +3 -3
  69. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_schema_params.py +3 -4
  70. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_service_manager.py +3 -2
  71. plesty_lib-0.2.8/tests/test_settings.py +140 -0
  72. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_sim.py +3 -2
  73. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_solver.py +3 -2
  74. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_tcp_ip_resources.py +82 -0
  75. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_traffic_drivers.py +3 -2
  76. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_traffic_utils.py +35 -6
  77. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/tests/test_utils.py +3 -3
  78. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/uv.lock +2 -406
  79. plesty_lib-0.2.6/CHANGELOG.md +0 -7
  80. plesty_lib-0.2.6/COPYING +0 -674
  81. plesty_lib-0.2.6/LICENSES/GPL-3.0-or-later.txt +0 -674
  82. plesty_lib-0.2.6/plesty/lib/test/__init__.py +0 -7
  83. plesty_lib-0.2.6/spdx.tmpl +0 -4
  84. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/.gitignore +0 -0
  85. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/.gitlab-ci.yml +0 -0
  86. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/LICENSE +0 -0
  87. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  88. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/README.md +0 -0
  89. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/contributing.md +0 -0
  90. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/base_device.md +0 -0
  91. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/cmd_solver.md +0 -0
  92. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/error_handling.md +0 -0
  93. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/func_system.md +0 -0
  94. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/index.md +0 -0
  95. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/logging_system.md +0 -0
  96. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/param_system.md +0 -0
  97. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/test_helper.md +0 -0
  98. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/device/traffic_manager.md +0 -0
  99. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/imgs/device_layers.png +0 -0
  100. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/imgs/device_standard.png +0 -0
  101. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/imgs/plesty_framework.png +0 -0
  102. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/index.md +0 -0
  103. {plesty_lib-0.2.6 → plesty_lib-0.2.8}/docs/toc.yaml +0 -0
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.2.8 — 2026-07-07
4
+
5
+ - Add `plesty.lib.test.experiment_pipeline.ExperimentPipeline` (lib#14) — the experiment-tier counterpart of `DevicePipeline`: five hardware-free contract gates (public `Experiment` subclass instantiable without devices; deterministic `build_plan()` by content hash; every `Step.op` resolves via the framework's own plan validation; `Plan.save`/`load` round-trip preserves the hash; `setup`/`teardown` overrides are async) plus a one-shot `run_mock_pipeline()`. Experiment HUB modules expose one pytest function per gate; SDK gate E1 (plesty-sdk#15) verifies their presence and passage, mirroring gate d1.
6
+
7
+ - `Experiment.run` now validates the plan before anything executes: every `Step.op` must name an existing public callable on the experiment and may not be a reserved lifecycle method (`build_plan`, `run`, `setup`, `teardown`) — violations raise the new `InvalidOperationError` listing all offending steps, before any device is touched or journal written. The class docstring now spells out the override contract (`build_plan` required; `setup`/`teardown` optional extensions).
8
+
9
+ - Add `plesty.lib.experiment` — async `Experiment` ABC with atomic scheduling, status journaling and checkpoint/resume (first step toward lib#3): `Plan`/`Step` define the frozen measurement schedule (content-hashed so a resumed run refuses a changed schedule); `Journal` is a crash-safe append-only JSONL status log replayed to derive run state and the completed-step set; `Experiment` provides the async `setup()`/`run()`/`teardown()` lifecycle with per-step retries, clean handling of cancel/error, and resume via `run(resume=<run_id>)` that skips journaled-complete steps.
10
+ - Add `plesty.lib.data.io` (first step toward lib#4): `save_result`/`load_result` persist measurement results crash-safely — blobs stay in their original raw format (`.npy` for `PlestyArray`, native encoding for image bytes) described by a JSON metadata document (Plesty meta, provenance, timestamp) with a reference link to the blob; `convert_to_hdf5` optionally packs such documents into a unified HDF5 archive (requires the optional `h5py` dependency).
11
+
12
+ - Surface `op_schema`-registered functions through the `describe` endpoint (lib#13): `DeviceTCPIPServer._describe` now merges the `FunctionSystem._functions` registry into the method listing. These ops are installed as instance attributes and were therefore invisible to the MRO-based `@expose_to_api` walk, so remote clients could not discover or bind them (though they were already callable via the generic `call` path). `DeviceTCPIPClient` now binds them as local proxy methods.
13
+ - Migrate licensing to the [REUSE](https://reuse.software/) standard (lib#12, follows plesty-sdk#10): Python sources carry inline SPDX headers with a collective `SPDX-FileCopyrightText` (Plesty Development Team) plus a `SPDX-FileContributor` per author; non-code files are covered by a central `REUSE.toml`. The root `LICENSE` is kept for forge (GitLab) license detection; `COPYING`, `spdx.tmpl` and the unused `GPL-3.0-or-later.txt` were removed. `plesty check` Gate 9 now verifies `reuse lint` and authorship currency.
14
+
15
+ ## 0.2.7 — 2026-07-02
16
+
17
+ - Add `EnvSettings` in `plesty.lib.utils.settings`: a dependency-free helper that loads environment variables (optionally seeded from a `.env` file, process env taking precedence) with typed `get`/`require` access. By default `load()` also registers parsed `.env` values into `os.environ` without overriding existing ones (dotenv-style; pass `export=False` to disable). Generic by design — device modules choose their own variable names and defaults for credential/connection info (lib#11, companion to plesty-sdk#13)
18
+ - Expose `identity`, `check_errors`, `check_operatability`, `reset`, `clear` to the remote API via `@expose_to_api` on `BaseDeviceSyncModel`
19
+ - Fix `TCPIPTrafficManager._send_command`: pass actual `timeout` to `receive_one` instead of the boolean flag `timeout_overrided` (lib#9)
20
+ - Fix `sock_smart_read` JSON framing: exit the read loop when `terminator in msg` (consistent with line-data branch) and return `msg.rstrip()` — the previous `rstrip(terminator)` only stripped `\n`, leaving `\r` and causing the brace check to fail for `\r\n`-terminated responses (e.g. AMC300) (lib#9)
21
+ - Fix `DeviceTCPIPClient._send`: drain stale ZMQ reply after `recv()` timeout (100 ms window) so the client remains usable on the next call (lib#10)
22
+
23
+ ## 2026-06-25
24
+
25
+ - Add `FuncMeta` and `SCPISolver.solve_func` for schema-driven operation dispatch
26
+ - Fix mypy type annotations across all public modules
27
+ - Migrate CI pipeline to `plesty-standard-ci@exp`
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plesty-lib
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: A library for the Plesty ecosystem.
5
+ Author: Plesty Development Team
5
6
  Author-email: Yunshuang Yuan <yunshuang.yuan@fkp.uni-hannover.de>
6
7
  Maintainer-email: Plesty Development Team <yunshuang.yuan@fkp.uni-hannover.de>
7
8
  License-Expression: LGPL-3.0-or-later
8
- License-File: COPYING
9
9
  License-File: LICENSE
10
+ License-File: LICENSES/LGPL-3.0-or-later.txt
10
11
  Requires-Python: >=3.12
11
12
  Requires-Dist: numpy>=1.24
12
13
  Requires-Dist: pyserial>=3.5
@@ -0,0 +1,21 @@
1
+ version = 1
2
+
3
+ # Non-code files (docs, images, configs, the root LICENSE, lockfiles) are
4
+ # licensed here; Python sources carry their own inline SPDX headers. Copyright
5
+ # is held collectively by the Plesty Development Team, with contributors listed
6
+ # below. `reuse lint` (Gate 9 of `plesty check`) verifies full coverage.
7
+
8
+ [[annotations]]
9
+ path = [
10
+ "LICENSE",
11
+ "README.md",
12
+ "CHANGELOG.md",
13
+ "pyproject.toml",
14
+ "uv.lock",
15
+ ".gitignore",
16
+ ".gitlab-ci.yml",
17
+ "docs/**",
18
+ ]
19
+ SPDX-FileCopyrightText = "2026 Plesty Development Team"
20
+ SPDX-FileContributor = "Yunshuang Yuan"
21
+ SPDX-License-Identifier = "LGPL-3.0-or-later"
@@ -11,6 +11,6 @@ Entry page for [Full API](reference/index) documentation.
11
11
  | [`plesty.lib.solver`](reference/plesty/lib/solver/index) | Command and operation solvers |
12
12
  | [`plesty.lib.service`](reference/plesty/lib/service/index) | TCP/IP server/client and service builders |
13
13
  | [`plesty.lib.sim`](reference/plesty/lib/sim/index) | Synthetic data generation for testing |
14
- | [`plesty.lib.utils`](reference/plesty/lib/utils/index) | Shared utilities (logging, errors, registry, DLL helpers) |
14
+ | [`plesty.lib.utils`](reference/plesty/lib/utils/index) | Shared utilities (env settings loader, logging, errors, registry, DLL helpers) |
15
15
  | [`plesty.lib.test`](reference/plesty/lib/test/index) | Test helpers |
16
16
 
@@ -163,6 +163,39 @@ for name in client.get_config_list():
163
163
  print(f"{name}: {value}")
164
164
  ```
165
165
 
166
+ ### Connection Settings (Env-Driven)
167
+
168
+ Credential and connection information — where to reach the device and which port to
169
+ serve it on — belongs in the environment, not hard-coded. `EnvSettings` is a small,
170
+ dependency-free helper that loads environment variables (optionally seeded from a
171
+ `.env` file) with typed access. It is deliberately generic: **each device module
172
+ chooses its own variable names and defaults**; the helper just loads and casts them.
173
+
174
+ ```python
175
+ from plesty.lib.utils import EnvSettings
176
+
177
+ env = EnvSettings.load() # reads ./.env, then the process env (process env wins)
178
+ # By default load() also registers the parsed `.env` values into os.environ
179
+ # (without overriding existing ones), so they are visible to os.getenv and child
180
+ # processes too. Pass export=False for a side-effect-free load.
181
+
182
+ # Precedence per value: CLI argument (override) > env var > default.
183
+ # Credentials use require() — never hard-code them as a default in source.
184
+ host = env.require("AMC300_HOST", override=args.host)
185
+ port = env.require("AMC300_PORT", cast=int, override=args.port)
186
+ # The serving port is not a credential, so a conventional default is fine.
187
+ tcp_port = env.get("AMC300_TCP_PORT", default=5555, cast=int, override=args.tcp_port)
188
+
189
+ device = PowermeterDevice(host, port)
190
+ server = build_server(device, address=f"tcp://*:{tcp_port}")
191
+ ```
192
+
193
+ Pass a parsed CLI argument as `override` (it wins when not `None`, so unset flags
194
+ fall through to the environment). `require()` raises `KeyError` with a clean message
195
+ when a mandatory credential is set neither by argument nor environment. The same
196
+ `.env` then drives both the server and its tests. Flexible operational tuning
197
+ (non-credential parameters) is configured separately, e.g. via a YAML file.
198
+
166
199
  ## Next Reading
167
200
 
168
201
  1. [Parameter system](device/param_system.md)
@@ -7,7 +7,7 @@
7
7
  "vendor": {
8
8
  "name": "GitLab"
9
9
  },
10
- "version": "7.37.0"
10
+ "version": "7.38.0"
11
11
  },
12
12
  "scanner": {
13
13
  "id": "gitleaks",
@@ -19,19 +19,19 @@
19
19
  "version": "8.30.1"
20
20
  },
21
21
  "type": "secret_detection",
22
- "start_time": "2026-06-26T14:45:00",
23
- "end_time": "2026-06-26T14:45:01",
22
+ "start_time": "2026-07-07T12:57:44",
23
+ "end_time": "2026-07-07T12:57:45",
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.483231567,
29
+ "time_s": 0.465675553,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 1260,
32
+ "repo_size_kb": 2561,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 23,
34
+ "bytes_scanned": 24,
35
35
  "pipeline_type": "Tag"
36
36
  },
37
37
  {
@@ -40,7 +40,7 @@
40
40
  "passthrough_types": null,
41
41
  "passthrough_count": 0,
42
42
  "passthrough_auth_used": false,
43
- "rules_version": "0.24.3"
43
+ "rules_version": "0.25.0"
44
44
  }
45
45
  ]
46
46
  }
@@ -1,7 +1,6 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Plesty library root package. Import as ``plesty.lib``."""
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Data types and utilities for Plesty."""
8
7
 
9
8
  from .array import PlestyArray
@@ -16,9 +15,13 @@ from .types import (
16
15
  cast_basic_type,
17
16
  )
18
17
  from .table import TableHeader, PlestyTable2D, PlestyTable3D
18
+ from .io import save_result, load_result, convert_to_hdf5
19
19
 
20
20
  __all__ = [
21
21
  "PlestyArray",
22
+ "save_result",
23
+ "load_result",
24
+ "convert_to_hdf5",
22
25
  "Units",
23
26
  "TableHeader",
24
27
  "PlestyTable2D",
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Data definitions for Plesty.
8
7
 
9
8
  For data from physical experiments, we need to strictly define the meta information
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """C type parameter manager for Plesty device interfaces."""
8
7
 
9
8
  import ctypes as ct
@@ -0,0 +1,210 @@
1
+ # SPDX-FileCopyrightText: 2026 Plesty Development Team
2
+ # SPDX-FileContributor: Yunshuang Yuan
3
+ #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
6
+ """Persistence helpers for Plesty data (issue plesty-lib#4).
7
+
8
+ Measured data must survive process crashes so experiments can resume from a
9
+ checkpoint. The storage contract keeps blob data in its **original raw
10
+ format** and describes it in a JSON metadata document that carries a
11
+ reference link to the blob:
12
+
13
+ * :class:`~plesty.lib.data.array.PlestyArray` — raw ``.npy`` blob; the JSON
14
+ document stores the Plesty metadata (name, range, options, unit,
15
+ description) plus shape/dtype and the blob reference.
16
+ * ``bytes`` (e.g. an image already encoded by a camera) — written untouched;
17
+ common image formats (PNG, JPEG, TIFF) are recognised so the blob gets its
18
+ native suffix.
19
+ * JSON-serializable values (dicts, lists, scalars) — inlined in the JSON
20
+ document itself; no blob is written.
21
+
22
+ Writes are crash-safe: the blob is written first (atomically, via a temporary
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.
25
+ """
26
+
27
+ import json
28
+ import os
29
+ from datetime import datetime, timezone
30
+ from io import BytesIO
31
+ from pathlib import Path
32
+ from typing import Any, Iterable, Optional
33
+
34
+ import numpy as np
35
+
36
+ from .array import PlestyArray
37
+
38
+ #: Magic-number prefixes for recognising already-encoded image blobs.
39
+ _IMAGE_MAGIC: dict[bytes, str] = {
40
+ b"\x89PNG\r\n\x1a\n": "png",
41
+ b"\xff\xd8\xff": "jpg",
42
+ b"II*\x00": "tif",
43
+ b"MM\x00*": "tif",
44
+ }
45
+
46
+
47
+ def _atomic_write_bytes(path: Path, payload: bytes) -> None:
48
+ """Write *payload* to *path* atomically via a temporary sibling file."""
49
+ tmp_path = path.with_name(path.name + ".tmp")
50
+ with open(tmp_path, "wb") as stream:
51
+ stream.write(payload)
52
+ stream.flush()
53
+ os.fsync(stream.fileno())
54
+ os.replace(tmp_path, path)
55
+
56
+
57
+ def _sniff_suffix(payload: bytes) -> str:
58
+ """Return the native file suffix for an encoded blob (default ``bin``)."""
59
+ for magic, suffix in _IMAGE_MAGIC.items():
60
+ if payload.startswith(magic):
61
+ return suffix
62
+ return "bin"
63
+
64
+
65
+ def save_result(
66
+ result: Any,
67
+ path_stem: str | Path,
68
+ provenance: Optional[dict[str, Any]] = None,
69
+ ) -> Path:
70
+ """Persist a measurement result as raw blob + JSON metadata document.
71
+
72
+ Args:
73
+ result: The value to persist — a :class:`PlestyArray`, encoded
74
+ ``bytes`` (e.g. an image), or any JSON-serializable value.
75
+ path_stem: Destination path without suffix; the JSON document is
76
+ written to ``<stem>.json`` and any blob next to it.
77
+ provenance: Optional context (step id, operation, parameters,
78
+ device identity) recorded verbatim in the document.
79
+
80
+ Returns:
81
+ The path of the JSON metadata document.
82
+
83
+ Raises:
84
+ TypeError: If the result is neither a PlestyArray, bytes, nor
85
+ JSON-serializable.
86
+ """
87
+ stem = Path(path_stem)
88
+ stem.parent.mkdir(parents=True, exist_ok=True)
89
+ document: dict[str, Any] = {
90
+ "saved_at": datetime.now(timezone.utc).isoformat(),
91
+ "provenance": provenance or {},
92
+ }
93
+
94
+ if isinstance(result, PlestyArray):
95
+ blob_path = stem.with_suffix(".npy")
96
+ buffer = BytesIO()
97
+ np.save(buffer, np.asarray(result))
98
+ _atomic_write_bytes(blob_path, buffer.getvalue())
99
+ document.update(
100
+ {
101
+ "type": "array",
102
+ "blob": blob_path.name,
103
+ "format": "npy",
104
+ "shape": list(result.shape),
105
+ "dtype": str(result.dtype),
106
+ "meta": {k: getattr(result, k, None) for k in PlestyArray._META_KEYS},
107
+ }
108
+ )
109
+ elif isinstance(result, (bytes, bytearray)):
110
+ payload = bytes(result)
111
+ suffix = _sniff_suffix(payload)
112
+ blob_path = stem.with_suffix(f".{suffix}")
113
+ _atomic_write_bytes(blob_path, payload)
114
+ document.update({"type": "bytes", "blob": blob_path.name, "format": suffix})
115
+ else:
116
+ document.update({"type": "value", "value": result})
117
+
118
+ target = stem.with_suffix(".json")
119
+ _atomic_write_bytes(target, json.dumps(document, default=str, indent=2).encode())
120
+ return target
121
+
122
+
123
+ def load_result(path: str | Path) -> Any:
124
+ """Load a result previously written by :func:`save_result`.
125
+
126
+ Args:
127
+ path: Path to the JSON metadata document (``<stem>.json``).
128
+
129
+ Returns:
130
+ The reconstructed value: a :class:`PlestyArray` with its metadata
131
+ restored, raw ``bytes`` for encoded blobs, or the inlined value.
132
+
133
+ Raises:
134
+ ValueError: If the document declares an unknown result type.
135
+ """
136
+ source = Path(path)
137
+ document: dict[str, Any] = json.loads(source.read_text())
138
+ result_type = document.get("type")
139
+
140
+ if result_type == "array":
141
+ data = np.load(source.with_name(document["blob"]))
142
+ meta = dict(document.get("meta", {}))
143
+ if meta.get("range") is not None:
144
+ meta["range"] = tuple(meta["range"])
145
+ return PlestyArray(data, **meta)
146
+ if result_type == "bytes":
147
+ return source.with_name(document["blob"]).read_bytes()
148
+ if result_type == "value":
149
+ return document.get("value")
150
+ raise ValueError(f"Unknown result type in {source}: {result_type!r}")
151
+
152
+
153
+ def convert_to_hdf5(documents: Iterable[str | Path], target: str | Path) -> Path:
154
+ """Pack results written by :func:`save_result` into one HDF5 archive.
155
+
156
+ The raw blob + JSON layout stays the primary on-disk format; this is an
157
+ optional post-hoc export to the unified HDF5 format preferred by issue
158
+ plesty-lib#4. Each JSON document becomes one HDF5 group (named after the
159
+ document stem) holding a ``data`` dataset — the decoded array, the raw
160
+ encoded bytes, or the JSON-encoded inline value — with the document's
161
+ metadata and provenance stored as group attributes.
162
+
163
+ Args:
164
+ documents: Paths of the JSON metadata documents to include.
165
+ target: Destination ``.h5`` file; parent directories are created.
166
+
167
+ Returns:
168
+ The resolved path of the written HDF5 file.
169
+
170
+ Raises:
171
+ ImportError: If the optional ``h5py`` dependency is not installed.
172
+ ValueError: If a document declares an unknown result type.
173
+ """
174
+ try:
175
+ import h5py
176
+ except ImportError as exc: # pragma: no cover - depends on environment
177
+ raise ImportError(
178
+ "convert_to_hdf5 requires the optional dependency 'h5py'; install it with: uv add h5py"
179
+ ) from exc
180
+
181
+ destination = Path(target)
182
+ if destination.suffix not in (".h5", ".hdf5"):
183
+ destination = destination.with_suffix(".h5")
184
+ destination.parent.mkdir(parents=True, exist_ok=True)
185
+
186
+ with h5py.File(destination, "w") as archive:
187
+ for doc_path in documents:
188
+ source = Path(doc_path)
189
+ document: dict[str, Any] = json.loads(source.read_text())
190
+ result_type = document.get("type")
191
+ group = archive.create_group(source.stem)
192
+
193
+ if result_type == "array":
194
+ group.create_dataset("data", data=np.load(source.with_name(document["blob"])))
195
+ for key, value in document.get("meta", {}).items():
196
+ if value is not None:
197
+ group.attrs[f"meta.{key}"] = json.dumps(value, default=str)
198
+ elif result_type == "bytes":
199
+ payload = source.with_name(document["blob"]).read_bytes()
200
+ group.create_dataset("data", data=np.frombuffer(payload, dtype=np.uint8))
201
+ group.attrs["format"] = document.get("format", "bin")
202
+ elif result_type == "value":
203
+ group.create_dataset("data", data=json.dumps(document.get("value"), default=str))
204
+ else:
205
+ raise ValueError(f"Unknown result type in {source}: {result_type!r}")
206
+
207
+ group.attrs["type"] = str(result_type)
208
+ group.attrs["saved_at"] = document.get("saved_at", "")
209
+ group.attrs["provenance"] = json.dumps(document.get("provenance", {}), default=str)
210
+ return destination
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """This module defines the PlestyTable class, which represents a tabular data structure
8
7
  with named columns and rows. It provides methods for accessing and manipulating the data
9
8
  in a structured way.
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Type resolution and casting utilities for Plesty data types."""
8
7
 
9
8
  from __future__ import annotations
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Unit parsing, composition, and conversion utilities for Plesty."""
8
7
 
9
8
  import re
@@ -1,7 +1,6 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Device abstractions and base classes for Plesty."""
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Async wrapper classes for synchronous device objects."""
8
7
 
9
8
  import asyncio
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Base synchronous device model for Plesty."""
8
7
 
9
8
  from __future__ import annotations
@@ -286,15 +285,18 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem):
286
285
  "Device disconnection logic is not implemented in the base model."
287
286
  )
288
287
 
288
+ @expose_to_api
289
289
  def identity(self) -> str:
290
290
  """Query the device for its identity string."""
291
291
  raise NotImplementedError("Device identity query is not implemented in the base model.")
292
292
 
293
+ @expose_to_api
293
294
  @abstractmethod
294
295
  def check_errors(self) -> list[str]:
295
296
  """Query the device for any error messages."""
296
297
  raise NotImplementedError("Device error checking is not implemented in the base model.")
297
298
 
299
+ @expose_to_api
298
300
  @abstractmethod
299
301
  def check_operatability(self) -> bool:
300
302
  """Check if the device is currently operatable (connected and responsive)."""
@@ -401,12 +403,10 @@ class BaseDeviceSyncModel(ABC, ConfigSystem, FunctionSystem):
401
403
  """Placeholder method for querying the device."""
402
404
  raise NotImplementedError("Command querying logic is not implemented in the base model.")
403
405
 
406
+ @expose_to_api
404
407
  def reset(self) -> None:
405
408
  """Reset the device to a known state."""
406
- # This method can be overridden by subclasses to implement device-specific reset logic.
407
- pass
408
409
 
410
+ @expose_to_api
409
411
  def clear(self) -> None:
410
412
  """Clear the device state."""
411
- # This method can be overridden by subclasses to implement device-specific clear logic.
412
- pass
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Base TCP/IP SCPI device implementation for Plesty."""
8
7
 
9
8
  import logging
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Base VISA SCPI device implementation for Plesty."""
8
7
 
9
8
  from typing import Any
@@ -1,3 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2026 Plesty Development Team
2
+ # SPDX-FileContributor: Yunshuang Yuan
3
+ #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
1
6
  """Composite device wrapper for managing multiple devices together."""
2
7
 
3
8
  from typing import Any
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Utility functions for device connectivity and configuration."""
8
7
 
9
8
  import socket
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Function system for registering and dispatching device operations."""
8
7
 
9
8
  from typing import Any, Callable, get_type_hints
@@ -24,7 +23,6 @@ from plesty.lib.data import (
24
23
  cast_basic_type,
25
24
  )
26
25
 
27
-
28
26
  _FUNC_META_DTYPE_MAP: dict[str, type] = {
29
27
  "float": float,
30
28
  "int": int,
@@ -1,9 +1,8 @@
1
- #
2
1
  # SPDX-FileCopyrightText: 2026 Plesty Development Team
3
- # SPDX-License-Identifier: LGPL-3.0-or-later
4
- # Author: Yunshuang Yuan
5
- # Email: yunshuang.yuan@fkp.uni-hannover.de
2
+ # SPDX-FileContributor: Yunshuang Yuan
6
3
  #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
7
6
  """Configuration parameter system for Plesty device models."""
8
7
 
9
8
  from dataclasses import dataclass, field
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: 2026 Plesty Development Team
2
+ # SPDX-FileContributor: Yunshuang Yuan
3
+ #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
6
+ """Experiment orchestration: plans, journals, and the async Experiment ABC."""
7
+
8
+ from .base_experiment import Experiment, InvalidOperationError, PlanMismatchError
9
+ from .journal import Journal
10
+ from .schedule import Plan, Step
11
+
12
+ __all__ = [
13
+ "Experiment",
14
+ "InvalidOperationError",
15
+ "PlanMismatchError",
16
+ "Journal",
17
+ "Plan",
18
+ "Step",
19
+ ]