plesty-sdk 0.3.3__tar.gz → 0.3.5.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/CHANGELOG.md +94 -0
  2. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/PKG-INFO +1 -1
  3. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/init.md +68 -1
  4. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/gl-secret-detection-report.json +5 -5
  5. plesty_sdk-0.3.5.dev1/plesty/sdk/assets/docs-build-gitlab-ci.yml +17 -0
  6. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/client-field-test.py.template +4 -2
  7. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/field-test-questions.yaml +59 -0
  8. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/field-test.py.template +2 -0
  9. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/next-steps.yaml +3 -1
  10. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/extras/device/base_device.py +4 -1
  11. plesty_sdk-0.3.5.dev1/plesty/sdk/assets/templates/extras/device/device.py +60 -0
  12. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/extras/experiment/experiment.py +30 -6
  13. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/{% if project_type in ['device', 'experiment'] %}__main__.py{% endif %}.jinja +66 -30
  14. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/pyproject.toml.jinja +8 -3
  15. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/tests/test_{{module_name}}.py.jinja +30 -11
  16. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/{% if project_type in ['device', 'experiment'] %}.env.example{% endif %}.jinja +10 -1
  17. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/field_test.py +362 -23
  18. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/init.py +131 -0
  19. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/tui.py +77 -17
  20. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_field_test_scaffold.py +301 -2
  21. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_init.py +185 -2
  22. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_tui.py +68 -35
  23. plesty_sdk-0.3.3/plesty/sdk/assets/docs-build-gitlab-ci.yml +0 -11
  24. plesty_sdk-0.3.3/plesty/sdk/assets/templates/extras/device/device.py +0 -37
  25. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/.coverage +0 -0
  26. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/.gitignore +0 -0
  27. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/.gitlab-ci.yml +0 -0
  28. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/.mailmap +0 -0
  29. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/LICENSE +0 -0
  30. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  31. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/README.md +0 -0
  32. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/REUSE.toml +0 -0
  33. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/architecture.md +0 -0
  34. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/check.md +0 -0
  35. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/docs.md +0 -0
  36. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/license.md +0 -0
  37. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/manual.md +0 -0
  38. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/commands/update.md +0 -0
  39. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/getting-started.md +0 -0
  40. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/index.md +0 -0
  41. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/docs/toc.yaml +0 -0
  42. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/__init__.py +0 -0
  43. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/check/guidance.yaml +0 -0
  44. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/hub-theme/plesty-hub.css +0 -0
  45. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/hub-theme/plesty-logo.svg +0 -0
  46. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/hub-theme/plesty-mark.svg +0 -0
  47. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/field-test-env.example +0 -0
  48. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/init/mock-test.py.template +0 -0
  49. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/manual/pdf-common.css +0 -0
  50. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/manual/pdf-page-landscape.css +0 -0
  51. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/manual/pdf-page-portrait.css +0 -0
  52. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/ruff.toml +0 -0
  53. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/copier.yml +0 -0
  54. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/extras/analyzer/analyzer.py +0 -0
  55. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/.gitignore +0 -0
  56. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/CHANGELOG.md.jinja +0 -0
  57. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/LICENSE +0 -0
  58. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/LICENSES/LGPL-3.0-or-later.txt +0 -0
  59. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/README.md.jinja +0 -0
  60. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/REUSE.toml.jinja +0 -0
  61. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/docs/index.md.jinja +0 -0
  62. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/docs/toc.yaml +0 -0
  63. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/__init__.py.jinja +0 -0
  64. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/config.yaml.jinja +0 -0
  65. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/tests/__init__.py.jinja +0 -0
  66. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/assets/templates/template/{% if project_type == 'experiment' %}config{% endif %}/default.yaml +0 -0
  67. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/authorship.py +0 -0
  68. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/cache.py +0 -0
  69. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/cli.py +0 -0
  70. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/__init__.py +0 -0
  71. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/check.py +0 -0
  72. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/docs/__init__.py +0 -0
  73. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/docs/deploy.py +0 -0
  74. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/docs/generate.py +0 -0
  75. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/docs/serve.py +0 -0
  76. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/docs/sphinx_builder.py +0 -0
  77. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/license.py +0 -0
  78. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/manual.py +0 -0
  79. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/mock_test.py +0 -0
  80. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/commands/update.py +0 -0
  81. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/context.py +0 -0
  82. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/report.py +0 -0
  83. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/plesty/sdk/semver.py +0 -0
  84. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/pyproject.toml +0 -0
  85. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/__init__.py +0 -0
  86. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_authorship.py +0 -0
  87. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_cache.py +0 -0
  88. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_check.py +0 -0
  89. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_cli.py +0 -0
  90. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_context.py +0 -0
  91. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_docs_deploy.py +0 -0
  92. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_docs_generate.py +0 -0
  93. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_docs_serve.py +0 -0
  94. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_license.py +0 -0
  95. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_manual.py +0 -0
  96. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_mock_test_scaffold.py +0 -0
  97. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_report.py +0 -0
  98. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_semver.py +0 -0
  99. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_sphinx_builder.py +0 -0
  100. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/tests/test_update.py +0 -0
  101. {plesty_sdk-0.3.3 → plesty_sdk-0.3.5.dev1}/uv.lock +0 -0
@@ -2,6 +2,100 @@
2
2
 
3
3
  The format is based on [Keep a Changelog](https://keepachangelog.com).
4
4
 
5
+ ## [0.3.5.dev1] — 2026-08-21
6
+
7
+ Development pre-release: the tutorial needs the pin option before 0.3.5 is
8
+ ready to be called finished.
9
+
10
+ ### Added
11
+ - `plesty init --lib-version VERSION` pins `plesty-lib` to what the caller
12
+ names instead of to the newest release on PyPI, and skips the lookup. A bare
13
+ version becomes a lower bound (`0.3.5.dev3` → `>=0.3.5.dev3`); a complete
14
+ specifier is written as given (`==0.4.0`, `>=0.3.5.dev3,<0.4`). One that PEP
15
+ 440 cannot read is refused at the command line, before a directory is
16
+ created. For the tutorial: a lesson quoting a `pyproject.toml` and the file
17
+ its reader ends up with have to keep saying the same thing however far the
18
+ library has moved since the page was written — and a scaffold on an
19
+ air-gapped machine now pins what was asked for rather than a fallback.
20
+
21
+ ## [0.3.4] — 2026-08-21
22
+
23
+ ### Changed
24
+ - The scaffold is generated against the **newest published `plesty-lib`**, not
25
+ the one that existed when the SDK was released: `plesty init` reads the
26
+ version list from PyPI (pre-releases included — the development line is where
27
+ the features the templates use land first) and pins `plesty-lib>=<newest>`.
28
+ The lookup has a 3 s budget and every way it can fail — no route, a proxy, a
29
+ body that is not the expected JSON, a fully yanked release — falls back to
30
+ `PLESTY_LIB_VERSION` (currently 0.3.5.dev3), the library the templates are
31
+ written against; the pin is never lower than that floor. The dev group pins
32
+ `plesty-sdk>=0.3.4` — this release, the one the scaffold is written against —
33
+ instead of `>=0.2.0`.
34
+ - The experiment scaffold starts its rig the way plesty-lib 0.3.5.dev2 asks it
35
+ to. `__main__.py` declares a `RIG` — one sub-device per instrument, naming
36
+ the operations the experiment calls on its server — and hands it to
37
+ `connect_rig()`, which builds the composite, preflights it, and on a failure
38
+ names the sub-device, its address and the variable that set it before
39
+ offering retry / abort / skip. Addresses are resolved from `<NAME>_ADDRESS`
40
+ by the composite itself, so `build_client` is gone; the rig is `release()`d
41
+ after the run, `--run-root` defaults to `PLESTY_DATA_MOUNT` (the run lands on
42
+ the share), and `.env.example` carries `PLESTY_DATA_DIR`/`PLESTY_DATA_MOUNT`.
43
+ The run config is read with `plesty.lib.utils.config.load_yaml` instead of a
44
+ hand-written `yaml.safe_load` — the last copy of a loader the library owns.
45
+ - The device scaffold exposes its example operation with `@expose_to_api` and
46
+ shows how an operation that acts on the world declares what it does
47
+ (`kind="motion"`, `position_key`, `step`, `kind="lifecycle"`) — what the
48
+ field-test interview and `FieldTestPipeline` now drive operations by.
49
+ `base_device.py` re-exports `expose_to_api`, as the hub modules do.
50
+
51
+ ### Added
52
+ - `plesty init field-test` reads the kind each operation declares
53
+ (plesty-lib#39: `@expose_to_api(kind=…)` / a schema `kind` block) and
54
+ asks only what it leaves open. A declared kind is shown as `[motion]`,
55
+ `[lifecycle]`, `[control]` beside the operation and pre-fills the
56
+ "acts on the world" question, which is asked only for undeclared
57
+ operations; a `configure` operation is not offered and is recorded as
58
+ excluded (the round-trip gate covers settings). For an allow-listed
59
+ motion the interview confirms `step` (the declared value is the default)
60
+ and asks `position_key` from the readable parameters when the module did
61
+ not declare it; a motion left without either is dropped, not guessed. A
62
+ motion slow-op skips the argument chooser and asks how far — a distance
63
+ the pipeline drives from wherever the part is and undoes. Both generated
64
+ files carry `OP_KINDS` and pass it to the host and client tiers.
65
+
66
+ ### Fixed
67
+ - Generated `tests/field_test_client.py` imported the device class at the
68
+ top and, for a module with no hardware-free construction, left it unused
69
+ — failing the module's own lint gate. The import now lives with the
70
+ `SPAWN_FACTORY` that needs it.
71
+ - `plesty init field-test`: an arrow key no longer answers the question
72
+ (#32). The key reader took the first byte of `ESC [ B` through
73
+ `sys.stdin`, whose buffer swallowed the rest, so the descriptor looked
74
+ idle and the press was read as a lone Escape — which confirmed the
75
+ question empty and moved on; three presses skipped three questions.
76
+ Keys are read from the descriptor itself, control sequences are consumed
77
+ whole, and Escape no longer counts as an answer.
78
+ - `plesty init field-test`: a list longer than ten entries is shown but not
79
+ asked (#32). Drawn as a checkable list, a spectrometer's 262 parameters
80
+ were 500 lines per redraw — taller than any terminal, so nothing could be
81
+ erased and every keypress appended the list again. The interview now says
82
+ so first, keeps whatever `.plesty/field-test.json` already holds for the
83
+ question (an empty list on a first run), names the entry to edit, lists
84
+ the first ten with `… and N more`, and waits for Enter — otherwise two
85
+ such questions scroll past together.
86
+ - A confirmed selection leaves a one-line record above the next question;
87
+ the list itself is erased on Enter and the transcript used to show every
88
+ question and none of the answers.
89
+ - `answered` in `.plesty/field-test.json` records each question once
90
+ instead of growing on every interrupted run.
91
+ - The generated docs-build Pages job gates on `html/latest/index.html`
92
+ instead of the directory path `"html/latest/"`: `rules.exists` matches
93
+ file globs, and the directory form stopped matching on gitlab.com
94
+ between 2026-07-21 and 2026-08-10 — every docs-build pipeline failed
95
+ with "no jobs" and the Pages sites froze while the deploys kept
96
+ succeeding. Existing repos pick the fix up on their next docs deploy
97
+ (the deploy rewrites the branch's `.gitlab-ci.yml` from this asset).
98
+
5
99
  ## [0.3.3] — 2026-08-14
6
100
 
7
101
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plesty-sdk
3
- Version: 0.3.3
3
+ Version: 0.3.5.dev1
4
4
  Summary: Command-line application for developing Python modules compliant with the Plesty standard.
5
5
  Author: Plesty Development Team, Yunshuang Yuan, Christopher Borchers, fvrehlinger, Maximilian Heller, Michael Zopf
6
6
  License-Expression: LGPL-3.0-or-later
@@ -41,6 +41,7 @@ name, so `plesty-foo-bar` → `foo_bar` without needing `--name`.
41
41
  | `--author` | `John Doe` | Author name written into `pyproject.toml` and `REUSE.toml` |
42
42
  | `--email` | `john.doe@example.com` | Author email written into `pyproject.toml` |
43
43
  | `--standard` | `pixel` | Compliance standard for the generated CI pipeline: `pixel`, `nebula`, or `quantum`. A fresh scaffold is a prototype, so CI starts at `pixel`; bump it alongside your releases. |
44
+ | `--lib-version` | newest on PyPI | Pin `plesty-lib` to this version instead of looking it up. A bare version becomes a lower bound (`0.3.5.dev3` → `>=0.3.5.dev3`); a complete specifier is written as given (`==0.4.0`, `'>=0.3.5.dev3,<0.4'`). PyPI is not consulted, so the scaffold is reproducible. |
44
45
  | `--no-ci` | off | Skip generating `.gitlab-ci.yml` entirely. |
45
46
 
46
47
  ## Directory rules
@@ -120,7 +121,23 @@ The `pyproject.toml` is pre-configured with:
120
121
  - The PyPI distribution name `plesty-foo-bar` (kebab-case)
121
122
  - `packages = ["plesty"]` so the module installs under the `plesty` namespace (`import plesty.foo_bar`)
122
123
  - Dynamic versioning via `versioningit`
123
- - `plesty-lib` as a runtime dependency
124
+ - `plesty-lib` as a runtime dependency, pinned to the **newest version published
125
+ on PyPI** when the project is scaffolded (pre-releases included — that is where
126
+ the features the templates use land first). The lookup has a 3 s budget; with
127
+ no route to PyPI the pin falls back to `PLESTY_LIB_VERSION` in
128
+ `plesty/sdk/commands/init.py`, the library the templates are written against.
129
+ The pin is never lower than that floor: the scaffold uses that library's
130
+ operation kinds and rig start-up, and an older one would not run it.
131
+ `--lib-version` overrides all of that and skips the lookup — for a tutorial,
132
+ whose lesson text and reader's `pyproject.toml` have to keep agreeing however
133
+ far the library has moved since the page was written:
134
+
135
+ ```bash
136
+ plesty init experiment --name scan_exp --lib-version 0.3.5.dev3
137
+ # → "plesty-lib>=0.3.5.dev3"
138
+ plesty init experiment --name scan_exp --lib-version '>=0.3.5.dev3,<0.4'
139
+ # → "plesty-lib>=0.3.5.dev3,<0.4"
140
+ ```
124
141
  - `pytest`, `pytest-cov`, `mypy`, and `plesty-sdk` under `[dependency-groups] dev`
125
142
  (included automatically by `uv run` — no `--extra dev` needed)
126
143
  - Sphinx docs settings under `[tool.plesty.docs]`
@@ -167,6 +184,40 @@ python -m plesty.foo_bar --host 10.0.0.5 --tcp-port 6000 # CLI overrides env
167
184
  variables, so one `.env` drives both the server and the tests. It is skipped unless
168
185
  `DEVICE_HOST` is set.
169
186
 
187
+ ## Rig start-up and the shared disk (experiment only)
188
+
189
+ The generated `__main__.py` does not open device clients itself. It declares
190
+ the rig — one entry per sub-device, naming the operations the experiment calls
191
+ on that server — and hands it to
192
+ [`plesty.lib.experiment.connect_rig`](https://gitlab.com/plesty/core/plesty-lib):
193
+
194
+ ```python
195
+ RIG = {"device": ["identity"]}
196
+ OPTIONAL: frozenset[str] = frozenset()
197
+ ```
198
+
199
+ `connect_rig` builds the composite, preflights it (every listed operation must
200
+ be exposed and `identity()` must answer), and on a failure prints which
201
+ sub-device, at which address, from which variable, then offers
202
+ **retry / abort / skip** — skip only for the names in `OPTIONAL`. A retry calls
203
+ the builder again, so a `.env` corrected or a server started in the meantime is
204
+ picked up. Non-interactive processes get the diagnosis logged and
205
+ `RigNotReadyError`; nothing is measured on a rig that is not what the plan
206
+ assumes.
207
+
208
+ Addresses are never written into the module: each sub-device reads
209
+ `<NAME>_ADDRESS` from the environment (`device` → `DEVICE_ADDRESS`), overridable
210
+ with `--device-address`.
211
+
212
+ | Variable | Meaning |
213
+ |---|---|
214
+ | `<NAME>_ADDRESS` | ZMQ address of one sub-device's server |
215
+ | `PLESTY_DATA_DIR` | Shared disk as the acquiring host writes it — devices write frames to `<PLESTY_DATA_DIR>/<run_id>/raw` and return only paths |
216
+ | `PLESTY_DATA_MOUNT` | The same disk as this machine reads it; also the default `run_root`, so runs land on the share |
217
+
218
+ Both data variables are optional: without them runs go to `./runs` and no
219
+ device writes blobs. `--run-root` overrides the default per call.
220
+
170
221
  ## Next steps after scaffolding
171
222
 
172
223
  `plesty init` ends with a numbered, project-type-specific **Next steps** list —
@@ -312,6 +363,22 @@ in neither list: it is never called, and the gate that would have called it
312
363
  skips saying so. A generated allow-list that quietly included a motion command
313
364
  would be worse than no generator at all.
314
365
 
366
+ **What the module declares is not asked again.** An operation exposed with a
367
+ kind — `@expose_to_api(kind="motion", position_key="MO.Position", step=1.0)`,
368
+ `kind="lifecycle"`, `kind="control"`, or a `"kind"` block in
369
+ `schema_func.json` (plesty-lib#39) — is shown as `[motion]`, `[lifecycle]`,
370
+ `[control]` in the chooser, and the "acts on the world" question is asked only
371
+ about operations that declared nothing. A `configure` operation is not offered
372
+ at all: it is recorded as excluded, because a setting that outlives the call is
373
+ verified by the round-trip gate. For an allow-listed motion the interview
374
+ confirms how far the test may move it (`step`; the declared value is the
375
+ default) and, if the module left it open, which parameter reports the position
376
+ — a motion with neither is dropped, since the pipeline drives a motion one step
377
+ out from that parameter and back and will not guess how far. A motion chosen as
378
+ the slow operation is asked only how far the long move goes: the pipeline
379
+ starts it from wherever the part is and moves it back afterwards. Both
380
+ generated files carry the result as `OP_KINDS`.
381
+
315
382
  The result is `tests/field_test.py`: hardware-gated (it defines no pytest test
316
383
  functions, so CI collects nothing), wired to publish
317
384
  `reports/<instrument>/field-test.json`, and runnable against the simulator
@@ -19,19 +19,19 @@
19
19
  "version": "8.30.1"
20
20
  },
21
21
  "type": "secret_detection",
22
- "start_time": "2026-08-14T11:38:56",
23
- "end_time": "2026-08-14T11:38:56",
22
+ "start_time": "2026-08-21T18:31:49",
23
+ "end_time": "2026-08-21T18:31:49",
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.271382223,
29
+ "time_s": 0.480570925,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 492,
32
+ "repo_size_kb": 468,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 1664,
34
+ "bytes_scanned": 136,
35
35
  "pipeline_type": "Tag"
36
36
  },
37
37
  {
@@ -0,0 +1,17 @@
1
+ stages: [deploy]
2
+
3
+ pages:
4
+ stage: deploy
5
+ rules:
6
+ # The gate for creating the job, not a selector of what is published —
7
+ # `publish: html/latest` below uploads the whole directory. exists
8
+ # matches FILE globs: the bare directory path "html/latest/" stopped
9
+ # matching on gitlab.com between 2026-07-21 and 2026-08-10, every
10
+ # docs-build pipeline failed with "no jobs", and Pages froze while the
11
+ # deploys kept succeeding. Name a file Sphinx always emits.
12
+ - exists:
13
+ - "html/latest/index.html"
14
+ script:
15
+ - echo "Serving documentation on GitLab pages..."
16
+ pages:
17
+ publish: html/latest
@@ -57,8 +57,6 @@ from plesty.lib.test.client_field_test import ClientFieldTest
57
57
  from plesty.lib.test.report_artifact import Instrument
58
58
  from plesty.lib.utils.settings import EnvSettings
59
59
 
60
- from {{DEVICE_IMPORT}} import {{DEVICE_CLASS}}
61
-
62
60
  #: The instrument address comes from ``.env`` — the same file the device
63
61
  #: server reads. Never written here: an address carries a serial number and
64
62
  #: this file is committed.
@@ -83,6 +81,7 @@ BASELINE = PROJECT_ROOT / "reports" / "{{INSTRUMENT_SLUG}}" / "field-test.json"
83
81
  {{TYPES_BLOCK}}
84
82
  {{RESOURCES_BLOCK}}
85
83
  {{SAFE_OPS_BLOCK}}
84
+ {{OP_KINDS_BLOCK}}
86
85
  {{SPAWN_BLOCK}}
87
86
 
88
87
  def build(address: str | None, spawn: bool) -> ClientFieldTest:
@@ -100,6 +99,9 @@ def build(address: str | None, spawn: bool) -> ClientFieldTest:
100
99
  expected_types=EXPECTED_TYPES,
101
100
  resources=RESOURCES,
102
101
  safe_ops=SAFE_OPS,
102
+ # How each of them is driven — the host tier's kinds, handed over
103
+ # because describe() cannot say which method moves a stage.
104
+ op_kinds=OP_KINDS,
103
105
  # The host tier's report decides what this tier touches. Its coverage
104
106
  # block records what that run actually reached on this instrument,
105
107
  # already filtered by what the fitted sensor supports — so the two
@@ -34,6 +34,17 @@ intro: |
34
34
 
35
35
  Ctrl-C stops without writing anything.
36
36
 
37
+ # Shown instead of a choice when a list is longer than the interview offers
38
+ # at the terminal (sdk#32). {count} items, {shown} listed, {key} is the entry
39
+ # in the answers file at {answers}, {state} says what is written for it now.
40
+ too_long: |
41
+ {count} entries — more than the {shown} this interview can offer at once.
42
+
43
+ The answer is written {state}: "{key}" in {answers}.
44
+ To choose, put the entries you want in that list and run this command
45
+ again; the list is then taken as the answer. The first {shown} of the
46
+ {count} follow, for orientation.
47
+
37
48
  questions:
38
49
  safe_ops:
39
50
  when_empty: "nothing selected — no operation will be called or timed"
@@ -62,6 +73,12 @@ questions:
62
73
  · changes a setting or calibration that outlives the call
63
74
  · takes a long time, or writes files
64
75
  · needs arguments you would have to invent
76
+
77
+ An operation shown as [motion], [lifecycle], [control] or [acquire]
78
+ was declared so by the module, and the test already knows how to
79
+ drive it: a motion one step out from its position parameter and back,
80
+ a lifecycle op once, a control op once after the motions. Pick those
81
+ when the bench allows it — the two questions above still apply.
65
82
  reason_prompt: "Why is '{item}' safe to run 20 times unsupervised?"
66
83
  after: >-
67
84
  Nothing selected. The test will not call any operation, and will say so
@@ -85,6 +102,41 @@ questions:
85
102
  reason_prompt: "Which safeguard makes '{item}' safe to repeat?"
86
103
  after: "nothing selected — taken as: all of these only read"
87
104
 
105
+ motion_position_key:
106
+ when_empty: "nothing selected — the motion is dropped from the allow-list"
107
+ prompt: "Which parameter reports where '{item}' moves the part to?"
108
+ why: >-
109
+ A motion is driven from where the part is: the test reads its position,
110
+ moves it one step out, moves it back and checks the position returned.
111
+ The module did not say which parameter that is, so it is asked here.
112
+ decide: |
113
+ Pick the parameter that reports the moving part's position in the same
114
+ units the operation takes — a stage angle in degrees against a move in
115
+ degrees, a piezo position in µm against a step in µm.
116
+
117
+ Pick nothing if no parameter reports it: the motion is then left out,
118
+ because the test will not move a part it cannot place.
119
+ after: >-
120
+ No position parameter named. The operation is left out of the
121
+ allow-list; declare position_key in the module and re-run.
122
+
123
+ motion_step:
124
+ prompt: "How far may the test move '{item}'?"
125
+ why: >-
126
+ Every repetition moves the part this far out and straight back, twenty
127
+ times in a row with nobody watching. The size is the safeguard: small
128
+ enough to be harmless whatever is mounted, large enough to be a real
129
+ move rather than a no-op below the stage's resolution.
130
+ decide: |
131
+ Give a distance in the position parameter's units. A stage that resolves
132
+ 26 µdeg is fine with 1 deg; a piezo with a 100 µm range with 1 µm.
133
+ Press Enter to accept the value the module declared, if it did.
134
+
135
+ Give nothing (or 0) if no distance is harmless: the motion is then left
136
+ out of the allow-list.
137
+ value_prompt: "Step for '{item}', in the units of {position_key}"
138
+ after: "no step given — the motion is dropped from the allow-list"
139
+
88
140
  only_keys:
89
141
  when_empty: "nothing selected — EVERY setting will be written back"
90
142
  prompt: "Which settings can be safely written back their own value?"
@@ -168,6 +220,13 @@ questions:
168
220
  Pick nothing if none of them work that way — a mode or a unit does not
169
221
  make a call slower, and the check is better skipped than fooled.
170
222
  value_prompt: "What value of '{item}' makes the call take longer than a tenth of a second?"
223
+ motion_value_prompt: >-
224
+ How far should the slow move of '{item}' go — a distance from wherever
225
+ the part is, in the units of {position_key} — to take longer than a
226
+ tenth of a second?
227
+ motion_restored: >-
228
+ '{item}' is a motion: the test moves the part that far from where it is,
229
+ interrupts the call, and afterwards moves it back to where it started.
171
230
  after: >-
172
231
  No argument chosen. The recovery check will be skipped: it can name the
173
232
  call but not make it slow enough to interrupt.
@@ -74,6 +74,7 @@ INSTRUMENT = Instrument(model="{{INSTRUMENT_MODEL}}", sensor={{INSTRUMENT_SENSOR
74
74
 
75
75
  {{SAFE_OPS_BLOCK}}
76
76
  {{UNSAFE_OPS_BLOCK}}
77
+ {{OP_KINDS_BLOCK}}
77
78
  {{ONLY_KEYS_BLOCK}}
78
79
  {{DRAIN_PROBE_BLOCK}}
79
80
  {{SLOW_OP_BLOCK}}
@@ -104,6 +105,7 @@ def build_pipeline(
104
105
  "report_path": report_path,
105
106
  "safe_ops": SAFE_OPS,
106
107
  "unsafe_ops": UNSAFE_OPS,
108
+ "op_kinds": OP_KINDS,
107
109
  "only_keys": ONLY_KEYS,
108
110
  "drain_probe": DRAIN_PROBE,
109
111
  "slow_op": SLOW_OP,
@@ -10,6 +10,7 @@ default:
10
10
  device:
11
11
  - "Define the device schema and wire it into plesty/{module_name}/base_device.py."
12
12
  - "Implement the vendor communication in plesty/{module_name}/device.py."
13
+ - "Expose your custom operations with @expose_to_api and declare what each one does (kind=motion/acquire/lifecycle/control/configure) — the field test drives them by kind instead of asking you about every call."
13
14
  - "Copy .env.example to .env and set DEVICE_HOST/DEVICE_PORT (never commit .env)."
14
15
  - "Try it live: uv run python -m plesty.{module_name} --tcp-port 5555."
15
16
 
@@ -20,7 +21,8 @@ analyzer:
20
21
 
21
22
  experiment:
22
23
  - "Implement build_plan() and the step methods in plesty/{module_name}/experiment.py."
23
- - "Copy .env.example to .env and set your device endpoints; tune config/default.yaml."
24
+ - "Name your rig in the RIG of plesty/{module_name}/__main__.py: one sub-device per instrument, listing the operations you call on it — the preflight checks them before the first step."
25
+ - "Copy .env.example to .env and set one <NAME>_ADDRESS per sub-device (plus PLESTY_DATA_DIR/PLESTY_DATA_MOUNT if runs go to the shared disk); tune config/default.yaml."
24
26
  - "Preview the plan without hardware: uv run python -m plesty.{module_name} --dry-run."
25
27
 
26
28
  verify:
@@ -5,7 +5,10 @@
5
5
 
6
6
  """Base device implementation template."""
7
7
 
8
- from plesty.lib.device.base_device_sync import BaseDeviceSyncModel
8
+ from plesty.lib.device.base_device_sync import BaseDeviceSyncModel, expose_to_api
9
+
10
+ # Re-exported so device.py imports both from here, as the hub modules do.
11
+ __all__ = ["BaseDevice", "expose_to_api"]
9
12
 
10
13
 
11
14
  class BaseDevice(BaseDeviceSyncModel):
@@ -0,0 +1,60 @@
1
+ # SPDX-FileCopyrightText: 2025-2026 Plesty Development Team
2
+ # SPDX-FileContributor: Yunshuang Yuan
3
+ #
4
+ # SPDX-License-Identifier: LGPL-3.0-or-later
5
+
6
+ """Concrete device implementation."""
7
+
8
+ from .base_device import BaseDevice, expose_to_api
9
+
10
+
11
+ class Device(BaseDevice):
12
+ """Concrete device class extending BaseDevice with custom functionality."""
13
+
14
+ def __init__(
15
+ self,
16
+ id: str,
17
+ host: str | None = None,
18
+ port: int | None = None,
19
+ param_schema=None,
20
+ op_schema=None,
21
+ ) -> None:
22
+ """Initialize the device with an id, connection info, and optional schemas."""
23
+ super().__init__(id, op_schema=op_schema, param_schema=param_schema)
24
+ ## Connection credentials (from the environment via EnvSettings); use them
25
+ ## in `connect()` to reach the hardware.
26
+ self.host = host
27
+ self.port = port
28
+ ## Device-specific initialization can be added here
29
+
30
+ @expose_to_api
31
+ def custom_method(self) -> dict:
32
+ """Implement a custom device-specific operation.
33
+
34
+ Custom methods are not required by the base model; they add workflows
35
+ on top of the registered configuration parameters and schema
36
+ operations. `@expose_to_api` is what puts one on the describe
37
+ endpoint, so a remote client and the field test can call it. Bare, it
38
+ declares a `read`: the operation answers and changes nothing.
39
+
40
+ Returns:
41
+ Whatever the operation reports; a mapping keeps the reply
42
+ self-describing over the wire.
43
+ """
44
+ ## Device-specific logic can be added here
45
+ return {}
46
+
47
+ ## An operation that acts on the world declares what it does, so the field
48
+ ## test (`plesty init field-test`) can drive it unattended instead of
49
+ ## asking about every call:
50
+ ##
51
+ ## @expose_to_api(kind="motion", position_key="position", step=1.0)
52
+ ## def move_absolute(self, position: float) -> float: ...
53
+ ##
54
+ ## @expose_to_api(kind="lifecycle")
55
+ ## def home(self) -> bool: ...
56
+ ##
57
+ ## Kinds are read | motion | acquire | lifecycle | control | configure. A
58
+ ## motion is driven one `step` out from what `position_key` reports and put
59
+ ## back, a lifecycle operation runs once, a control operation once at rest
60
+ ## after the motions, and a configure operation is never called.
@@ -1,9 +1,20 @@
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
  """Experiment implementation.
2
7
 
3
8
  Replace the example step operation with real measurement logic. The plan/
4
9
  journal/resume lifecycle is inherited from ``plesty.lib.experiment`` — keep
5
10
  ``build_plan()`` deterministic and device-free so interrupted runs can be
6
11
  resumed with ``run(resume=<run_id>)``.
12
+
13
+ Each run owns a directory under ``run_root`` holding ``plan.json``,
14
+ ``journal.jsonl``, one ``records.jsonl`` line per completed step, and the
15
+ blobs those records reference. Live views over the same records are written
16
+ against ``plesty.lib.monitor`` (``Viz``), so watching a run needs nothing
17
+ from this file.
7
18
  """
8
19
 
9
20
  from pathlib import Path
@@ -27,7 +38,7 @@ class Experiment(BaseExperiment):
27
38
  self,
28
39
  devices: Optional[CompositeDevice] = None,
29
40
  config: Optional[dict[str, Any]] = None,
30
- run_root: str | Path = "runs",
41
+ run_root: str | Path | None = None,
31
42
  **kwargs: Any,
32
43
  ) -> None:
33
44
  """Initialize the experiment.
@@ -39,9 +50,12 @@ class Experiment(BaseExperiment):
39
50
  config: Measurement configuration (tuning values, no
40
51
  credentials); recorded in the plan.
41
52
  run_root: Directory under which each run creates its checkpoint
42
- directory.
53
+ directory. ``None`` takes ``PLESTY_DATA_MOUNT`` from the
54
+ environment — runs land on the shared disk — and falls
55
+ back to ``runs`` when no share is configured.
43
56
  **kwargs: Forwarded to the base ``Experiment`` (``name``,
44
- ``max_retries``, ``retry_sleep``).
57
+ ``max_retries``, ``retry_sleep``, ``data_dir``,
58
+ ``data_mount``).
45
59
  """
46
60
  super().__init__(devices=devices, run_root=run_root, **kwargs)
47
61
  self.config: dict[str, Any] = dict(config or {})
@@ -62,15 +76,25 @@ class Experiment(BaseExperiment):
62
76
  """Example step operation — replace with a real measurement.
63
77
 
64
78
  Access instruments through ``self.devices``. Return a result (e.g. a
65
- ``plesty.lib.data.PlestyArray``) to have it persisted to the run's
66
- ``data/`` directory; returning ``None`` skips persistence.
79
+ ``plesty.lib.data.PlestyArray``) to have it appended to the run's
80
+ ``records.jsonl`` (large arrays land beside it under ``data/``);
81
+ returning ``None`` records nothing for the step. A device that writes
82
+ its own files writes them to ``self.raw_dir`` on the shared disk —
83
+ never back over the network.
67
84
 
68
85
  Args:
69
86
  x: Example sweep position taken from the step params.
70
87
  """
71
88
 
72
89
  async def setup(self) -> None:
73
- """Prepare instruments before the first step; extend as needed."""
90
+ """Prepare instruments before the first step; extend as needed.
91
+
92
+ The inherited setup connects the composite and preflights it — every
93
+ remote sub-device must expose the methods its ``requires`` names and
94
+ answer ``identity()``, or ``RigNotReadyError`` stops the run before
95
+ anything is measured. Route the writing devices here, e.g.
96
+ ``self.devices.set_data_path(self.raw_dir, devices=["cam"])``.
97
+ """
74
98
  await super().setup()
75
99
 
76
100
  async def teardown(self) -> None: