webapp-shell 0.1__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 (176) hide show
  1. webapp_shell-0.1/.gitignore +12 -0
  2. webapp_shell-0.1/CHANGE_LOG.md +28 -0
  3. webapp_shell-0.1/CONTRIBUTING.md +62 -0
  4. webapp_shell-0.1/LICENSE +21 -0
  5. webapp_shell-0.1/PKG-INFO +132 -0
  6. webapp_shell-0.1/README.md +91 -0
  7. webapp_shell-0.1/change_logs/README.md +5 -0
  8. webapp_shell-0.1/docs/API.md +273 -0
  9. webapp_shell-0.1/docs/CONFIGURATION.md +52 -0
  10. webapp_shell-0.1/docs/CONTRACT.md +42 -0
  11. webapp_shell-0.1/docs/FREEZING.md +69 -0
  12. webapp_shell-0.1/docs/NATIVE_FUNCTIONS.md +51 -0
  13. webapp_shell-0.1/docs/REMAINING.md +30 -0
  14. webapp_shell-0.1/docs/TRANSLATING.md +74 -0
  15. webapp_shell-0.1/docs/TROUBLESHOOTING.md +16 -0
  16. webapp_shell-0.1/examples/minimal/README.md +21 -0
  17. webapp_shell-0.1/examples/minimal/acme_backend/__init__.py +50 -0
  18. webapp_shell-0.1/examples/minimal/acme_backend/__main__.py +19 -0
  19. webapp_shell-0.1/examples/minimal/acme_backend/spec.py +16 -0
  20. webapp_shell-0.1/examples/minimal/app.py +25 -0
  21. webapp_shell-0.1/examples/minimal/freeze_plan.py +15 -0
  22. webapp_shell-0.1/examples/minimal/frontend/dist/app.js +26 -0
  23. webapp_shell-0.1/examples/minimal/frontend/dist/index.html +19 -0
  24. webapp_shell-0.1/examples/minimal/frontend/dist/webapp-shell-bridge.d.ts +48 -0
  25. webapp_shell-0.1/examples/minimal/frontend/dist/webapp-shell-bridge.js +146 -0
  26. webapp_shell-0.1/examples/site/.prettierignore +3 -0
  27. webapp_shell-0.1/examples/site/.prettierrc.json +1 -0
  28. webapp_shell-0.1/examples/site/app.py +27 -0
  29. webapp_shell-0.1/examples/site/docs_backend/__init__.py +55 -0
  30. webapp_shell-0.1/examples/site/docs_backend/__main__.py +19 -0
  31. webapp_shell-0.1/examples/site/docs_backend/crash.py +10 -0
  32. webapp_shell-0.1/examples/site/docs_backend/describe.py +12 -0
  33. webapp_shell-0.1/examples/site/docs_backend/job.py +37 -0
  34. webapp_shell-0.1/examples/site/docs_backend/list_folder.py +13 -0
  35. webapp_shell-0.1/examples/site/docs_backend/messages.py +9 -0
  36. webapp_shell-0.1/examples/site/docs_backend/save_note.py +14 -0
  37. webapp_shell-0.1/examples/site/docs_backend/spec.py +16 -0
  38. webapp_shell-0.1/examples/site/frontend/dist/assets/index-DPVhUfoU.js +584 -0
  39. webapp_shell-0.1/examples/site/frontend/dist/assets/index-UmDhVWJw.css +1 -0
  40. webapp_shell-0.1/examples/site/frontend/dist/index.html +13 -0
  41. webapp_shell-0.1/examples/site/index.html +12 -0
  42. webapp_shell-0.1/examples/site/native_functions.py +28 -0
  43. webapp_shell-0.1/examples/site/package-lock.json +3272 -0
  44. webapp_shell-0.1/examples/site/package.json +30 -0
  45. webapp_shell-0.1/examples/site/src/App.tsx +83 -0
  46. webapp_shell-0.1/examples/site/src/bridge/webapp-shell-bridge.d.ts +48 -0
  47. webapp_shell-0.1/examples/site/src/bridge/webapp-shell-bridge.js +146 -0
  48. webapp_shell-0.1/examples/site/src/bridgeContext.tsx +18 -0
  49. webapp_shell-0.1/examples/site/src/browserNative.ts +134 -0
  50. webapp_shell-0.1/examples/site/src/components/CodeBlock.tsx +115 -0
  51. webapp_shell-0.1/examples/site/src/components/CodeTabs.tsx +39 -0
  52. webapp_shell-0.1/examples/site/src/components/Demo.tsx +41 -0
  53. webapp_shell-0.1/examples/site/src/components/FeaturePage.tsx +56 -0
  54. webapp_shell-0.1/examples/site/src/components/Flow.tsx +42 -0
  55. webapp_shell-0.1/examples/site/src/components/Guide.tsx +65 -0
  56. webapp_shell-0.1/examples/site/src/demos/bootColors.ts +6 -0
  57. webapp_shell-0.1/examples/site/src/demos/health.ts +18 -0
  58. webapp_shell-0.1/examples/site/src/demos/job.ts +17 -0
  59. webapp_shell-0.1/examples/site/src/demos/messages.ts +12 -0
  60. webapp_shell-0.1/examples/site/src/demos/openLink.ts +4 -0
  61. webapp_shell-0.1/examples/site/src/demos/ownFunction.ts +11 -0
  62. webapp_shell-0.1/examples/site/src/demos/pickFile.ts +15 -0
  63. webapp_shell-0.1/examples/site/src/demos/pickFolder.ts +15 -0
  64. webapp_shell-0.1/examples/site/src/demos/reveal.ts +9 -0
  65. webapp_shell-0.1/examples/site/src/demos/saveAs.ts +18 -0
  66. webapp_shell-0.1/examples/site/src/demos/stopping.ts +18 -0
  67. webapp_shell-0.1/examples/site/src/demos/zoom.ts +6 -0
  68. webapp_shell-0.1/examples/site/src/main.tsx +10 -0
  69. webapp_shell-0.1/examples/site/src/pages/BackendJob.tsx +53 -0
  70. webapp_shell-0.1/examples/site/src/pages/BackendStopping.tsx +63 -0
  71. webapp_shell-0.1/examples/site/src/pages/BootColors.tsx +84 -0
  72. webapp_shell-0.1/examples/site/src/pages/ChooseFile.tsx +95 -0
  73. webapp_shell-0.1/examples/site/src/pages/ChooseFolder.tsx +96 -0
  74. webapp_shell-0.1/examples/site/src/pages/ForFrontend.tsx +73 -0
  75. webapp_shell-0.1/examples/site/src/pages/GettingStarted.tsx +41 -0
  76. webapp_shell-0.1/examples/site/src/pages/OpenLink.tsx +42 -0
  77. webapp_shell-0.1/examples/site/src/pages/Overview.tsx +38 -0
  78. webapp_shell-0.1/examples/site/src/pages/OwnFunction.tsx +79 -0
  79. webapp_shell-0.1/examples/site/src/pages/Reveal.tsx +40 -0
  80. webapp_shell-0.1/examples/site/src/pages/SaveAs.tsx +83 -0
  81. webapp_shell-0.1/examples/site/src/pages/Security.tsx +64 -0
  82. webapp_shell-0.1/examples/site/src/pages/StartUp.tsx +111 -0
  83. webapp_shell-0.1/examples/site/src/pages/TokenAndHealth.tsx +80 -0
  84. webapp_shell-0.1/examples/site/src/pages/Zoom.tsx +74 -0
  85. webapp_shell-0.1/examples/site/src/pages/guides.tsx +14 -0
  86. webapp_shell-0.1/examples/site/src/pages.ts +231 -0
  87. webapp_shell-0.1/examples/site/src/styles.css +393 -0
  88. webapp_shell-0.1/examples/site/src/theme.ts +28 -0
  89. webapp_shell-0.1/examples/site/src/vite-env.d.ts +1 -0
  90. webapp_shell-0.1/examples/site/tsconfig.json +14 -0
  91. webapp_shell-0.1/examples/site/vite.config.ts +9 -0
  92. webapp_shell-0.1/pyproject.toml +68 -0
  93. webapp_shell-0.1/src/webapp_shell/__init__.py +50 -0
  94. webapp_shell-0.1/src/webapp_shell/__main__.py +53 -0
  95. webapp_shell-0.1/src/webapp_shell/backend.py +30 -0
  96. webapp_shell-0.1/src/webapp_shell/backend_process.py +136 -0
  97. webapp_shell-0.1/src/webapp_shell/bridge/webapp-shell-bridge.d.ts +48 -0
  98. webapp_shell-0.1/src/webapp_shell/bridge/webapp-shell-bridge.js +146 -0
  99. webapp_shell-0.1/src/webapp_shell/cli.py +76 -0
  100. webapp_shell-0.1/src/webapp_shell/config.py +27 -0
  101. webapp_shell-0.1/src/webapp_shell/contract/__init__.py +10 -0
  102. webapp_shell-0.1/src/webapp_shell/contract/guard.py +106 -0
  103. webapp_shell-0.1/src/webapp_shell/contract/launch.py +49 -0
  104. webapp_shell-0.1/src/webapp_shell/contract/tokens.py +41 -0
  105. webapp_shell-0.1/src/webapp_shell/enums.py +20 -0
  106. webapp_shell-0.1/src/webapp_shell/freezing/__init__.py +10 -0
  107. webapp_shell-0.1/src/webapp_shell/freezing/app.spec +5 -0
  108. webapp_shell-0.1/src/webapp_shell/freezing/builder.py +82 -0
  109. webapp_shell-0.1/src/webapp_shell/freezing/entry.py +24 -0
  110. webapp_shell-0.1/src/webapp_shell/freezing/plan.py +38 -0
  111. webapp_shell-0.1/src/webapp_shell/freezing/profile.py +49 -0
  112. webapp_shell-0.1/src/webapp_shell/freezing/spec.py +68 -0
  113. webapp_shell-0.1/src/webapp_shell/freezing/verify.py +43 -0
  114. webapp_shell-0.1/src/webapp_shell/identity.py +48 -0
  115. webapp_shell-0.1/src/webapp_shell/installation.py +41 -0
  116. webapp_shell-0.1/src/webapp_shell/json_types.py +4 -0
  117. webapp_shell-0.1/src/webapp_shell/library_check.py +28 -0
  118. webapp_shell-0.1/src/webapp_shell/messages.py +58 -0
  119. webapp_shell-0.1/src/webapp_shell/native/__init__.py +4 -0
  120. webapp_shell-0.1/src/webapp_shell/native/arguments.py +131 -0
  121. webapp_shell-0.1/src/webapp_shell/native/errors.py +5 -0
  122. webapp_shell-0.1/src/webapp_shell/native/filters.py +24 -0
  123. webapp_shell-0.1/src/webapp_shell/native/os_actions.py +33 -0
  124. webapp_shell-0.1/src/webapp_shell/native/qt_bridge.py +62 -0
  125. webapp_shell-0.1/src/webapp_shell/native/qt_window.py +34 -0
  126. webapp_shell-0.1/src/webapp_shell/native/registry.py +91 -0
  127. webapp_shell-0.1/src/webapp_shell/native/standard.py +89 -0
  128. webapp_shell-0.1/src/webapp_shell/native/start_location.py +28 -0
  129. webapp_shell-0.1/src/webapp_shell/native/webview_background.py +39 -0
  130. webapp_shell-0.1/src/webapp_shell/native/webview_bridge.py +25 -0
  131. webapp_shell-0.1/src/webapp_shell/native/webview_icon.py +32 -0
  132. webapp_shell-0.1/src/webapp_shell/native/webview_window.py +54 -0
  133. webapp_shell-0.1/src/webapp_shell/native/window.py +34 -0
  134. webapp_shell-0.1/src/webapp_shell/native/zoom.py +104 -0
  135. webapp_shell-0.1/src/webapp_shell/native/zoom_range.py +9 -0
  136. webapp_shell-0.1/src/webapp_shell/preferences.py +84 -0
  137. webapp_shell-0.1/src/webapp_shell/protocol.py +34 -0
  138. webapp_shell-0.1/src/webapp_shell/py.typed +0 -0
  139. webapp_shell-0.1/src/webapp_shell/qt_shell.py +126 -0
  140. webapp_shell-0.1/src/webapp_shell/registry.py +20 -0
  141. webapp_shell-0.1/src/webapp_shell/settings.py +78 -0
  142. webapp_shell-0.1/src/webapp_shell/shell.py +119 -0
  143. webapp_shell-0.1/src/webapp_shell/smoke.py +30 -0
  144. webapp_shell-0.1/src/webapp_shell/splash.py +52 -0
  145. webapp_shell-0.1/src/webapp_shell/watchdog.py +60 -0
  146. webapp_shell-0.1/src/webapp_shell/webview2.py +80 -0
  147. webapp_shell-0.1/src/webapp_shell/webview_shell.py +86 -0
  148. webapp_shell-0.1/tests/__init__.py +0 -0
  149. webapp_shell-0.1/tests/conftest.py +48 -0
  150. webapp_shell-0.1/tests/fake_backend/__init__.py +1 -0
  151. webapp_shell-0.1/tests/fake_backend/__main__.py +49 -0
  152. webapp_shell-0.1/tests/test_api_listing.py +17 -0
  153. webapp_shell-0.1/tests/test_backend_command.py +48 -0
  154. webapp_shell-0.1/tests/test_backend_process.py +72 -0
  155. webapp_shell-0.1/tests/test_boot_look.py +96 -0
  156. webapp_shell-0.1/tests/test_bridge_js.py +131 -0
  157. webapp_shell-0.1/tests/test_change_log.py +22 -0
  158. webapp_shell-0.1/tests/test_cli.py +55 -0
  159. webapp_shell-0.1/tests/test_config.py +40 -0
  160. webapp_shell-0.1/tests/test_contract.py +206 -0
  161. webapp_shell-0.1/tests/test_example_minimal.py +73 -0
  162. webapp_shell-0.1/tests/test_example_site.py +232 -0
  163. webapp_shell-0.1/tests/test_freezing.py +115 -0
  164. webapp_shell-0.1/tests/test_guides.py +59 -0
  165. webapp_shell-0.1/tests/test_installation.py +26 -0
  166. webapp_shell-0.1/tests/test_library_is_independent.py +16 -0
  167. webapp_shell-0.1/tests/test_messages.py +70 -0
  168. webapp_shell-0.1/tests/test_native.py +72 -0
  169. webapp_shell-0.1/tests/test_native_arguments.py +99 -0
  170. webapp_shell-0.1/tests/test_native_registry.py +268 -0
  171. webapp_shell-0.1/tests/test_start_location.py +23 -0
  172. webapp_shell-0.1/tests/test_watchdog.py +86 -0
  173. webapp_shell-0.1/tests/test_webview2.py +53 -0
  174. webapp_shell-0.1/tests/test_window_title.py +26 -0
  175. webapp_shell-0.1/tests/test_zoom.py +58 -0
  176. webapp_shell-0.1/tools/api_listing.py +133 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .DS_Store
10
+
11
+ node_modules/
12
+ !examples/*/frontend/dist/
@@ -0,0 +1,28 @@
1
+ # 0.1
2
+
3
+ The first version (not yet published). What it holds:
4
+
5
+ - **The shell as a library:** `run(config)` gives a program its command line; the backend is started, watched and restarted by the shell (`BackendProcess`, `BackendWatchdog`), with a splash and a first-run library check.
6
+ - **Config objects:** `AppIdentity`, `BackendSpec`, `WireProtocol`, `CliFlag` and `ShellConfig`; no program's name is built into the library.
7
+ - **Two windows:** pywebview (built in) and Qt (the `qt` extra).
8
+ - **The backend contract** (`webapp_shell.contract`): launch token, origin policy, launch arguments, and a Starlette `TokenGuard` (the `backend` extra).
9
+ - **WebSocket guard:** `WebSocketGuard` (Starlette) applies the same token and origin rules to a socket as `TokenGuard` does to a request, closing a refused one with code 4401.
10
+ - **Typed everywhere:** `pyright` strict passes on the whole library (`py.typed` ships); everything that crosses between the page and Python is a `JsonValue`.
11
+ - **Typed native arguments:** a function declares its arguments as a dataclass (`NativeFunction(name, Arguments, handler)`); the page's values are checked against the field types before the handler runs and nothing is converted silently.
12
+ - **Native functions:** the standard set (choose a file or folder, save as, show in the file manager, open a link, zoom, boot colours) and your own through `NativeFunction`, one envelope on both toolkits.
13
+ - **The page-side bridge:** `webapp-shell-bridge.js` with its `.d.ts`, copied into a frontend with `python -m webapp_shell bridge --out DIR`.
14
+ - **Documentation site:** twenty-two pages with live demonstrations and Frontend/Backend/Shell code tabs, a page for frontend developers and a security page whose claims are checked by tests.
15
+ - **Examples:** `examples/minimal` and the documentation site `examples/site`.
16
+ - `ShellMessages` / `MessageKey`: the shell's own sentences as one catalogue a project can translate (`ShellConfig.messages`); `python -m webapp_shell messages` prints it as JSON for a frontend; `docs/TRANSLATING.md`.
17
+ - `docs/API.md`, generated from the source by `tools/api_listing.py` (a test keeps it current), lists only the public API: the names in the `__all__` of `webapp_shell`, `webapp_shell.contract` and `webapp_shell.contract.guard`. `Installation`, `FileFilter`, `standard_functions` and `build_parser` are exported for freezing and tuning; the guide samples and the examples are tested to import nothing else.
18
+ - **Freezing:** `webapp_shell.freezing` (`FreezePlan`, `DataFolder`) and `python -m webapp_shell freeze PLAN [qt] [webview]` freeze a program with PyInstaller (`webapp-shell[freeze]`): the library writes the entry file and the spec, leaves the other shell's libraries out, and checks the folder (required files, nothing of the other shell, the frozen program's own `--check-libraries`). `examples/minimal/freeze_plan.py` is a plan; the frozen Qt build was run headless (`--smoke`) and the frozen `--run-backend` path answered with the token guard on. The zoom limits moved to `native/zoom_range.py` so a Qt-only build does not reach pywebview.
19
+ - **Site:** a page for the splash and the start-up checks (it reads the shell's sentences from the library through the site's backend), the six guides of `docs/` as pages (the same Markdown files, rendered with their code highlighted and copyable; links between guides stay in the site), and tests that keep the guides, their links and the committed build in step.
20
+ - Badges in the README (CI, PyPI, Python, licence, docs) and `CONTRIBUTING.md`.
21
+ - Guides: contract, configuration, native functions, freezing, troubleshooting (their Python samples are checked by a test).
22
+ - Pyright strict also covers the examples; CI (3 systems, Python 3.13), a Pages workflow and a trusted-publishing release workflow (TestPyPI, then PyPI on a tag).
23
+ - Package metadata (classifiers, URLs, keywords); the wheel and sdist pass `twine check`.
24
+ - Name checked free on PyPI on 1 November 2026. Checked: Starlette 0.40 and 1.10, pywebview 6.2 and 6.2.1; a clean install of the built wheel runs the example.
25
+ - Plainer start-up errors: a backend that stops while starting reports its exit code; one that never answers names the health path and the seconds waited.
26
+ - On Windows, a missing WebView2 runtime is found before any window opens and reported in a message box with the download page (`webapp_shell.webview2`).
27
+ - The example site colours every code block (Python, TypeScript, shell) with highlight.js; the README links to the documentation site.
28
+ - Example site: every code card has a Copy button; in a plain browser the demos use the browser's own features (file, folder and save pickers, zoom, theme colour, new-tab links), with a note that a browser never gives a path on the disk. Only "Show in the file manager" needs the shell.
@@ -0,0 +1,62 @@
1
+ # Contributing
2
+
3
+ Thank you for helping. The library is small and typed on purpose; these are the rules that keep it so.
4
+
5
+ ## Set up
6
+
7
+ Python 3.13 or newer, and Node (only for the documentation site and the Markdown checks).
8
+
9
+ ```sh
10
+ git clone https://github.com/prmpsmart/webapp_shell.git
11
+ cd webapp_shell
12
+ python -m venv .venv && . .venv/bin/activate # on Windows: .venv\Scripts\activate
13
+ python -m pip install -e ".[dev]" pyright
14
+ ```
15
+
16
+ ## The checks
17
+
18
+ CI runs all of these on Ubuntu, macOS and Windows; run them before you push, with the newest `ruff`.
19
+
20
+ ```sh
21
+ python -m ruff check .
22
+ python -m ruff format --check .
23
+ npx --yes prettier --check "**/*.md"
24
+ npx --yes markdownlint-cli "**/*.md" --ignore "**/node_modules/**"
25
+ python -m pyright
26
+ python -m pytest
27
+ python -m pytest -m freeze # freezes the minimal example with PyInstaller (slow)
28
+ ```
29
+
30
+ ## How the code is written
31
+
32
+ - **Typed.** `pyright` is strict on `src/` and the examples. Use dataclasses and `StrEnum`s, not dictionaries and loose
33
+ strings; everything that crosses between the page and Python is a `JsonValue`.
34
+ - **One job per class.** A window toolkit (`webview`, `PySide6`) is imported only by the modules of its own shell, never
35
+ by a shared one, so a Qt-only build never reaches pywebview (a test checks it). `PyInstaller` is imported by
36
+ `freezing/spec.py` alone.
37
+ - **The public API is what is exported.** A name is public when it is in the `__all__` of `webapp_shell`,
38
+ `webapp_shell.contract`, `webapp_shell.contract.guard` or `webapp_shell.freezing`. Adding one is a decision: it needs a
39
+ test and a line in `CHANGE_LOG.md`. Regenerate `docs/API.md` with `python tools/api_listing.py` (a test fails when it
40
+ is out of date). The examples and the guides import only public names.
41
+ - **Tests.** Every behaviour has one. A fake backend lives in `tests/fake_backend`; tests are not type-checked because
42
+ their fakes are not the real types.
43
+ - **Docs.** `CHANGE_LOG.md` holds the version in progress (`change_logs/` the past ones); `docs/REMAINING.md` lists only
44
+ what is still open. Markdown is formatted by `prettier` and checked by `markdownlint`.
45
+
46
+ ## The documentation site
47
+
48
+ `examples/site` is a React and TypeScript site that the library's own shell opens. Its compiled `frontend/dist` is
49
+ committed (a test checks that it contains the code it shows), so after changing the site:
50
+
51
+ ```sh
52
+ cd examples/site
53
+ npm ci
54
+ npm run build
55
+ ```
56
+
57
+ The guides in `docs/` are shown as pages of the site; editing one means rebuilding it.
58
+
59
+ ## A change
60
+
61
+ Open an issue first for anything that changes the public API. A pull request needs the checks above to pass, a test for
62
+ the change, and a line in `CHANGE_LOG.md` when behaviour changes.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miracle Apata (prmpsmart)
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,132 @@
1
+ Metadata-Version: 2.5
2
+ Name: webapp-shell
3
+ Version: 0.1
4
+ Summary: A desktop window for a web app made of a Python backend and a compiled frontend.
5
+ Project-URL: Homepage, https://github.com/prmpsmart/webapp_shell
6
+ Project-URL: Documentation, https://prmpsmart.github.io/webapp_shell
7
+ Project-URL: Source, https://github.com/prmpsmart/webapp_shell
8
+ Project-URL: Issues, https://github.com/prmpsmart/webapp_shell/issues
9
+ Project-URL: Changelog, https://github.com/prmpsmart/webapp_shell/blob/main/CHANGE_LOG.md
10
+ Author-email: Miracle Apata <prmpsmart@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: backend,desktop,frontend,native,python,pywebview,shell,webview
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.13
25
+ Requires-Dist: httpx>=0.28
26
+ Requires-Dist: pywebview>=6.2
27
+ Provides-Extra: backend
28
+ Requires-Dist: starlette>=0.40; extra == 'backend'
29
+ Provides-Extra: dev
30
+ Requires-Dist: pyinstaller>=6; extra == 'dev'
31
+ Requires-Dist: pyside6>=6.9; extra == 'dev'
32
+ Requires-Dist: pytest>=8; extra == 'dev'
33
+ Requires-Dist: ruff>=0.5; extra == 'dev'
34
+ Requires-Dist: starlette>=0.40; extra == 'dev'
35
+ Requires-Dist: uvicorn>=0.30; extra == 'dev'
36
+ Provides-Extra: freeze
37
+ Requires-Dist: pyinstaller>=6; extra == 'freeze'
38
+ Provides-Extra: qt
39
+ Requires-Dist: pyside6>=6.9; extra == 'qt'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # webapp-shell
43
+
44
+ [![CI](https://github.com/prmpsmart/webapp_shell/actions/workflows/ci.yml/badge.svg)](https://github.com/prmpsmart/webapp_shell/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/webapp-shell)](https://pypi.org/project/webapp-shell/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/webapp-shell)](https://pypi.org/project/webapp-shell/)
47
+ [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
48
+ [![Docs](https://img.shields.io/badge/docs-site-3556d6)](https://prmpsmart.github.io/webapp_shell)
49
+
50
+ A desktop window for a web app made of a **Python backend** and a **compiled frontend**. Your project describes itself
51
+ in one config object; the library starts the backend, waits until it is healthy, opens the frontend in a native window
52
+ (pywebview, or Qt on request) and gives the page a small set of native functions (file and folder dialogs, reveal in the
53
+ file manager, open a link, zoom).
54
+
55
+ > Documentation: <https://prmpsmart.github.io/webapp_shell>. The same site is what `examples/site` opens in the
56
+ > library's own window, where each feature's demonstration button does the real thing.
57
+ >
58
+ > By Miracle Apata ([@prmpsmart](https://github.com/prmpsmart)). MIT licence.
59
+
60
+ ```python
61
+ from pathlib import Path
62
+
63
+ from webapp_shell import AppIdentity, BackendSpec, BootColors, ShellConfig, WireProtocol, run
64
+
65
+ ROOT = Path(__file__).resolve().parent
66
+
67
+ config = ShellConfig(
68
+ identity=AppIdentity(
69
+ name="Acme Notes",
70
+ slug="acme",
71
+ storage_dir=Path.home() / ".acme",
72
+ boot_colors=BootColors(base="#102030", glow_a="#204060", glow_b="#4060a0"),
73
+ ),
74
+ backend=BackendSpec(module="acme_backend", wire=WireProtocol.for_slug("acme"), package_dirs=(ROOT / "backend",)),
75
+ source_root=ROOT, # holds frontend/dist, the compiled frontend
76
+ )
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(run(config))
80
+ ```
81
+
82
+ The backend reads the same contract with the library's helpers (Starlette or FastAPI):
83
+
84
+ ```python
85
+ parser = argparse.ArgumentParser()
86
+ add_launch_arguments(parser, config.backend) # --port, --frontend, --preferences-file, --token
87
+ launch = read_launch_arguments(parser, parser.parse_args(), config.backend.wire)
88
+ app.add_middleware(TokenGuard, validator=TokenValidator(launch.token, wire), wire=wire)
89
+ ```
90
+
91
+ Native functions: the standard set (file and folder dialogs, save as, reveal, open a link, zoom, boot colours) is always
92
+ there; a project adds its own and the page calls them by name.
93
+
94
+ ```python
95
+ from dataclasses import dataclass
96
+ from webapp_shell import JsonValue, NativeContext, NativeFunction
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class Greeting: # the page's arguments, in order; each is type-checked before your code runs
101
+ who: str
102
+
103
+
104
+ def greet(context: NativeContext, arguments: Greeting) -> JsonValue: # context.window, .preferences, .actions
105
+ return f"hello {arguments.who}"
106
+
107
+
108
+ config = ShellConfig(..., native_functions=(NativeFunction("greet", Greeting, greet),))
109
+ ```
110
+
111
+ ```js
112
+ // copy the bridge into the frontend once: python -m webapp_shell bridge --out src/
113
+ import { getBridge } from "./webapp-shell-bridge.js";
114
+
115
+ const bridge = await getBridge({ nativeGlobal: "acmeNative" });
116
+ const path = await bridge.pickFile([{ name: "Excel", extensions: ["xlsx"] }]);
117
+ const hello = await bridge.call("greet", "Ada");
118
+ ```
119
+
120
+ Install: `pip install webapp-shell` (pywebview is included); `pip install "webapp-shell[qt]"` adds Qt, and
121
+ `pip install "webapp-shell[backend]"` adds Starlette for the token guard.
122
+
123
+ On Windows the pywebview window needs the WebView2 runtime (present on Windows 11); `pythonnet` is installed by pywebview.
124
+
125
+ A runnable minimal program is in `examples/minimal/`, and `examples/site/` is the documentation website, opened by the
126
+ library's own shell, with a live demonstration of every feature.
127
+
128
+ Guides (`docs/`): [the contract](docs/CONTRACT.md), [configuration](docs/CONFIGURATION.md),
129
+ [native functions](docs/NATIVE_FUNCTIONS.md), [freezing with PyInstaller](docs/FREEZING.md),
130
+ [translating](docs/TRANSLATING.md), [troubleshooting](docs/TROUBLESHOOTING.md), and the generated
131
+ [API listing](docs/API.md) (the public API: nothing that is not listed there is promised). The same guides are pages of
132
+ the documentation site. To work on the library, read [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,91 @@
1
+ # webapp-shell
2
+
3
+ [![CI](https://github.com/prmpsmart/webapp_shell/actions/workflows/ci.yml/badge.svg)](https://github.com/prmpsmart/webapp_shell/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/webapp-shell)](https://pypi.org/project/webapp-shell/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/webapp-shell)](https://pypi.org/project/webapp-shell/)
6
+ [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
7
+ [![Docs](https://img.shields.io/badge/docs-site-3556d6)](https://prmpsmart.github.io/webapp_shell)
8
+
9
+ A desktop window for a web app made of a **Python backend** and a **compiled frontend**. Your project describes itself
10
+ in one config object; the library starts the backend, waits until it is healthy, opens the frontend in a native window
11
+ (pywebview, or Qt on request) and gives the page a small set of native functions (file and folder dialogs, reveal in the
12
+ file manager, open a link, zoom).
13
+
14
+ > Documentation: <https://prmpsmart.github.io/webapp_shell>. The same site is what `examples/site` opens in the
15
+ > library's own window, where each feature's demonstration button does the real thing.
16
+ >
17
+ > By Miracle Apata ([@prmpsmart](https://github.com/prmpsmart)). MIT licence.
18
+
19
+ ```python
20
+ from pathlib import Path
21
+
22
+ from webapp_shell import AppIdentity, BackendSpec, BootColors, ShellConfig, WireProtocol, run
23
+
24
+ ROOT = Path(__file__).resolve().parent
25
+
26
+ config = ShellConfig(
27
+ identity=AppIdentity(
28
+ name="Acme Notes",
29
+ slug="acme",
30
+ storage_dir=Path.home() / ".acme",
31
+ boot_colors=BootColors(base="#102030", glow_a="#204060", glow_b="#4060a0"),
32
+ ),
33
+ backend=BackendSpec(module="acme_backend", wire=WireProtocol.for_slug("acme"), package_dirs=(ROOT / "backend",)),
34
+ source_root=ROOT, # holds frontend/dist, the compiled frontend
35
+ )
36
+
37
+ if __name__ == "__main__":
38
+ raise SystemExit(run(config))
39
+ ```
40
+
41
+ The backend reads the same contract with the library's helpers (Starlette or FastAPI):
42
+
43
+ ```python
44
+ parser = argparse.ArgumentParser()
45
+ add_launch_arguments(parser, config.backend) # --port, --frontend, --preferences-file, --token
46
+ launch = read_launch_arguments(parser, parser.parse_args(), config.backend.wire)
47
+ app.add_middleware(TokenGuard, validator=TokenValidator(launch.token, wire), wire=wire)
48
+ ```
49
+
50
+ Native functions: the standard set (file and folder dialogs, save as, reveal, open a link, zoom, boot colours) is always
51
+ there; a project adds its own and the page calls them by name.
52
+
53
+ ```python
54
+ from dataclasses import dataclass
55
+ from webapp_shell import JsonValue, NativeContext, NativeFunction
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Greeting: # the page's arguments, in order; each is type-checked before your code runs
60
+ who: str
61
+
62
+
63
+ def greet(context: NativeContext, arguments: Greeting) -> JsonValue: # context.window, .preferences, .actions
64
+ return f"hello {arguments.who}"
65
+
66
+
67
+ config = ShellConfig(..., native_functions=(NativeFunction("greet", Greeting, greet),))
68
+ ```
69
+
70
+ ```js
71
+ // copy the bridge into the frontend once: python -m webapp_shell bridge --out src/
72
+ import { getBridge } from "./webapp-shell-bridge.js";
73
+
74
+ const bridge = await getBridge({ nativeGlobal: "acmeNative" });
75
+ const path = await bridge.pickFile([{ name: "Excel", extensions: ["xlsx"] }]);
76
+ const hello = await bridge.call("greet", "Ada");
77
+ ```
78
+
79
+ Install: `pip install webapp-shell` (pywebview is included); `pip install "webapp-shell[qt]"` adds Qt, and
80
+ `pip install "webapp-shell[backend]"` adds Starlette for the token guard.
81
+
82
+ On Windows the pywebview window needs the WebView2 runtime (present on Windows 11); `pythonnet` is installed by pywebview.
83
+
84
+ A runnable minimal program is in `examples/minimal/`, and `examples/site/` is the documentation website, opened by the
85
+ library's own shell, with a live demonstration of every feature.
86
+
87
+ Guides (`docs/`): [the contract](docs/CONTRACT.md), [configuration](docs/CONFIGURATION.md),
88
+ [native functions](docs/NATIVE_FUNCTIONS.md), [freezing with PyInstaller](docs/FREEZING.md),
89
+ [translating](docs/TRANSLATING.md), [troubleshooting](docs/TROUBLESHOOTING.md), and the generated
90
+ [API listing](docs/API.md) (the public API: nothing that is not listed there is promised). The same guides are pages of
91
+ the documentation site. To work on the library, read [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,5 @@
1
+ # Change logs of past versions
2
+
3
+ One file per released version, named `<version>.md` (`0.1.md`, `0.2.md`, …), saying what changed compared with the version before it.
4
+
5
+ The version being worked on is `CHANGE_LOG.md` in the root of the repository. When a new version starts, the root `CHANGE_LOG.md` is moved here as `<its version>.md` and a new `CHANGE_LOG.md` is begun for the new version (`tests/test_change_log.py` checks that its title is the version in `pyproject.toml`).
@@ -0,0 +1,273 @@
1
+ # API listing
2
+
3
+ Generated by `python tools/api_listing.py` from the source; do not edit by hand (a test fails when it is out of date).
4
+
5
+ This is the whole public API: what a project imports (`webapp_shell`, `webapp_shell.contract`) and what it can use to
6
+ tune the shell (`webapp_shell.contract.guard`, `standard_functions`, `build_parser`, `Installation`, `ShellMessages`).
7
+ Nothing else is promised: a module or name that is not listed here can change in any release, even though it can be
8
+ imported.
9
+
10
+ Public names: 34.
11
+
12
+ ## `webapp_shell.backend`
13
+
14
+ How a project's backend is started: a Python module run as `python -m <module>` (or, in a frozen program, by the program running itself).
15
+
16
+ - `class CliFlag` — A command-line switch the project adds to its program (`--sandbox`). When given, the backend gets `passes`.
17
+ - `name: str`
18
+ - `help: str`
19
+ - `passes: tuple[str, ...]`
20
+ - `class BackendSpec`
21
+ - `module: str`
22
+ - `wire: WireProtocol`
23
+ - `frontend_dist: Path`
24
+ - `frontend_argument: str`
25
+ - `port_argument: str`
26
+ - `preferences_argument: str | None`
27
+ - `arguments: tuple[str, ...]`
28
+ - `source_only_arguments: tuple[str, ...]`
29
+ - `flags: tuple[CliFlag, ...]`
30
+ - `package_dirs: tuple[Path, ...]`
31
+
32
+ ## `webapp_shell.cli`
33
+
34
+ The command line every project gets: `<program> [webview|qt] [--smoke] [--splash-seconds N] [project switches]`.
35
+
36
+ - `def build_parser(config: ShellConfig) -> argparse.ArgumentParser`
37
+ - `def run(config: ShellConfig, argv: list[str] | None=None, installation: Installation | None=None) -> int` — The entry point of a project's program: its launcher builds a `ShellConfig` and calls this.
38
+
39
+ ## `webapp_shell.config`
40
+
41
+ The one object a project gives the library.
42
+
43
+ - `class ShellConfig`
44
+ - `identity: AppIdentity`
45
+ - `backend: BackendSpec`
46
+ - `source_root: Path`
47
+ - `host: str`
48
+ - `startup_timeout: float`
49
+ - `splash_seconds: float`
50
+ - `native_functions: tuple[NativeCallable, ...]`
51
+ - `required_libraries: tuple[str, ...]`
52
+ - `library_checks: tuple[Callable[[], list[LibraryProblem]], ...]`
53
+ - `messages: ShellMessages`
54
+
55
+ ## `webapp_shell.contract.guard`
56
+
57
+ Access control for a Starlette or FastAPI backend: the launch token, from a header or a cookie.
58
+
59
+ - `class GuardResponses` — What a refused request gets back. A project with its own error format replaces them.
60
+ - `unauthorized: dict[str, str]`
61
+ - `foreign_origin: dict[str, str]`
62
+ - `class TokenGuard(BaseHTTPMiddleware)` — Protects the API with the launch token.
63
+ - `def __init__(self, app: ASGIApp, validator: TokenValidator, wire: WireProtocol, origins: OriginPolicy | None=None, responses: GuardResponses | None=None)`
64
+ - `async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response`
65
+ - `class WebSocketGuard` — The same rules for a socket as `TokenGuard` has for a request: the token (header, cookie or query string) and the origin. A refused socket is closed with `wire.ws_close_unauthorized` before it is accepted.
66
+ - `def __init__(self, validator: TokenValidator, wire: WireProtocol, origins: OriginPolicy | None=None)`
67
+ - `def allows(self, socket: WebSocket) -> bool`
68
+ - `async def accept(self, socket: WebSocket) -> bool`
69
+
70
+ ## `webapp_shell.contract.launch`
71
+
72
+ The command line the shell starts a backend with, read by the backend: the port, the compiled frontend, the preferences file and the token. The argument names come from the same `BackendSpec` the shell builds the command from, so the two cannot drift.
73
+
74
+ - `class LaunchArguments`
75
+ - `port: int`
76
+ - `token: str`
77
+ - `frontend: Path | None`
78
+ - `preferences_file: Path | None`
79
+ - `host: str`
80
+ - `def add_launch_arguments(parser: argparse.ArgumentParser, backend: BackendSpec) -> None` — Adds the standard arguments to a backend's parser. Read them back with `LaunchArguments.of(args, ...)`.
81
+ - `def read_launch_arguments(parser: argparse.ArgumentParser, args: argparse.Namespace, wire: WireProtocol) -> LaunchArguments` — Checks the token is there (the parser reports the error) and gathers the standard arguments.
82
+
83
+ ## `webapp_shell.contract.tokens`
84
+
85
+ Decides whether a candidate is the launch token, and whether a request comes from the app's own page. No HTTP framework code here.
86
+
87
+ - `class TokenValidator`
88
+ - `def __init__(self, token: str, wire: WireProtocol)`
89
+ - `property token(self) -> str`
90
+ - `def is_valid(self, candidate: str | None) -> bool`
91
+ - `def is_valid_connection(self, headers: Mapping[str, str], cookies: Mapping[str, str], query: Mapping[str, str]) -> bool`
92
+ - `class OriginPolicy` — A browser tells which page made a request in `Origin`. Cookies are shared by every port of a host, so another program's page on 127.0.0.1 would otherwise be allowed to call this one: only the app's own page is.
93
+ - `def allows(self, origin: str | None, host: str | None) -> bool`
94
+
95
+ ## `webapp_shell.enums`
96
+
97
+ - `class ShellKind(StrEnum)` — The desktop shells. The value is what the command line and the run scripts use.
98
+ - `QT`
99
+ - `WEBVIEW`
100
+ - `class NativeMethod(StrEnum)` — The native functions a page may call (02-architecture.md). The value is the name on the wire, in both shells.
101
+ - `PICK_FOLDER`
102
+ - `PICK_FILE`
103
+ - `SAVE_AS`
104
+ - `REVEAL`
105
+ - `OPEN_URL`
106
+ - `SET_BOOT_COLORS`
107
+ - `SET_ZOOM`
108
+
109
+ ## `webapp_shell.freezing.plan`
110
+
111
+ What a project says about its frozen build. The rest (the entry file, what each shell leaves out, the checks of the finished folder) belongs to the library.
112
+
113
+ - `class DataFolder` — A folder of the project that goes into the build as data (the compiled frontend, files the backend reads).
114
+ - `source: Path`
115
+ - `target: str`
116
+ - `class FreezePlan`
117
+ - `product: str`
118
+ - `config: str`
119
+ - `packages: tuple[str, ...]`
120
+ - `data_folders: tuple[DataFolder, ...]`
121
+ - `required: tuple[str, ...]`
122
+ - `collect_all: tuple[str, ...]`
123
+ - `runtime_hooks: tuple[Path, ...]`
124
+ - `excludes: tuple[str, ...]`
125
+ - `icon: Path | None`
126
+ - `def load(cls, reference: str) -> 'FreezePlan'`
127
+
128
+ ## `webapp_shell.identity`
129
+
130
+ Who the program is: the one place a project says its name, where it keeps things and how its opening looks.
131
+
132
+ - `class AppIdentity` — What the shell shows and stores for one program. Nothing else in the library names the program.
133
+ - `name: str`
134
+ - `slug: str`
135
+ - `storage_dir: Path`
136
+ - `boot_colors: BootColors`
137
+ - `icon_png: Path | None`
138
+ - `icon_ico: Path | None`
139
+ - `window_size: tuple[int, int]`
140
+ - `property native_global(self) -> str`
141
+ - `property native_ready_event(self) -> str`
142
+ - `property shell_preferences_path(self) -> Path`
143
+ - `property app_preferences_path(self) -> Path`
144
+
145
+ ## `webapp_shell.installation`
146
+
147
+ Where the program lives and how its backend is started: from the source tree, or frozen into one folder.
148
+
149
+ - `class Installation` — A frozen program (PyInstaller) has one executable and no `python -m`: it starts its own backend by running itself with `RUN_BACKEND`. From the source tree the backend is `python -m <module>`, as always.
150
+ - `RUN_BACKEND`
151
+ - `CHECK_LIBRARIES`
152
+ - `COMMANDS`
153
+ - `def __init__(self, source_root: Path, frozen: bool | None=None, bundle: Path | None=None)`
154
+ - `property root(self) -> Path`
155
+ - `def package_dirs(self, backend: BackendSpec) -> tuple[Path, ...]`
156
+ - `def backend_command(self, backend: BackendSpec) -> list[str]`
157
+
158
+ ## `webapp_shell.json_types`
159
+
160
+ What JSON can carry. Everything that crosses between the page and Python is one of these, never a bare `dict`.
161
+
162
+ - `type JsonValue = str | int | float | bool | list[JsonValue] | dict[str, JsonValue] | None`
163
+ - `type JsonObject = dict[str, JsonValue]`
164
+
165
+ ## `webapp_shell.library_check`
166
+
167
+ Imports every third-party library a project lists. Heavy libraries load lazily, so a frozen program that lacks one would only fail when the person reaches the feature; this finds it when the program is built.
168
+
169
+ - `class LibraryProblem`
170
+ - `name: str`
171
+ - `detail: str`
172
+
173
+ ## `webapp_shell.messages`
174
+
175
+ The few sentences the shell shows itself (in the splash page's error, and in what it prints), as one catalogue.
176
+
177
+ - `class MessageKey(StrEnum)` — The name of each sentence. The member name, lower-cased, is the field of `ShellMessages`; the value is the key in the JSON catalogue.
178
+ - `FRONTEND_MISSING`
179
+ - `BACKEND_NOT_STARTED`
180
+ - `BACKEND_EXITED`
181
+ - `VERSION_NOT_OBJECT`
182
+ - `BACKEND_GAVE_UP`
183
+ - `LIBRARIES_PRESENT`
184
+ - `WEBVIEW2_MISSING`
185
+ - `class ShellMessages` — Every sentence, with its English default. `{name}` and `{path}` are filled in when the sentence is shown.
186
+ - `frontend_missing: str`
187
+ - `backend_not_started: str`
188
+ - `backend_exited: str`
189
+ - `version_not_object: str`
190
+ - `backend_gave_up: str`
191
+ - `libraries_present: str`
192
+ - `webview2_missing: str`
193
+ - `def text(self, key: MessageKey, **values: str) -> str`
194
+ - `def sentences(self) -> dict[MessageKey, str]`
195
+ - `def catalogue(self) -> JsonObject`
196
+
197
+ ## `webapp_shell.native.errors`
198
+
199
+ The error a native function raises to refuse a call in words the page may show.
200
+
201
+ - `class NativeError(Exception)` — Raised by a handler to refuse a call in words the page may show.
202
+
203
+ ## `webapp_shell.native.filters`
204
+
205
+ File-type filters in the one shape the frontend sends, converted for each toolkit.
206
+
207
+ - `class FileFilter` — `{name: 'Excel', extensions: ['xlsx', 'xls']}` from the page. An extension may be written `xlsx` or `*.xlsx`.
208
+ - `name: str`
209
+ - `extensions: tuple[str, ...]`
210
+ - `property is_usable(self) -> bool`
211
+ - `def to_qt(self) -> str`
212
+ - `def to_webview(self) -> str`
213
+
214
+ ## `webapp_shell.native.registry`
215
+
216
+ The native functions a page may call: a name, typed arguments, a handler, and one place that runs them for both toolkits.
217
+
218
+ - `class NoArguments` — The arguments of a function that takes none.
219
+ - `class NativeCallable(Protocol)` — What the registry stores: any `NativeFunction`, whatever its arguments are.
220
+ - `property name(self) -> str`
221
+ - `def run(self, context: NativeContext, values: list[JsonValue]) -> JsonValue`
222
+ - `class NativeFunction` — A function the page can call by `name`. `arguments` is a dataclass (see `native.arguments`); the handler is called as `handler(context, arguments)` and returns anything JSON can carry.
223
+ - `def __init__(self, name: str, arguments: type[A], handler: Callable[[NativeContext, A], JsonValue])`
224
+ - `property name(self) -> str`
225
+ - `def run(self, context: NativeContext, values: list[JsonValue]) -> JsonValue`
226
+
227
+ ## `webapp_shell.native.standard`
228
+
229
+ The native functions every project gets. Written once, against `NativeContext`; the page sees them under the names of `NativeMethod`. Each declares its arguments as a dataclass.
230
+
231
+ - `def standard_functions() -> tuple[NativeCallable, ...]`
232
+
233
+ ## `webapp_shell.native.window`
234
+
235
+ What a native function can use: the window it opens dialogs on, the shell's preferences and the OS actions. Each toolkit implements `NativeWindow`; the functions are written once, against it.
236
+
237
+ - `class NativeWindow(ABC)` — The window of one toolkit. A dialog returns the chosen path, or None when the person cancelled.
238
+ - `def pick_folder(self, start: str) -> str | None`
239
+ - `def pick_file(self, filters: list[FileFilter], start: str) -> str | None`
240
+ - `def save_as(self, name: str, start: str) -> str | None`
241
+ - `def set_zoom(self, factor: float) -> bool`
242
+ - `class NativeContext` — Handed to every native function as its first argument.
243
+ - `window: NativeWindow`
244
+ - `preferences: ShellPreferences`
245
+ - `actions: OsActions`
246
+
247
+ ## `webapp_shell.preferences`
248
+
249
+ What the shell itself remembers between launches: the colours of the app's opening screen, so the window and the splash start in the colours of the theme the user chose (the shell cannot read the web page's storage before the page loads).
250
+
251
+ - `class BootColors` — The three colours of the navy gradient (frontend tokens `--nav`, `--hero-a`, `--hero-b`).
252
+ - `base: str`
253
+ - `glow_a: str`
254
+ - `glow_b: str`
255
+ - `def from_values(cls, base: JsonValue, glow_a: JsonValue, glow_b: JsonValue) -> 'BootColors | None'`
256
+ - `property is_valid(self) -> bool`
257
+ - `def normalised(self) -> 'BootColors'`
258
+
259
+ ## `webapp_shell.protocol`
260
+
261
+ The names the shell and a backend must agree on. The backend reads the same names; the library never invents them silently: `for_slug` gives a consistent default set and a project may write its own.
262
+
263
+ - `class WireProtocol`
264
+ - `token_header: str`
265
+ - `token_cookie: str`
266
+ - `token_query: str`
267
+ - `token_env: str`
268
+ - `api_prefix: str`
269
+ - `health_path: str`
270
+ - `version_path: str | None`
271
+ - `ws_close_unauthorized: int`
272
+ - `def for_slug(cls, slug: str) -> 'WireProtocol'`
273
+ - `def token_from_environment(self, environ: Mapping[str, str] | None=None) -> str | None`