de-shell 0.2.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 (67) hide show
  1. de_shell-0.2.0/LICENSE +21 -0
  2. de_shell-0.2.0/PKG-INFO +196 -0
  3. de_shell-0.2.0/README.md +161 -0
  4. de_shell-0.2.0/de_shell/__init__.py +25 -0
  5. de_shell-0.2.0/de_shell/actions/__init__.py +0 -0
  6. de_shell-0.2.0/de_shell/actions/context.py +62 -0
  7. de_shell-0.2.0/de_shell/actions/figure_registry.py +53 -0
  8. de_shell-0.2.0/de_shell/actions/lifecycle.py +295 -0
  9. de_shell-0.2.0/de_shell/actions/registry.py +141 -0
  10. de_shell-0.2.0/de_shell/actions/wizard.py +115 -0
  11. de_shell-0.2.0/de_shell/app.py +170 -0
  12. de_shell-0.2.0/de_shell/compute.py +103 -0
  13. de_shell-0.2.0/de_shell/debug_flags.py +69 -0
  14. de_shell-0.2.0/de_shell/ipc.py +236 -0
  15. de_shell-0.2.0/de_shell/js/__init__.py +38 -0
  16. de_shell-0.2.0/de_shell/js/__main__.py +4 -0
  17. de_shell-0.2.0/de_shell/js/main/backendProcess.test.ts +70 -0
  18. de_shell-0.2.0/de_shell/js/main/backendProcess.ts +330 -0
  19. de_shell-0.2.0/de_shell/js/main/config.ts +53 -0
  20. de_shell-0.2.0/de_shell/js/main/dialogs.ts +62 -0
  21. de_shell-0.2.0/de_shell/js/main/envProgress.ts +126 -0
  22. de_shell-0.2.0/de_shell/js/main/errorReport.ts +261 -0
  23. de_shell-0.2.0/de_shell/js/main/index.ts +57 -0
  24. de_shell-0.2.0/de_shell/js/main/problemLog.ts +53 -0
  25. de_shell-0.2.0/de_shell/js/main/pythonEnv.test.ts +125 -0
  26. de_shell-0.2.0/de_shell/js/main/pythonEnv.ts +442 -0
  27. de_shell-0.2.0/de_shell/js/main/sentryEnvelope.test.ts +94 -0
  28. de_shell-0.2.0/de_shell/js/main/sentryEnvelope.ts +100 -0
  29. de_shell-0.2.0/de_shell/js/main/updater.ts +322 -0
  30. de_shell-0.2.0/de_shell/js/main/updaterErrors.test.ts +111 -0
  31. de_shell-0.2.0/de_shell/js/main/updaterErrors.ts +65 -0
  32. de_shell-0.2.0/de_shell/js/main/window.ts +141 -0
  33. de_shell-0.2.0/de_shell/js/package.json +5 -0
  34. de_shell-0.2.0/de_shell/js/preload/index.ts +130 -0
  35. de_shell-0.2.0/de_shell/js/renderer/FigureFrame.tsx +88 -0
  36. de_shell-0.2.0/de_shell/js/renderer/figureBridge.react.ts +58 -0
  37. de_shell-0.2.0/de_shell/js/renderer/figureBridge.test.ts +184 -0
  38. de_shell-0.2.0/de_shell/js/renderer/figureBridge.ts +169 -0
  39. de_shell-0.2.0/de_shell/js/renderer/index.ts +34 -0
  40. de_shell-0.2.0/de_shell/js/renderer/protocol.ts +164 -0
  41. de_shell-0.2.0/de_shell/js/renderer/shellState.test.ts +193 -0
  42. de_shell-0.2.0/de_shell/js/renderer/shellState.ts +310 -0
  43. de_shell-0.2.0/de_shell/js/testing/harness.cjs +244 -0
  44. de_shell-0.2.0/de_shell/js/testing/harness.test.cjs +73 -0
  45. de_shell-0.2.0/de_shell/log_stream.py +185 -0
  46. de_shell-0.2.0/de_shell/plotting/__init__.py +0 -0
  47. de_shell-0.2.0/de_shell/plotting/colormaps.py +27 -0
  48. de_shell-0.2.0/de_shell/plotting/figure.py +601 -0
  49. de_shell-0.2.0/de_shell/plotting/selectors/__init__.py +0 -0
  50. de_shell-0.2.0/de_shell/plotting/selectors/utils.py +29 -0
  51. de_shell-0.2.0/de_shell/plotting/stream.py +172 -0
  52. de_shell-0.2.0/de_shell/process_guard.py +190 -0
  53. de_shell-0.2.0/de_shell/session.py +211 -0
  54. de_shell-0.2.0/de_shell/testing/__init__.py +0 -0
  55. de_shell-0.2.0/de_shell/timing.py +28 -0
  56. de_shell-0.2.0/de_shell.egg-info/PKG-INFO +196 -0
  57. de_shell-0.2.0/de_shell.egg-info/SOURCES.txt +65 -0
  58. de_shell-0.2.0/de_shell.egg-info/dependency_links.txt +1 -0
  59. de_shell-0.2.0/de_shell.egg-info/requires.txt +7 -0
  60. de_shell-0.2.0/de_shell.egg-info/top_level.txt +1 -0
  61. de_shell-0.2.0/pyproject.toml +83 -0
  62. de_shell-0.2.0/setup.cfg +4 -0
  63. de_shell-0.2.0/tests/test_actions.py +294 -0
  64. de_shell-0.2.0/tests/test_boundary.py +127 -0
  65. de_shell-0.2.0/tests/test_figure.py +158 -0
  66. de_shell-0.2.0/tests/test_session_base.py +178 -0
  67. de_shell-0.2.0/tests/test_stream.py +227 -0
de_shell-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Direct Electron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: de-shell
3
+ Version: 0.2.0
4
+ Summary: The Electron + Python desktop-app shell Direct Electron's apps (SpyDE, Ground Crew, Autopilot) are assembled from
5
+ Author-email: Carter Francis <cartsfrancis@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CSSFrancis/de-shell
8
+ Project-URL: Repository, https://github.com/CSSFrancis/de-shell
9
+ Project-URL: Issues, https://github.com/CSSFrancis/de-shell/issues
10
+ Project-URL: Changelog, https://github.com/CSSFrancis/de-shell/blob/main/CHANGELOG.md
11
+ Keywords: electron,desktop,anyplotlib,electron-microscopy,direct-electron
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: JavaScript
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.20.0
29
+ Requires-Dist: anyplotlib>=0.7.3
30
+ Requires-Dist: pyyaml
31
+ Provides-Extra: tests
32
+ Requires-Dist: pytest>=3.6; extra == "tests"
33
+ Requires-Dist: pytest-timeout; extra == "tests"
34
+ Dynamic: license-file
35
+
36
+ # DE Shell
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/de-shell.svg)](https://pypi.org/project/de-shell/)
39
+ [![CI](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml)
40
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
41
+
42
+ The substrate Direct Electron's desktop apps are assembled from: **SpyDE**
43
+ (offline analysis), **Ground Crew** (manual camera control) and **Autopilot**
44
+ (automated acquisition). Each app is an Electron window over a Python
45
+ sidecar, and everything the three have in common lives here — the
46
+ Python↔JS message pipe, the window and its menus, the figure bridge over
47
+ [anyplotlib](https://pypi.org/project/anyplotlib/), the sidecar process
48
+ manager and its Python environment, the updater, the problem reporter, the
49
+ Playwright harness.
50
+
51
+ It contains **no domain logic**. No detectors, no microscopes, no signal
52
+ types, no analysis. `tests/test_boundary.py` enforces that in a clean
53
+ subprocess: the shell must stay installable without the science stack, so
54
+ the live in-memory apps never acquire it transitively.
55
+
56
+ ```bash
57
+ pip install de-shell
58
+ ```
59
+
60
+ ## One package
61
+
62
+ The shell is **one pip package**, and the TypeScript rides inside the wheel:
63
+
64
+ ```
65
+ pyproject.toml the package: de-shell
66
+ de_shell/ app loop, session base, actions, IPC, figures, compute, logging
67
+ de_shell/js/ the TypeScript half, one folder per Electron target
68
+ main/ Electron main: window, sidecar + stdout demuxer, python env, updater, reports
69
+ preload/ the contextBridge surface (exposeShellBridge)
70
+ renderer/ React: figure bridge, FigureFrame, the chrome slice of state
71
+ testing/ the Playwright harness (launchApp)
72
+ tests/ the Python suite (incl. the boundary test)
73
+ package.json DEV ONLY: typechecks and unit-tests de_shell/js; nothing is published to npm
74
+ ```
75
+
76
+ The JavaScript that speaks the sidecar protocol ships in the same artifact
77
+ as the Python that speaks it. One `pip install -U de-shell` moves both, and
78
+ an app cannot end up with the two halves at different versions. The
79
+ TypeScript is shipped as **source** and compiled by the consuming app's
80
+ bundler, so there is no build step here and an editable install is
81
+ live-editable from the app.
82
+
83
+ ## Consuming it
84
+
85
+ **Python** — an ordinary dependency:
86
+
87
+ ```toml
88
+ dependencies = ["de-shell>=0.2,<0.3"]
89
+ ```
90
+
91
+ For hacking on the shell and an app at once, a sibling checkout as an
92
+ editable path source (uv) or `pip install -e ../de-shell` overlays it.
93
+
94
+ **Electron** — ask the installed package where its TypeScript is and link
95
+ it into the project at a fixed path, then alias and `paths` through the link:
96
+
97
+ ```
98
+ python -m de_shell.js # prints …/de_shell/js
99
+ ```
100
+
101
+ Autopilot's `electron/scripts/shell-link.mjs` is the reference: it makes
102
+ `electron/shell` a junction (a symlink off Windows) to that folder, runs from
103
+ npm's postinstall and from the vite config on every build, and re-points a
104
+ stale link rather than trusting it. With it in place:
105
+
106
+ ```ts
107
+ // electron.vite.config.ts
108
+ const shell = ensureShellLink(__dirname)
109
+ resolve: { alias: { '@de/shell-main': resolve(shell, 'main', 'index.ts') },
110
+ dedupe: ['react', 'react-dom'] }
111
+ ```
112
+
113
+ ```json
114
+ // tsconfig.json
115
+ "noEmit": true, "allowImportingTsExtensions": true,
116
+ "paths": { "@de/shell-main": ["./shell/main/index.ts"], … }
117
+ ```
118
+
119
+ `dedupe` matters: an editable checkout carries its own `node_modules` for
120
+ its typecheck, and without it the renderer would bundle a second React. The
121
+ peer dependencies — react, electron, electron-updater, @playwright/test —
122
+ are the app's to declare; every app already does. The e2e specs take the
123
+ harness from `shell/testing/harness.cjs`.
124
+
125
+ Autopilot and SpyDE (on its `chore/de-shell-split` branch) are wired this
126
+ way; Ground Crew still carries a copy under its `packages/` and is next.
127
+
128
+ ## Developing it
129
+
130
+ ```bash
131
+ uv sync --extra tests && uv run pytest # the Python suite
132
+ ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install # types only; drop the variable to run Electron
133
+ npm run typecheck # every target, tests included, under tsconfig.json
134
+ npm run test:unit # node --test over de_shell/js
135
+ uv build # the wheel — CI checks it carries de_shell/js
136
+ ```
137
+
138
+ The unit tests run under Node's native type stripping, which resolves
139
+ relative imports literally — so shell modules import their siblings with the
140
+ `.ts` extension spelled out, and every tsconfig that compiles them (this one
141
+ and each app's) sets `allowImportingTsExtensions`.
142
+
143
+ CI (`.github/workflows/ci.yml`) runs the Python suite on Linux, Windows and
144
+ macOS at the oldest and newest supported Python, the typecheck and unit
145
+ tests, and builds the wheel and checks what it carries.
146
+
147
+ ## Releasing
148
+
149
+ The version is written once, in `de_shell/__init__.py`. To release:
150
+
151
+ 1. Bump `__version__`, move the `CHANGELOG.md` entries under the new version.
152
+ 2. Commit, tag `vX.Y.Z`, push the tag.
153
+
154
+ `.github/workflows/publish.yml` builds the distributions, refuses a tag that
155
+ does not match `__version__`, and uploads to PyPI through trusted publishing
156
+ — on pypi.org the project must list this repository, that workflow file and
157
+ the `pypi` environment as a publisher (no token lives in the repo). Semver,
158
+ with the 0.x caveat: a breaking change to the sidecar protocol bumps the
159
+ minor, and the apps pin `>=0.x,<0.(x+1)`.
160
+
161
+ ## Provenance
162
+
163
+ Merged 2026-09-02 from the three vendored copies, three-way against the
164
+ SpyDE commit the app copies were taken from:
165
+
166
+ * SpyDE `main` @ 1f3331d (v0.4.3): the problem reporter (`errorReport`,
167
+ `problemLog`, `sentryEnvelope`), `recentBackendOutput`, the workspace-member
168
+ wheels in `pythonEnv`, the update handoff that tree-kills the sidecar first,
169
+ `run_on_worker`'s in-flight count and `ComputeHandle` in `lifecycle.py`.
170
+ * Autopilot @ 7f0651e: the sidecar's close handler forgets only ITS child, the
171
+ malformed-message report, the figure/stream fixes ported from the siblings,
172
+ the renderer state and FigureFrame changes.
173
+ * Ground Crew `main` @ 26e853a: the spawn-error trap and 5 s tree-kill grace,
174
+ the resolved `uv` path, the open-directory dialog, `_pin_tile_band` (black
175
+ panes on large stills), JSON emit that never writes bare `NaN`, the harness
176
+ hardening, and the unit tests for all of it.
177
+
178
+ Not yet included: Ground Crew's `dev/instrument-actions` shell deltas
179
+ (`stdoutDemux`, `sizeReporter`, the `frameBytes` transport) — they ride a
180
+ branch that has not merged.
181
+
182
+ ## Rules
183
+
184
+ * **Nothing here mentions a detector, a signal type, or an analysis.** If
185
+ extracting something into the shell requires touching one, the boundary is
186
+ in the wrong place.
187
+ * **The Python side stays tiny.** Every dependency added is one all three
188
+ apps install: numpy, anyplotlib, pyyaml, and that is the list.
189
+ * **The protocol is the contract.** `PLOTAPP:` JSON lines and `PLOTBIN:`
190
+ binary frames over the sidecar's stdio. Both halves of it live in this one
191
+ package on purpose; keep it that way.
192
+ * **LF line endings**, enforced by `.gitattributes`.
193
+
194
+ ## License
195
+
196
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,161 @@
1
+ # DE Shell
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/de-shell.svg)](https://pypi.org/project/de-shell/)
4
+ [![CI](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ The substrate Direct Electron's desktop apps are assembled from: **SpyDE**
8
+ (offline analysis), **Ground Crew** (manual camera control) and **Autopilot**
9
+ (automated acquisition). Each app is an Electron window over a Python
10
+ sidecar, and everything the three have in common lives here — the
11
+ Python↔JS message pipe, the window and its menus, the figure bridge over
12
+ [anyplotlib](https://pypi.org/project/anyplotlib/), the sidecar process
13
+ manager and its Python environment, the updater, the problem reporter, the
14
+ Playwright harness.
15
+
16
+ It contains **no domain logic**. No detectors, no microscopes, no signal
17
+ types, no analysis. `tests/test_boundary.py` enforces that in a clean
18
+ subprocess: the shell must stay installable without the science stack, so
19
+ the live in-memory apps never acquire it transitively.
20
+
21
+ ```bash
22
+ pip install de-shell
23
+ ```
24
+
25
+ ## One package
26
+
27
+ The shell is **one pip package**, and the TypeScript rides inside the wheel:
28
+
29
+ ```
30
+ pyproject.toml the package: de-shell
31
+ de_shell/ app loop, session base, actions, IPC, figures, compute, logging
32
+ de_shell/js/ the TypeScript half, one folder per Electron target
33
+ main/ Electron main: window, sidecar + stdout demuxer, python env, updater, reports
34
+ preload/ the contextBridge surface (exposeShellBridge)
35
+ renderer/ React: figure bridge, FigureFrame, the chrome slice of state
36
+ testing/ the Playwright harness (launchApp)
37
+ tests/ the Python suite (incl. the boundary test)
38
+ package.json DEV ONLY: typechecks and unit-tests de_shell/js; nothing is published to npm
39
+ ```
40
+
41
+ The JavaScript that speaks the sidecar protocol ships in the same artifact
42
+ as the Python that speaks it. One `pip install -U de-shell` moves both, and
43
+ an app cannot end up with the two halves at different versions. The
44
+ TypeScript is shipped as **source** and compiled by the consuming app's
45
+ bundler, so there is no build step here and an editable install is
46
+ live-editable from the app.
47
+
48
+ ## Consuming it
49
+
50
+ **Python** — an ordinary dependency:
51
+
52
+ ```toml
53
+ dependencies = ["de-shell>=0.2,<0.3"]
54
+ ```
55
+
56
+ For hacking on the shell and an app at once, a sibling checkout as an
57
+ editable path source (uv) or `pip install -e ../de-shell` overlays it.
58
+
59
+ **Electron** — ask the installed package where its TypeScript is and link
60
+ it into the project at a fixed path, then alias and `paths` through the link:
61
+
62
+ ```
63
+ python -m de_shell.js # prints …/de_shell/js
64
+ ```
65
+
66
+ Autopilot's `electron/scripts/shell-link.mjs` is the reference: it makes
67
+ `electron/shell` a junction (a symlink off Windows) to that folder, runs from
68
+ npm's postinstall and from the vite config on every build, and re-points a
69
+ stale link rather than trusting it. With it in place:
70
+
71
+ ```ts
72
+ // electron.vite.config.ts
73
+ const shell = ensureShellLink(__dirname)
74
+ resolve: { alias: { '@de/shell-main': resolve(shell, 'main', 'index.ts') },
75
+ dedupe: ['react', 'react-dom'] }
76
+ ```
77
+
78
+ ```json
79
+ // tsconfig.json
80
+ "noEmit": true, "allowImportingTsExtensions": true,
81
+ "paths": { "@de/shell-main": ["./shell/main/index.ts"], … }
82
+ ```
83
+
84
+ `dedupe` matters: an editable checkout carries its own `node_modules` for
85
+ its typecheck, and without it the renderer would bundle a second React. The
86
+ peer dependencies — react, electron, electron-updater, @playwright/test —
87
+ are the app's to declare; every app already does. The e2e specs take the
88
+ harness from `shell/testing/harness.cjs`.
89
+
90
+ Autopilot and SpyDE (on its `chore/de-shell-split` branch) are wired this
91
+ way; Ground Crew still carries a copy under its `packages/` and is next.
92
+
93
+ ## Developing it
94
+
95
+ ```bash
96
+ uv sync --extra tests && uv run pytest # the Python suite
97
+ ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install # types only; drop the variable to run Electron
98
+ npm run typecheck # every target, tests included, under tsconfig.json
99
+ npm run test:unit # node --test over de_shell/js
100
+ uv build # the wheel — CI checks it carries de_shell/js
101
+ ```
102
+
103
+ The unit tests run under Node's native type stripping, which resolves
104
+ relative imports literally — so shell modules import their siblings with the
105
+ `.ts` extension spelled out, and every tsconfig that compiles them (this one
106
+ and each app's) sets `allowImportingTsExtensions`.
107
+
108
+ CI (`.github/workflows/ci.yml`) runs the Python suite on Linux, Windows and
109
+ macOS at the oldest and newest supported Python, the typecheck and unit
110
+ tests, and builds the wheel and checks what it carries.
111
+
112
+ ## Releasing
113
+
114
+ The version is written once, in `de_shell/__init__.py`. To release:
115
+
116
+ 1. Bump `__version__`, move the `CHANGELOG.md` entries under the new version.
117
+ 2. Commit, tag `vX.Y.Z`, push the tag.
118
+
119
+ `.github/workflows/publish.yml` builds the distributions, refuses a tag that
120
+ does not match `__version__`, and uploads to PyPI through trusted publishing
121
+ — on pypi.org the project must list this repository, that workflow file and
122
+ the `pypi` environment as a publisher (no token lives in the repo). Semver,
123
+ with the 0.x caveat: a breaking change to the sidecar protocol bumps the
124
+ minor, and the apps pin `>=0.x,<0.(x+1)`.
125
+
126
+ ## Provenance
127
+
128
+ Merged 2026-09-02 from the three vendored copies, three-way against the
129
+ SpyDE commit the app copies were taken from:
130
+
131
+ * SpyDE `main` @ 1f3331d (v0.4.3): the problem reporter (`errorReport`,
132
+ `problemLog`, `sentryEnvelope`), `recentBackendOutput`, the workspace-member
133
+ wheels in `pythonEnv`, the update handoff that tree-kills the sidecar first,
134
+ `run_on_worker`'s in-flight count and `ComputeHandle` in `lifecycle.py`.
135
+ * Autopilot @ 7f0651e: the sidecar's close handler forgets only ITS child, the
136
+ malformed-message report, the figure/stream fixes ported from the siblings,
137
+ the renderer state and FigureFrame changes.
138
+ * Ground Crew `main` @ 26e853a: the spawn-error trap and 5 s tree-kill grace,
139
+ the resolved `uv` path, the open-directory dialog, `_pin_tile_band` (black
140
+ panes on large stills), JSON emit that never writes bare `NaN`, the harness
141
+ hardening, and the unit tests for all of it.
142
+
143
+ Not yet included: Ground Crew's `dev/instrument-actions` shell deltas
144
+ (`stdoutDemux`, `sizeReporter`, the `frameBytes` transport) — they ride a
145
+ branch that has not merged.
146
+
147
+ ## Rules
148
+
149
+ * **Nothing here mentions a detector, a signal type, or an analysis.** If
150
+ extracting something into the shell requires touching one, the boundary is
151
+ in the wrong place.
152
+ * **The Python side stays tiny.** Every dependency added is one all three
153
+ apps install: numpy, anyplotlib, pyyaml, and that is the list.
154
+ * **The protocol is the contract.** `PLOTAPP:` JSON lines and `PLOTBIN:`
155
+ binary frames over the sidecar's stdio. Both halves of it live in this one
156
+ package on purpose; keep it that way.
157
+ * **LF line endings**, enforced by `.gitattributes`.
158
+
159
+ ## License
160
+
161
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ """
2
+ de_shell — the Python half of the DE desktop-app shell.
3
+
4
+ The substrate three applications share: SpyDE (offline EM analysis),
5
+ de-groundcrew (live camera/hardware control) and de-autopilot (automated
6
+ acquisition). It answers "how do I be a desktop app with a Python brain and
7
+ pictures in it?" — the asyncio stdin/stdout loop, the PLOTAPP IPC protocol, log
8
+ streaming, the window/figure registry, the action + staged-wizard framework, and
9
+ the anyplotlib plotting wrapper.
10
+
11
+ It answers nothing about what the data IS. No HyperSpy, no Dask, no
12
+ RosettaSciIO, no pyxem — de-groundcrew and de-autopilot are live, in-memory
13
+ applications and must not acquire those dependencies transitively. That
14
+ constraint is what fixes the boundary, and `tests/test_boundary.py` enforces
15
+ it in a clean subprocess: anything that answers "what is the data and what do
16
+ you do to it?" (the array-cache tiering, the signal tree, the navigator read
17
+ path, the distributed compute branch, every action handler) stays in the app.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ #: The package version — the one place it is written. pyproject.toml reads it
22
+ #: (dynamic version) and the release workflow refuses a tag that disagrees.
23
+ __version__ = "0.2.0"
24
+
25
+ __all__ = ["ipc", "log_stream", "process_guard", "debug_flags", "compute"]
File without changes
@@ -0,0 +1,62 @@
1
+ """
2
+ context.py — ActionContext: the adapter passed to action functions.
3
+
4
+ An action function is handed one of these instead of reaching for the UI. It
5
+ carries the clicked plot, the parameter values the frontend's panel collected
6
+ (forwarded as kwargs), and a per-plot scratch dict for state that must outlive a
7
+ single invocation (an FFT window, a toggle group, a widget the action added).
8
+
9
+ Everything it touches is duck-typed — ``plot.plot_window``, ``plot.session`` —
10
+ so it does not care what kind of plot or session an app has.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+
17
+ class ActionContext:
18
+ """The attribute surface an action function is written against."""
19
+
20
+ def __init__(self, plot, params: dict[str, Any] | None = None,
21
+ action_name: str = ""):
22
+ self.plot = plot
23
+ self.params = params or {}
24
+ self.action_name = action_name
25
+
26
+ # Per-plot persistent action state (FFT windows, toggle groups, …).
27
+ # Stored ON THE PLOT so it survives across action invocations — an
28
+ # ActionContext is built fresh for each one.
29
+ if not hasattr(plot, "_action_widgets"):
30
+ plot._action_widgets = {}
31
+ self.action_widgets = plot._action_widgets
32
+
33
+ # ── Plot / session access ─────────────────────────────────────────────────
34
+
35
+ @property
36
+ def plot_window(self):
37
+ return self.plot.plot_window
38
+
39
+ @property
40
+ def session(self):
41
+ return self.plot.session
42
+
43
+ # ── Stateful action registration ──────────────────────────────────────────
44
+
45
+ def register_action_plot_item(self, action_name: str, item, key: str) -> None:
46
+ slot = self.action_widgets.setdefault(action_name, {})
47
+ slot.setdefault("plot_items", {})[key] = item
48
+
49
+ def register_action_plot_window(self, action_name: str, plot_window, key: str) -> None:
50
+ slot = self.action_widgets.setdefault(action_name, {})
51
+ slot.setdefault("plot_windows", {})[key] = plot_window
52
+
53
+ def add_action_widget(self, action_name: str, widget=None, layout=None) -> None:
54
+ slot = self.action_widgets.setdefault(action_name, {})
55
+ slot["widget"] = widget
56
+ slot["layout"] = layout
57
+
58
+ def actions(self) -> list:
59
+ """The toolbar lives in the frontend, so there are no host-side action
60
+ objects to return. Kept because action code written against the old Qt
61
+ toolbar still calls it."""
62
+ return []
@@ -0,0 +1,53 @@
1
+ """
2
+ figure_registry.py — per-window keep-alive for bare anyplotlib figures.
3
+
4
+ Result windows that are NOT registered ``Plot``s emit raw ``figure`` messages
5
+ whose Python-side figure objects must be kept referenced, or their widget
6
+ callbacks are garbage-collected while the window is still open. Historically
7
+ each module kept its own append-only ``_ALIVE`` list, which leaked every figure
8
+ for the process lifetime.
9
+
10
+ This registry keys the references by ``window_id`` and is evicted from the
11
+ session's ``_forget_window``, so a figure lives exactly as long as its window.
12
+
13
+ Apps hang their own per-window state off the same eviction via
14
+ :func:`register_evictor`, rather than this module reaching into them — which is
15
+ what it used to do (a hardcoded import of SpyDE's ``actions.views``).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Callable
20
+
21
+ _FIGS: dict[int, list[Any]] = {}
22
+
23
+ #: App callbacks run when a window is forgotten. See `register_evictor`.
24
+ _EVICTORS: list[Callable[[int], None]] = []
25
+
26
+
27
+ def register_evictor(fn: Callable[[int], None]) -> None:
28
+ """Register ``fn(window_id)`` to run whenever a window is forgotten.
29
+
30
+ For app state keyed by window id that must die with the window — SpyDE's
31
+ per-window chip-view arrays, for instance. Registering the same function
32
+ twice is a no-op, so a module can call this at import without guarding.
33
+ """
34
+ if fn not in _EVICTORS:
35
+ _EVICTORS.append(fn)
36
+
37
+
38
+ def keep_alive(window_id: int, fig: Any) -> None:
39
+ """Keep *fig* referenced until *window_id*'s window is forgotten."""
40
+ _FIGS.setdefault(int(window_id), []).append(fig)
41
+
42
+
43
+ def forget_window(window_id: int) -> None:
44
+ """Drop every reference held for *window_id*, and run the app's evictors."""
45
+ wid = int(window_id)
46
+ _FIGS.pop(wid, None)
47
+ for fn in _EVICTORS:
48
+ try:
49
+ fn(wid)
50
+ except Exception:
51
+ # Teardown must not fail: this runs while a window is going away,
52
+ # and one app's bookkeeping error should not strand the rest.
53
+ pass