plesty-lib 0.2.7__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.7 → plesty_lib-0.2.8}/PKG-INFO +3 -2
  3. plesty_lib-0.2.8/REUSE.toml +21 -0
  4. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/gl-secret-detection-report.json +5 -5
  5. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/__init__.py +3 -4
  6. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/__init__.py +7 -4
  7. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/array.py +3 -4
  8. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/ctype_manager.py +3 -4
  9. plesty_lib-0.2.8/plesty/lib/data/io.py +210 -0
  10. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/table.py +3 -4
  11. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/types.py +3 -4
  12. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/data/units.py +3 -4
  13. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/__init__.py +3 -4
  14. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/async_wrapper.py +3 -4
  15. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/base_device_sync.py +3 -4
  16. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/base_tcp_scpi_device.py +3 -4
  17. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/base_visa_scpi_device.py +3 -4
  18. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/composite_device.py +5 -0
  19. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/device_utils.py +3 -4
  20. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/funcs.py +3 -5
  21. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/device/params.py +3 -4
  22. plesty_lib-0.2.8/plesty/lib/experiment/__init__.py +19 -0
  23. plesty_lib-0.2.8/plesty/lib/experiment/base_experiment.py +296 -0
  24. plesty_lib-0.2.8/plesty/lib/experiment/journal.py +113 -0
  25. plesty_lib-0.2.8/plesty/lib/experiment/schedule.py +123 -0
  26. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/service/__init__.py +3 -5
  27. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/service/resource_manager.py +5 -0
  28. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/service/tcp_ip_client.py +3 -4
  29. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/service/tcp_ip_server.py +11 -4
  30. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/sim/__init__.py +3 -4
  31. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/sim/data_generator.py +3 -4
  32. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/solver/__init__.py +3 -4
  33. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/solver/iceblock.py +3 -4
  34. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/solver/scpi.py +3 -4
  35. plesty_lib-0.2.8/plesty/lib/test/__init__.py +6 -0
  36. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/device_func_system.py +3 -4
  37. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/device_param_system.py +3 -4
  38. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/device_pipeline.py +3 -4
  39. plesty_lib-0.2.8/plesty/lib/test/experiment_pipeline.py +228 -0
  40. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/grouped_param_keys.py +3 -4
  41. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/resource_allocation.py +5 -0
  42. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/test/schema_params.py +5 -0
  43. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/__init__.py +3 -4
  44. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/serial.py +3 -5
  45. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/tcp_ip.py +3 -4
  46. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/usb_utils.py +3 -4
  47. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/utils.py +3 -4
  48. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/traffic/visa.py +3 -4
  49. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/__init__.py +3 -4
  50. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/dll_utils.py +3 -4
  51. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/error_utils.py +3 -4
  52. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/logger.py +3 -4
  53. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/registry.py +3 -4
  54. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plesty/lib/utils/settings.py +3 -4
  55. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/plestylib/__init__.py +3 -5
  56. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/pyproject.toml +2 -1
  57. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_data_array.py +3 -4
  58. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_data_types.py +3 -2
  59. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_device_base.py +3 -3
  60. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_device_funcs.py +3 -3
  61. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_device_params.py +3 -3
  62. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_device_pipeline.py +3 -3
  63. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_device_scpi.py +3 -3
  64. plesty_lib-0.2.8/tests/test_experiment.py +276 -0
  65. plesty_lib-0.2.8/tests/test_experiment_pipeline.py +115 -0
  66. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_misc.py +3 -3
  67. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_schema_params.py +3 -4
  68. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_service_manager.py +3 -2
  69. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_settings.py +3 -2
  70. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_sim.py +3 -2
  71. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_solver.py +3 -2
  72. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_tcp_ip_resources.py +44 -0
  73. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_traffic_drivers.py +3 -2
  74. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_traffic_utils.py +3 -3
  75. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/tests/test_utils.py +3 -3
  76. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/uv.lock +2 -406
  77. plesty_lib-0.2.7/CHANGELOG.md +0 -17
  78. plesty_lib-0.2.7/COPYING +0 -674
  79. plesty_lib-0.2.7/LICENSES/GPL-3.0-or-later.txt +0 -674
  80. plesty_lib-0.2.7/plesty/lib/test/__init__.py +0 -7
  81. plesty_lib-0.2.7/spdx.tmpl +0 -4
  82. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/.gitignore +0 -0
  83. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/.gitlab-ci.yml +0 -0
  84. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/LICENSE +0 -0
  85. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  86. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/README.md +0 -0
  87. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/api_reference.md +0 -0
  88. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/contributing.md +0 -0
  89. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/base_device.md +0 -0
  90. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/cmd_solver.md +0 -0
  91. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/error_handling.md +0 -0
  92. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/func_system.md +0 -0
  93. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/index.md +0 -0
  94. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/logging_system.md +0 -0
  95. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/param_system.md +0 -0
  96. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/test_helper.md +0 -0
  97. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/device/traffic_manager.md +0 -0
  98. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/imgs/device_layers.png +0 -0
  99. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/imgs/device_standard.png +0 -0
  100. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/imgs/plesty_framework.png +0 -0
  101. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/index.md +0 -0
  102. {plesty_lib-0.2.7 → plesty_lib-0.2.8}/docs/quickstart.md +0 -0
  103. {plesty_lib-0.2.7 → 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.7
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"
@@ -19,19 +19,19 @@
19
19
  "version": "8.30.1"
20
20
  },
21
21
  "type": "secret_detection",
22
- "start_time": "2026-07-02T11:47:17",
23
- "end_time": "2026-07-02T11:47:17",
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.462341149,
29
+ "time_s": 0.465675553,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 2523,
32
+ "repo_size_kb": 2561,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 39,
34
+ "bytes_scanned": 24,
35
35
  "pipeline_type": "Tag"
36
36
  },
37
37
  {
@@ -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
@@ -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
+ ]