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.
- de_shell-0.2.0/LICENSE +21 -0
- de_shell-0.2.0/PKG-INFO +196 -0
- de_shell-0.2.0/README.md +161 -0
- de_shell-0.2.0/de_shell/__init__.py +25 -0
- de_shell-0.2.0/de_shell/actions/__init__.py +0 -0
- de_shell-0.2.0/de_shell/actions/context.py +62 -0
- de_shell-0.2.0/de_shell/actions/figure_registry.py +53 -0
- de_shell-0.2.0/de_shell/actions/lifecycle.py +295 -0
- de_shell-0.2.0/de_shell/actions/registry.py +141 -0
- de_shell-0.2.0/de_shell/actions/wizard.py +115 -0
- de_shell-0.2.0/de_shell/app.py +170 -0
- de_shell-0.2.0/de_shell/compute.py +103 -0
- de_shell-0.2.0/de_shell/debug_flags.py +69 -0
- de_shell-0.2.0/de_shell/ipc.py +236 -0
- de_shell-0.2.0/de_shell/js/__init__.py +38 -0
- de_shell-0.2.0/de_shell/js/__main__.py +4 -0
- de_shell-0.2.0/de_shell/js/main/backendProcess.test.ts +70 -0
- de_shell-0.2.0/de_shell/js/main/backendProcess.ts +330 -0
- de_shell-0.2.0/de_shell/js/main/config.ts +53 -0
- de_shell-0.2.0/de_shell/js/main/dialogs.ts +62 -0
- de_shell-0.2.0/de_shell/js/main/envProgress.ts +126 -0
- de_shell-0.2.0/de_shell/js/main/errorReport.ts +261 -0
- de_shell-0.2.0/de_shell/js/main/index.ts +57 -0
- de_shell-0.2.0/de_shell/js/main/problemLog.ts +53 -0
- de_shell-0.2.0/de_shell/js/main/pythonEnv.test.ts +125 -0
- de_shell-0.2.0/de_shell/js/main/pythonEnv.ts +442 -0
- de_shell-0.2.0/de_shell/js/main/sentryEnvelope.test.ts +94 -0
- de_shell-0.2.0/de_shell/js/main/sentryEnvelope.ts +100 -0
- de_shell-0.2.0/de_shell/js/main/updater.ts +322 -0
- de_shell-0.2.0/de_shell/js/main/updaterErrors.test.ts +111 -0
- de_shell-0.2.0/de_shell/js/main/updaterErrors.ts +65 -0
- de_shell-0.2.0/de_shell/js/main/window.ts +141 -0
- de_shell-0.2.0/de_shell/js/package.json +5 -0
- de_shell-0.2.0/de_shell/js/preload/index.ts +130 -0
- de_shell-0.2.0/de_shell/js/renderer/FigureFrame.tsx +88 -0
- de_shell-0.2.0/de_shell/js/renderer/figureBridge.react.ts +58 -0
- de_shell-0.2.0/de_shell/js/renderer/figureBridge.test.ts +184 -0
- de_shell-0.2.0/de_shell/js/renderer/figureBridge.ts +169 -0
- de_shell-0.2.0/de_shell/js/renderer/index.ts +34 -0
- de_shell-0.2.0/de_shell/js/renderer/protocol.ts +164 -0
- de_shell-0.2.0/de_shell/js/renderer/shellState.test.ts +193 -0
- de_shell-0.2.0/de_shell/js/renderer/shellState.ts +310 -0
- de_shell-0.2.0/de_shell/js/testing/harness.cjs +244 -0
- de_shell-0.2.0/de_shell/js/testing/harness.test.cjs +73 -0
- de_shell-0.2.0/de_shell/log_stream.py +185 -0
- de_shell-0.2.0/de_shell/plotting/__init__.py +0 -0
- de_shell-0.2.0/de_shell/plotting/colormaps.py +27 -0
- de_shell-0.2.0/de_shell/plotting/figure.py +601 -0
- de_shell-0.2.0/de_shell/plotting/selectors/__init__.py +0 -0
- de_shell-0.2.0/de_shell/plotting/selectors/utils.py +29 -0
- de_shell-0.2.0/de_shell/plotting/stream.py +172 -0
- de_shell-0.2.0/de_shell/process_guard.py +190 -0
- de_shell-0.2.0/de_shell/session.py +211 -0
- de_shell-0.2.0/de_shell/testing/__init__.py +0 -0
- de_shell-0.2.0/de_shell/timing.py +28 -0
- de_shell-0.2.0/de_shell.egg-info/PKG-INFO +196 -0
- de_shell-0.2.0/de_shell.egg-info/SOURCES.txt +65 -0
- de_shell-0.2.0/de_shell.egg-info/dependency_links.txt +1 -0
- de_shell-0.2.0/de_shell.egg-info/requires.txt +7 -0
- de_shell-0.2.0/de_shell.egg-info/top_level.txt +1 -0
- de_shell-0.2.0/pyproject.toml +83 -0
- de_shell-0.2.0/setup.cfg +4 -0
- de_shell-0.2.0/tests/test_actions.py +294 -0
- de_shell-0.2.0/tests/test_boundary.py +127 -0
- de_shell-0.2.0/tests/test_figure.py +158 -0
- de_shell-0.2.0/tests/test_session_base.py +178 -0
- 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.
|
de_shell-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/de-shell/)
|
|
39
|
+
[](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml)
|
|
40
|
+
[](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).
|
de_shell-0.2.0/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# DE Shell
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/de-shell/)
|
|
4
|
+
[](https://github.com/CSSFrancis/de-shell/actions/workflows/ci.yml)
|
|
5
|
+
[](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
|