plesty-sdk 0.3.5.dev1__tar.gz → 0.4.0__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 (104) hide show
  1. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/CHANGELOG.md +170 -0
  2. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/PKG-INFO +7 -3
  3. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/README.md +6 -2
  4. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/architecture.md +15 -0
  5. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/check.md +1 -1
  6. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/init.md +32 -0
  7. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/getting-started.md +17 -0
  8. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/gl-secret-detection-report.json +7 -7
  9. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/check/guidance.yaml +3 -1
  10. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/docs-build-gitlab-ci.yml +6 -2
  11. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/hub-theme/plesty-hub.css +2 -0
  12. plesty_sdk-0.4.0/plesty/sdk/assets/hub-theme/plesty-versions.css +49 -0
  13. plesty_sdk-0.4.0/plesty/sdk/assets/hub-theme/plesty-versions.js +110 -0
  14. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/next-steps.yaml +1 -0
  15. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/extras/device/device.py +6 -0
  16. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/README.md.jinja +12 -1
  17. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/docs/index.md.jinja +16 -0
  18. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/{% if project_type in ['device', 'experiment'] %}__main__.py{% endif %}.jinja +93 -16
  19. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/pyproject.toml.jinja +10 -1
  20. plesty_sdk-0.4.0/plesty/sdk/commands/docs/deploy.py +288 -0
  21. plesty_sdk-0.4.0/plesty/sdk/commands/docs/picker.py +141 -0
  22. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/docs/sphinx_builder.py +19 -3
  23. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/field_test.py +5 -1
  24. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/init.py +37 -1
  25. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/pyproject.toml +11 -0
  26. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_docs_deploy.py +135 -32
  27. plesty_sdk-0.4.0/tests/test_docs_picker.py +148 -0
  28. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_field_test_scaffold.py +87 -0
  29. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_init.py +88 -0
  30. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_sphinx_builder.py +1 -1
  31. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/uv.lock +189 -13
  32. plesty_sdk-0.3.5.dev1/plesty/sdk/commands/docs/deploy.py +0 -198
  33. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/.coverage +0 -0
  34. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/.gitignore +0 -0
  35. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/.gitlab-ci.yml +0 -0
  36. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/.mailmap +0 -0
  37. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/LICENSE +0 -0
  38. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/LICENSES/LGPL-3.0-or-later.txt +0 -0
  39. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/REUSE.toml +0 -0
  40. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/docs.md +0 -0
  41. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/license.md +0 -0
  42. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/manual.md +0 -0
  43. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/commands/update.md +0 -0
  44. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/index.md +0 -0
  45. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/docs/toc.yaml +0 -0
  46. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/__init__.py +0 -0
  47. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/hub-theme/plesty-logo.svg +0 -0
  48. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/hub-theme/plesty-mark.svg +0 -0
  49. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/client-field-test.py.template +0 -0
  50. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/field-test-env.example +0 -0
  51. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/field-test-questions.yaml +0 -0
  52. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/field-test.py.template +0 -0
  53. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/init/mock-test.py.template +0 -0
  54. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/manual/pdf-common.css +0 -0
  55. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/manual/pdf-page-landscape.css +0 -0
  56. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/manual/pdf-page-portrait.css +0 -0
  57. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/ruff.toml +0 -0
  58. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/copier.yml +0 -0
  59. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/extras/analyzer/analyzer.py +0 -0
  60. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/extras/device/base_device.py +0 -0
  61. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/extras/experiment/experiment.py +0 -0
  62. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/.gitignore +0 -0
  63. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/CHANGELOG.md.jinja +0 -0
  64. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/LICENSE +0 -0
  65. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/LICENSES/LGPL-3.0-or-later.txt +0 -0
  66. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/REUSE.toml.jinja +0 -0
  67. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/docs/toc.yaml +0 -0
  68. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/__init__.py.jinja +0 -0
  69. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/plesty/{{module_name}}/config.yaml.jinja +0 -0
  70. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/tests/__init__.py.jinja +0 -0
  71. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/tests/test_{{module_name}}.py.jinja +0 -0
  72. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/{% if project_type == 'experiment' %}config{% endif %}/default.yaml +0 -0
  73. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/assets/templates/template/{% if project_type in ['device', 'experiment'] %}.env.example{% endif %}.jinja +0 -0
  74. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/authorship.py +0 -0
  75. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/cache.py +0 -0
  76. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/cli.py +0 -0
  77. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/__init__.py +0 -0
  78. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/check.py +0 -0
  79. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/docs/__init__.py +0 -0
  80. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/docs/generate.py +0 -0
  81. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/docs/serve.py +0 -0
  82. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/license.py +0 -0
  83. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/manual.py +0 -0
  84. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/mock_test.py +0 -0
  85. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/commands/update.py +0 -0
  86. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/context.py +0 -0
  87. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/report.py +0 -0
  88. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/semver.py +0 -0
  89. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/plesty/sdk/tui.py +0 -0
  90. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/__init__.py +0 -0
  91. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_authorship.py +0 -0
  92. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_cache.py +0 -0
  93. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_check.py +0 -0
  94. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_cli.py +0 -0
  95. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_context.py +0 -0
  96. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_docs_generate.py +0 -0
  97. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_docs_serve.py +0 -0
  98. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_license.py +0 -0
  99. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_manual.py +0 -0
  100. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_mock_test_scaffold.py +0 -0
  101. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_report.py +0 -0
  102. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_semver.py +0 -0
  103. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_tui.py +0 -0
  104. {plesty_sdk-0.3.5.dev1 → plesty_sdk-0.4.0}/tests/test_update.py +0 -0
@@ -2,6 +2,176 @@
2
2
 
3
3
  The format is based on [Keep a Changelog](https://keepachangelog.com).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.4.0] — 2026-09-07
8
+
9
+ A scaffolded device can now be driven by hand. `plesty init device` generates a
10
+ `--gui` entry point that opens the module's control panel — every control built
11
+ from the device schema, nothing hand-written — as an alternative to serving the
12
+ instrument, so a new module is usable at the bench before anyone writes a client
13
+ for it. Qt rides in a `gui` extra, so a headless bench that only ever serves the
14
+ device pays nothing for the window it never opens. Generated projects pin
15
+ `plesty-lib>=0.4.0`, the release `plesty.lib.ui` begins at.
16
+
17
+ ### Added
18
+ - **A scaffolded device offers its control panel.** `plesty init device` now
19
+ generates `--gui` in the module's `__main__.py`: it opens plesty-lib's
20
+ `DevicePanel` over a `LocalHandle` on the instrument in this process, as an
21
+ alternative to serving it — both want the one session the instrument grants,
22
+ so `main()` does one or the other. Nothing about the window is hand-written;
23
+ the controls, their bounds, units and tooltips come from the device schema, so
24
+ a parameter added there appears in the panel.
25
+
26
+ The generated `open_panel()` calls `connect()` **before** the panel is built.
27
+ That ordering is the point of the entry point rather than an implementation
28
+ detail: what the instrument reports on connection narrows the schema's
29
+ device-wide envelope to the unit in front of the operator, and those are the
30
+ bounds worth typing into. On a PM100D the fitted head answers 400–1100 nm
31
+ against the 185–25000 nm the schema declares for every head.
32
+
33
+ Qt lands in a new `gui` extra (`plesty-lib[gui]`), not in `dependencies` — a
34
+ headless bench serves the device and never opens a window — and the
35
+ `plesty.lib.ui` import is local to `open_panel`, so serving pulls no toolkit.
36
+ Every module used to hand-add this entry point; `plesty-pm100d` was the last
37
+ to do it by hand.
38
+
39
+ - The device template's note on `kind` now says what declaring `lifecycle` does
40
+ beyond the field test: an operation whose effect outlives the call is kept off
41
+ a control panel — the local one and the one a remote client renders. The
42
+ schema is the only place either can learn it.
43
+
44
+ ### Changed
45
+ - `PLESTY_LIB_VERSION`, the `plesty-lib` floor a scaffold falls back to when
46
+ PyPI is unreachable, moves `0.3.6` → `0.4.0`. That is the release
47
+ `plesty.lib.ui` begins at, so it is the floor the generated `--gui` needs;
48
+ below it the panel path raises `ImportError`. The live PyPI lookup already
49
+ resolved 0.4.0 on any machine with a route out — this only changes the
50
+ offline case.
51
+
52
+ ### Fixed
53
+ - **The pre-push hook no longer leaks git's hook variables into the gates.**
54
+ Git exports `GIT_DIR` and friends into every hook, and everything the hook
55
+ runs inherits them, so git work nested inside the gates resolves the pushing
56
+ repository instead of the one it is standing in. The hook now clears them
57
+ before running `plesty check`; nothing about what the gates check changes.
58
+
59
+ Two failures this had already caused. Gate 10 has pip clone each git-pinned
60
+ dependency into a temp directory, where versioningit read the outer
61
+ repository's tag and commit — on lightfield_spectrometer (2026-09-04) a
62
+ pinned `plesty-lib` came out as `0.1.0.dev65+<the module's sha>`, which
63
+ cannot satisfy the module's own floor, so the audit failed with a resolution
64
+ error unrelated to the code. That is enough to make anyone drop the pin
65
+ rather than debug it, and a dropped pin is a module quietly building against
66
+ the wrong dependency. Separately, a test suite that drives git could commit
67
+ into the real repository rather than its fixture, which is what made pushing
68
+ this repo from a worktree unsafe.
69
+
70
+ Existing modules keep the old hook until `plesty init` is re-run; the fix is
71
+ a one-line `unset` in `.git/hooks/pre-push` for anyone who does not want to.
72
+
73
+ ## [0.3.6] — 2026-09-03
74
+
75
+ Everything the scaffolds generate now passes the gates the scaffolds ask for.
76
+ Two generated files failed a check the SDK itself enforces — a field-test
77
+ client with an import nothing used, and entry points on an event loop that
78
+ Python 3.16 will not give them — and the version picker no longer strands a
79
+ reader on a release it cannot leave. Generated projects pin `plesty-lib>=0.3.6`.
80
+
81
+ ### Fixed
82
+ - Generated device and experiment entry points start through
83
+ `plesty.lib.service.run_server` instead of a bare `asyncio.run`. The bare
84
+ call worked on Windows only because importing `plesty.lib.service` installed
85
+ a global event-loop policy — deprecated in Python 3.14 (two warnings per
86
+ server start on benches already there) and removed in 3.16, where the entry
87
+ point would get a `ProactorEventLoop` and fail inside pyzmq for want of
88
+ `add_reader`. The `plesty-lib` pin floor moves to 0.3.6, the first release
89
+ carrying `run_server`. Existing modules keep working on 3.14 and 3.15 and
90
+ pick the swap up when they regenerate.
91
+ - `plesty init field-test` no longer writes a client whose imports fail the
92
+ module's own lint gate. A module reached through its `DocDevice` — or through
93
+ any construction taking no arguments — gets a bare factory, and the generated
94
+ `tests/field_test_client.py` imported `functools.partial` anyway: F401, so
95
+ `plesty check` could not pass on a module that had just been field-tested.
96
+ The import is now emitted only in the branch that calls `partial`. Both
97
+ branches are linted in the tests; neither was before, because every fixture
98
+ probed into the "no hardware-free construction" path.
99
+ Generating against released 0.3.3 has a worse form of the same block — it
100
+ emits the `partial` import but not the device import, leaving `SPAWN_FACTORY`
101
+ an undefined name (F821). Add the `from plesty.<module> import <factory>`
102
+ line by hand, or regenerate once this ships.
103
+ - The version picker no longer offers a version the reader cannot switch back
104
+ out of. A release built before the Read-the-Docs theme has no sidebar for the
105
+ picker to mount on and cannot be stamped, yet the manifest still listed it —
106
+ on the SDK's own docs, selecting 0.1 stranded the reader. The manifest now
107
+ advertises only releases that carry the picker; such a release stays
108
+ published and reachable at its own URL, and `/releases/` still redirects to
109
+ the newest one that exists. Stamping consequently runs before the manifest is
110
+ written, so the manifest sees the final state of every tree.
111
+ - A module with no releases rendered a dropdown holding a single entry. The
112
+ picker now appears only when there is something to pick between.
113
+
114
+ ## [0.3.5] — 2026-08-24
115
+
116
+ Documentation gains a version picker, and the two things standing between it
117
+ and the published sites are fixed: the docs-deploy gate had been skipped since
118
+ 2026-08-21 behind a failing test collection, and releases built before the
119
+ picker existed had no way back to the current docs.
120
+
121
+ ### Added
122
+ - Every module's documentation carries a **version picker** in the sidebar.
123
+ Each module already deployed `latest/` from its default branch and
124
+ `releases/<major>.<minor>/` from each tag, but nothing linked them: a reader
125
+ on the release docs could not reach any other version. The picker is fed by
126
+ `versions.json`, fetched at page load rather than baked in — a page built
127
+ months ago cannot know which versions exist today. Switching versions keeps
128
+ the current page when the target has it, and falls back to that version's
129
+ index when it does not (pages come and go between releases). A deployment
130
+ without a manifest — a local `plesty docs serve` preview — renders no picker
131
+ at all.
132
+
133
+ ### Fixed
134
+ - The version picker is no longer a one-way trip. Releases published before it
135
+ existed are never rebuilt, so switching to one landed the reader on pages
136
+ with no picker and no way back. Each deploy now stamps the picker's two
137
+ assets and the tags referencing them into any release tree that lacks them.
138
+ Only the `<head>` gains anything — the prose, the API reference and the
139
+ stylesheets a historical release was built with are untouched, which is why
140
+ the picker's styling moved out of `plesty-hub.css` into its own
141
+ `plesty-versions.css`. Idempotent, so re-running over a stamped tree does
142
+ nothing. A release predating the Read-the-Docs theme (plesty-sdk 0.1) has no
143
+ sidebar to mount on and is skipped.
144
+ - The test suite no longer fails to collect in CI. `plesty-lib` is declared in
145
+ a `dev` dependency group: `tests/test_mock_test_scaffold.py` drives the
146
+ mock-test scaffold against a real `plesty.lib.sim.demo_device.DemoDevice`,
147
+ which it subclasses and runs, but nothing installed the library on a runner —
148
+ so `hub-check` had been failing with `ModuleNotFoundError: No module named
149
+ 'plesty.lib'`, which in turn skipped `hub-deploy-docs` and froze the
150
+ published documentation. It is a group rather than a runtime dependency:
151
+ installing plesty-sdk must not pull the library it scaffolds modules for.
152
+ - `versions.json` entries were origin-absolute (`"/0.3/"`), so they resolved
153
+ against the host rather than the module root, and the `releases/` redirect
154
+ stub built from them has been broken since it was written — on
155
+ docs.plesty.net it pointed at `docs.plesty.net/0.3/`. The manifest moves to
156
+ the module root, lists `latest` alongside the releases, and its URLs are
157
+ relative to that root, which is `/<slug>/` on docs.plesty.net and
158
+ `/<group>/<project>/` on GitLab Pages.
159
+
160
+ ### Changed
161
+ - Both deploy modes rewrite the manifest, not only `--release`. This is what
162
+ gives a module that has never been tagged a picker, and what migrates
163
+ branches deployed by an older SDK — including their `.gitlab-ci.yml`, which
164
+ was previously written once at branch creation and never updated again.
165
+ - The Pages job publishes the whole `html/` tree instead of `html/latest`, so
166
+ the releases the picker lists are reachable there too, and `html/index.html`
167
+ redirects the Pages root to `latest/`. The layout now matches what
168
+ docs.plesty.net already serves, so one picker runs unchanged on both.
169
+ **Deep links to `plesty.gitlab.io/<group>/<project>/<page>.html` move under
170
+ `/latest/`**; docs.plesty.net URLs are unaffected. The reference link
171
+ `plesty check` prints on a failing gate followed that move, so it now points
172
+ at docs.plesty.net — the site readers are sent to, and the one whose path is
173
+ independent of how Pages lays the tree out.
174
+
5
175
  ## [0.3.5.dev1] — 2026-08-21
6
176
 
7
177
  Development pre-release: the tutorial needs the pin option before 0.3.5 is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plesty-sdk
3
- Version: 0.3.5.dev1
3
+ Version: 0.4.0
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
@@ -55,10 +55,14 @@ plesty init device --author "Jane Doe" --email jane@example.com
55
55
 
56
56
  # 2. Develop your module — the pre-push hook runs compliance checks automatically
57
57
 
58
- # 3. Run the full [SDK] compliance check manually at any time
58
+ # 3. Drive the instrument from a control panel built out of your device schema
59
+ uv sync --extra gui
60
+ python -m plesty.power_meter --gui
61
+
62
+ # 4. Run the full [SDK] compliance check manually at any time
59
63
  plesty check
60
64
 
61
- # 4. Preview the documentation locally
65
+ # 5. Preview the documentation locally
62
66
  plesty docs serve
63
67
  ```
64
68
 
@@ -23,10 +23,14 @@ plesty init device --author "Jane Doe" --email jane@example.com
23
23
 
24
24
  # 2. Develop your module — the pre-push hook runs compliance checks automatically
25
25
 
26
- # 3. Run the full [SDK] compliance check manually at any time
26
+ # 3. Drive the instrument from a control panel built out of your device schema
27
+ uv sync --extra gui
28
+ python -m plesty.power_meter --gui
29
+
30
+ # 4. Run the full [SDK] compliance check manually at any time
27
31
  plesty check
28
32
 
29
- # 4. Preview the documentation locally
33
+ # 5. Preview the documentation locally
30
34
  plesty docs serve
31
35
  ```
32
36
 
@@ -187,6 +187,21 @@ have already diverged. The cost is a coupling to the component's ref and input c
187
187
  a rename or ref change in plesty-ci breaks scaffolds (see technical debt below) — which we
188
188
  accept because centralised evolution has proven far cheaper than fleet-wide file surgery.
189
189
 
190
+ **A generated device carries its own control panel, with Qt in an extra.** A device can be
191
+ reached two ways — served over TCP for a client to drive, or opened in a window on the
192
+ machine it is plugged into — and the scaffold generates the second as `--gui` on the module's
193
+ existing entry point rather than shipping a separate console or leaving each module to
194
+ hand-write one. The controls are derived from the device schema, so a module gets a usable
195
+ window the moment its schema is right, and it is the same serialized `describe` payload a
196
+ remote client renders: a hole in the payload surfaces at the bench instead of only across a
197
+ network. Operations are filtered by their declared `kind`, which makes the schema the single
198
+ place that decides what a panel offers, local or remote. Qt lives in a generated `gui` extra
199
+ rather than in the generated `dependencies` — a headless bench serves the device and never
200
+ opens a window — which is why `plesty.lib.ui` is imported inside the panel path and not at
201
+ module scope. The tradeoff is that the two modes are alternatives rather than layers: both
202
+ claim the single session most instruments grant, so a module cannot serve and show a local
203
+ panel at the same time.
204
+
190
205
  **`plesty update --install-hook` for cloned projects.** Git hooks live in `.git/` and are
191
206
  never cloned, so `plesty init`'s pre-push hook silently vanishes for every contributor
192
207
  after the first. Rather than adopting a hook-manager dependency (pre-commit) or committing
@@ -146,7 +146,7 @@ failures and their guidance always print:
146
146
  Error: [Coverage] total coverage 62% below threshold
147
147
  This gate verifies: the pytest suite passes with at least 80% line coverage.
148
148
  How to fix: Reproduce with 'uv run pytest --cov=plesty' and add tests ...
149
- Reference: https://plesty.gitlab.io/core/plesty-sdk/commands/check.html
149
+ Reference: https://docs.plesty.net/plesty-sdk/latest/commands/check.html
150
150
  ```
151
151
 
152
152
  ## Examples
@@ -184,6 +184,38 @@ python -m plesty.foo_bar --host 10.0.0.5 --tcp-port 6000 # CLI overrides env
184
184
  variables, so one `.env` drives both the server and the tests. It is skipped unless
185
185
  `DEVICE_HOST` is set.
186
186
 
187
+ ## The control panel (device only)
188
+
189
+ The same entry point opens the device's control panel instead of serving it:
190
+
191
+ ```bash
192
+ uv sync --extra gui
193
+ python -m plesty.foo_bar --gui
194
+ ```
195
+
196
+ Serving and `--gui` are alternatives — both want the one session the instrument
197
+ grants, so `main()` does one or the other. The panel is
198
+ [`plesty.lib.ui.DevicePanel`](https://gitlab.com/plesty/core/plesty-lib) over a
199
+ `LocalHandle`, which builds every control from the device schema; a parameter
200
+ added to the schema appears in the window with no GUI code written. The
201
+ generated `open_panel()` calls `connect()` **before** the panel is built,
202
+ because it is what the instrument reports on connection — a fitted sensor head's
203
+ wavelength range, say — that narrows the schema's device-wide envelope to the
204
+ unit in front of the operator.
205
+
206
+ Operations are filtered by their `kind`: one whose effect outlives the call is
207
+ declared `@expose_to_api(kind="lifecycle")`, and that keeps it off this panel
208
+ *and* off the one a remote client renders, since the schema is the only place
209
+ either can learn it.
210
+
211
+ Qt goes in a `gui` extra rather than in `dependencies` — a headless bench serves
212
+ the device and never opens a window:
213
+
214
+ ```toml
215
+ [project.optional-dependencies]
216
+ gui = ["plesty-lib[gui]>=0.4.0"]
217
+ ```
218
+
187
219
  ## Rig start-up and the shared disk (experiment only)
188
220
 
189
221
  The generated `__main__.py` does not open device clients itself. It declares
@@ -35,6 +35,23 @@ Use `--standard` to choose the compliance standard the generated CI pipeline enf
35
35
  (default `pixel` — a fresh scaffold is a prototype; bump it alongside your releases), or
36
36
  `--no-ci` to omit the CI file entirely.
37
37
 
38
+ ### Running what you scaffolded
39
+
40
+ A device module's entry point does one of two things, and they are alternatives —
41
+ both want the one session the instrument grants:
42
+
43
+ ```bash
44
+ python -m plesty.power_meter --tcp-port 5555 # serve it for clients to drive
45
+ uv sync --extra gui
46
+ python -m plesty.power_meter --gui # drive it yourself, in a window
47
+ ```
48
+
49
+ The panel is built from the device schema, so the parameters and operations you
50
+ declare are the controls you get — there is no GUI code to write. Qt lives in the
51
+ `gui` extra, so a headless bench that only ever serves the device never installs
52
+ it. See [`plesty init`](commands/init.md) for how the panel picks up an
53
+ instrument's real limits, and which operations it leaves out.
54
+
38
55
  ## 2. Check compliance
39
56
 
40
57
  Navigate into the project and run the compliance checker:
@@ -7,7 +7,7 @@
7
7
  "vendor": {
8
8
  "name": "GitLab"
9
9
  },
10
- "version": "7.38.1"
10
+ "version": "7.39.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-08-21T18:31:49",
23
- "end_time": "2026-08-21T18:31:49",
22
+ "start_time": "2026-09-07T14:07:04",
23
+ "end_time": "2026-09-07T14:07:04",
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.480570925,
29
+ "time_s": 0.467099461,
30
30
  "exit_code": 0,
31
31
  "git_strategy": "FetchShallow",
32
- "repo_size_kb": 468,
32
+ "repo_size_kb": 500,
33
33
  "commit_count": 1,
34
- "bytes_scanned": 136,
34
+ "bytes_scanned": 1215,
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.25.0"
43
+ "rules_version": "0.25.1"
44
44
  }
45
45
  ]
46
46
  }
@@ -1,6 +1,8 @@
1
1
  # Per-gate failure guidance echoed by `plesty check` (issue sdk#26).
2
2
  # Keys match the gate keys used in check_command; text is plain CLI prose.
3
- docs_url: https://plesty.gitlab.io/core/plesty-sdk/commands/check.html
3
+ # docs.plesty.net, not the Pages mirror: it is the site readers are sent to,
4
+ # and its /<slug>/latest/ path is unaffected by how Pages lays the tree out.
5
+ docs_url: https://docs.plesty.net/plesty-sdk/latest/commands/check.html
4
6
 
5
7
  gates:
6
8
  metadata:
@@ -4,7 +4,7 @@ pages:
4
4
  stage: deploy
5
5
  rules:
6
6
  # The gate for creating the job, not a selector of what is published —
7
- # `publish: html/latest` below uploads the whole directory. exists
7
+ # `publish: html` below uploads the whole directory. exists
8
8
  # matches FILE globs: the bare directory path "html/latest/" stopped
9
9
  # matching on gitlab.com between 2026-07-21 and 2026-08-10, every
10
10
  # docs-build pipeline failed with "no jobs", and Pages froze while the
@@ -14,4 +14,8 @@ pages:
14
14
  script:
15
15
  - echo "Serving documentation on GitLab pages..."
16
16
  pages:
17
- publish: html/latest
17
+ # The whole tree, not just html/latest: the sidebar version picker needs
18
+ # versions.json and the releases/ it lists to be reachable from the same
19
+ # site. html/index.html forwards the Pages root to latest/, and the layout
20
+ # then matches docs.plesty.net, where the same picker runs unchanged.
21
+ publish: html
@@ -24,6 +24,8 @@
24
24
 
25
25
  /* The sidebar header keeps the stock Read-the-Docs styling. */
26
26
 
27
+ /* The version picker styles itself from plesty-versions.css. */
28
+
27
29
  /* Schema shields → soft rounded pills. */
28
30
  .rst-content .sd-badge {
29
31
  border-radius: 999px;
@@ -0,0 +1,49 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2026 Plesty Development Team
3
+ * SPDX-FileContributor: Yunshuang Yuan
4
+ * SPDX-License-Identifier: LGPL-3.0-or-later
5
+ *
6
+ * Styling for the sidebar version picker (plesty-versions.js).
7
+ *
8
+ * Separate from plesty-hub.css because it is also stamped into release docs
9
+ * built before the picker existed. Those trees carry their own copy of
10
+ * plesty-hub.css, and replacing it would change how a historical page looks
11
+ * beyond adding the dropdown; dropping one more file in beside it does not.
12
+ */
13
+
14
+ /* Sits at the foot of the sidebar header, under the search box. Added by the
15
+ * script, so a deployment without a versions.json shows no empty slot. */
16
+ .wy-side-nav-search .plesty-versions {
17
+ margin: 0.75rem 0 0;
18
+ text-align: left;
19
+ }
20
+ .wy-side-nav-search .plesty-versions label {
21
+ display: block;
22
+ margin-bottom: 0.3rem;
23
+ color: #d9d9d9;
24
+ font-size: 80%;
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.06em;
27
+ }
28
+ .wy-side-nav-search .plesty-versions select {
29
+ width: 100%;
30
+ padding: 0.3rem 0.5rem;
31
+ border: 1px solid rgba(255, 255, 255, 0.25);
32
+ border-radius: 4px;
33
+ background: rgba(0, 0, 0, 0.2);
34
+ color: #fff;
35
+ font-size: 90%;
36
+ cursor: pointer;
37
+ }
38
+ .wy-side-nav-search .plesty-versions select:focus {
39
+ /* Plesty cyan, spelled out: the historical trees this file is stamped into
40
+ carry a plesty-hub.css from before the custom property existed. */
41
+ border-color: #0891b2;
42
+ outline: none;
43
+ }
44
+ /* The dropdown list itself renders in the OS palette, not the sidebar's —
45
+ without this the options are white-on-white on most platforms. */
46
+ .wy-side-nav-search .plesty-versions option {
47
+ color: #404040;
48
+ background: #fff;
49
+ }
@@ -0,0 +1,110 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2026 Plesty Development Team
3
+ * SPDX-FileContributor: Yunshuang Yuan
4
+ * SPDX-License-Identifier: LGPL-3.0-or-later
5
+ *
6
+ * Version picker for the Read-the-Docs sidebar.
7
+ *
8
+ * Every module is deployed as a tree of independently built docs — `latest/`
9
+ * from the default branch, `releases/<major>.<minor>/` from each tag — under
10
+ * one module root, and `versions.json` at that root lists them. A page built
11
+ * months ago cannot know which versions exist today, so the list is read at
12
+ * page load rather than baked in at build time.
13
+ *
14
+ * The module root is not derivable from the URL: docs.plesty.net serves the
15
+ * tree at /<slug>/ and GitLab Pages at /<group>/<project>/. It is derived
16
+ * from this script's own location instead — the file is always at
17
+ * <docs-root>/_static/plesty-versions.js, and `data-root-prefix` (set by
18
+ * sphinx_builder) is the hop from the docs root up to the module root.
19
+ */
20
+ (() => {
21
+ // Read synchronously: document.currentScript is null once the DOM is ready.
22
+ const script = document.currentScript;
23
+ if (!script) return;
24
+ const rootPrefix = script.dataset.rootPrefix;
25
+ const current = script.dataset.version || "latest";
26
+ if (!rootPrefix) return;
27
+
28
+ const docsRoot = new URL("../", script.src);
29
+ const moduleRoot = new URL(rootPrefix, docsRoot);
30
+ // Page path within this version's docs, carried across when the target
31
+ // version has the same page.
32
+ const pagePath = decodeURIComponent(location.href.slice(docsRoot.href.length));
33
+
34
+ const label = (version) =>
35
+ version === "latest" ? "latest (development)" : version;
36
+
37
+ /* Navigate to `path` under `target`, falling back to the target's index
38
+ when the page does not exist there — pages come and go between versions,
39
+ and a picker that lands on a 404 is worse than one that lands on a
40
+ landing page. A HEAD request that cannot run at all (file:// origin)
41
+ falls back the same way. */
42
+ const goTo = async (target, path) => {
43
+ const home = new URL(target, moduleRoot);
44
+ if (!path) {
45
+ location.assign(home);
46
+ return;
47
+ }
48
+ const exact = new URL(path, home);
49
+ try {
50
+ const response = await fetch(exact, { method: "HEAD" });
51
+ location.assign(response.ok ? exact : home);
52
+ } catch {
53
+ location.assign(home);
54
+ }
55
+ };
56
+
57
+ const render = (versions) => {
58
+ const container = document.querySelector(".wy-side-nav-search");
59
+ if (!container) return;
60
+
61
+ const picker = document.createElement("div");
62
+ picker.className = "plesty-versions";
63
+
64
+ const caption = document.createElement("label");
65
+ caption.textContent = "Version";
66
+ caption.htmlFor = "plesty-version-select";
67
+ picker.appendChild(caption);
68
+
69
+ const select = document.createElement("select");
70
+ select.id = "plesty-version-select";
71
+ for (const entry of versions) {
72
+ const option = document.createElement("option");
73
+ option.value = entry.url;
74
+ option.textContent = label(entry.version);
75
+ option.selected = entry.version === current;
76
+ select.appendChild(option);
77
+ }
78
+ // The running version is missing from versions.json when this build has
79
+ // not been indexed yet (a local preview, a tag mid-deploy). Show it so the
80
+ // picker never reads as if the reader were somewhere else.
81
+ if (!versions.some((entry) => entry.version === current)) {
82
+ const option = document.createElement("option");
83
+ option.textContent = label(current);
84
+ option.selected = true;
85
+ option.disabled = true;
86
+ select.insertBefore(option, select.firstChild);
87
+ }
88
+ select.addEventListener("change", () => goTo(select.value, pagePath));
89
+ picker.appendChild(select);
90
+
91
+ // Below the search box, so the picker does not push the search field out
92
+ // of its usual place under the logo.
93
+ container.appendChild(picker);
94
+ };
95
+
96
+ const ready = (run) =>
97
+ document.readyState === "loading"
98
+ ? document.addEventListener("DOMContentLoaded", run)
99
+ : run();
100
+
101
+ fetch(new URL("versions.json", moduleRoot))
102
+ .then((response) => (response.ok ? response.json() : null))
103
+ .then((versions) => {
104
+ // Nothing to pick between — no manifest (a local `plesty docs serve`
105
+ // preview), or a module with no releases yet, whose manifest holds only
106
+ // `latest`. A dropdown with one entry is furniture, not navigation.
107
+ if (Array.isArray(versions) && versions.length > 1) ready(() => render(versions));
108
+ })
109
+ .catch(() => {});
110
+ })();
@@ -13,6 +13,7 @@ device:
13
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."
14
14
  - "Copy .env.example to .env and set DEVICE_HOST/DEVICE_PORT (never commit .env)."
15
15
  - "Try it live: uv run python -m plesty.{module_name} --tcp-port 5555."
16
+ - "Or drive it by hand: uv sync --extra gui, then uv run python -m plesty.{module_name} --gui — the control panel is built from your schema, so every parameter and operation you declare shows up in it."
16
17
 
17
18
  analyzer:
18
19
  - "Declare your input/output schemas and implement analyze() in plesty/{module_name}/analyzer.py."
@@ -58,3 +58,9 @@ class Device(BaseDevice):
58
58
  ## motion is driven one `step` out from what `position_key` reports and put
59
59
  ## back, a lifecycle operation runs once, a control operation once at rest
60
60
  ## after the motions, and a configure operation is never called.
61
+ ##
62
+ ## The field test is not the only reader. An operation whose effect
63
+ ## outlives the call — homing, zeroing an offset, a reset — is declared
64
+ ## `lifecycle`, and that is what keeps it off a control panel: the one
65
+ ## `--gui` opens here, and the one a remote client renders over the wire.
66
+ ## The schema is the only place either of them can learn it.
@@ -17,7 +17,18 @@ uv sync
17
17
  Make sure the device is connected and powered on.
18
18
 
19
19
  ```bash
20
- uv run python -m {{ module_name }} --tcp-port 5555
20
+ uv run python -m plesty.{{ module_name }} --tcp-port 5555
21
+ ```
22
+
23
+ ## Open The Control Panel
24
+
25
+ `--gui` drives the instrument from this process instead of serving it — the two
26
+ are alternatives, since one session is all the instrument grants. The controls
27
+ are built from the device schema, so nothing here is hand-written.
28
+
29
+ ```bash
30
+ uv sync --extra gui
31
+ uv run python -m plesty.{{ module_name }} --gui
21
32
  ```
22
33
  {% elif project_type == 'experiment' %}
23
34
 
@@ -25,6 +25,22 @@ Make sure the device is connected and powered on, then:
25
25
  ```bash
26
26
  uv run python -m plesty.{{ module_name }} --tcp-port 5555
27
27
  ```
28
+
29
+ ## Opening the control panel
30
+
31
+ ```bash
32
+ uv sync --extra gui
33
+ uv run python -m plesty.{{ module_name }} --gui
34
+ ```
35
+
36
+ `--gui` and serving are alternatives: both want the one session the instrument
37
+ grants, so the process does one or the other. The panel derives its controls —
38
+ the parameters, their bounds and units, the operations — from the device
39
+ schema, and it connects before it builds them, so the bounds shown are the ones
40
+ this unit reports rather than the envelope the schema carries for every unit.
41
+
42
+ An operation whose effect outlives the call belongs to `kind="lifecycle"`; that
43
+ is what keeps it off the panel here and off the one a remote client renders.
28
44
  {% elif project_type == 'experiment' %}
29
45
 
30
46
  ## Running the experiment