webapp-shell 0.1__py3-none-any.whl

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 (59) hide show
  1. webapp_shell/__init__.py +50 -0
  2. webapp_shell/__main__.py +53 -0
  3. webapp_shell/backend.py +30 -0
  4. webapp_shell/backend_process.py +136 -0
  5. webapp_shell/bridge/webapp-shell-bridge.d.ts +48 -0
  6. webapp_shell/bridge/webapp-shell-bridge.js +146 -0
  7. webapp_shell/cli.py +76 -0
  8. webapp_shell/config.py +27 -0
  9. webapp_shell/contract/__init__.py +10 -0
  10. webapp_shell/contract/guard.py +106 -0
  11. webapp_shell/contract/launch.py +49 -0
  12. webapp_shell/contract/tokens.py +41 -0
  13. webapp_shell/enums.py +20 -0
  14. webapp_shell/freezing/__init__.py +10 -0
  15. webapp_shell/freezing/app.spec +5 -0
  16. webapp_shell/freezing/builder.py +82 -0
  17. webapp_shell/freezing/entry.py +24 -0
  18. webapp_shell/freezing/plan.py +38 -0
  19. webapp_shell/freezing/profile.py +49 -0
  20. webapp_shell/freezing/spec.py +68 -0
  21. webapp_shell/freezing/verify.py +43 -0
  22. webapp_shell/identity.py +48 -0
  23. webapp_shell/installation.py +41 -0
  24. webapp_shell/json_types.py +4 -0
  25. webapp_shell/library_check.py +28 -0
  26. webapp_shell/messages.py +58 -0
  27. webapp_shell/native/__init__.py +4 -0
  28. webapp_shell/native/arguments.py +131 -0
  29. webapp_shell/native/errors.py +5 -0
  30. webapp_shell/native/filters.py +24 -0
  31. webapp_shell/native/os_actions.py +33 -0
  32. webapp_shell/native/qt_bridge.py +62 -0
  33. webapp_shell/native/qt_window.py +34 -0
  34. webapp_shell/native/registry.py +91 -0
  35. webapp_shell/native/standard.py +89 -0
  36. webapp_shell/native/start_location.py +28 -0
  37. webapp_shell/native/webview_background.py +39 -0
  38. webapp_shell/native/webview_bridge.py +25 -0
  39. webapp_shell/native/webview_icon.py +32 -0
  40. webapp_shell/native/webview_window.py +54 -0
  41. webapp_shell/native/window.py +34 -0
  42. webapp_shell/native/zoom.py +104 -0
  43. webapp_shell/native/zoom_range.py +9 -0
  44. webapp_shell/preferences.py +84 -0
  45. webapp_shell/protocol.py +34 -0
  46. webapp_shell/py.typed +0 -0
  47. webapp_shell/qt_shell.py +126 -0
  48. webapp_shell/registry.py +20 -0
  49. webapp_shell/settings.py +78 -0
  50. webapp_shell/shell.py +119 -0
  51. webapp_shell/smoke.py +30 -0
  52. webapp_shell/splash.py +52 -0
  53. webapp_shell/watchdog.py +60 -0
  54. webapp_shell/webview2.py +80 -0
  55. webapp_shell/webview_shell.py +86 -0
  56. webapp_shell-0.1.dist-info/METADATA +132 -0
  57. webapp_shell-0.1.dist-info/RECORD +59 -0
  58. webapp_shell-0.1.dist-info/WHEEL +4 -0
  59. webapp_shell-0.1.dist-info/licenses/LICENSE +21 -0
webapp_shell/shell.py ADDED
@@ -0,0 +1,119 @@
1
+ """The shell contract. A concrete shell only knows how to show a window; everything else lives here."""
2
+
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Callable
6
+
7
+ from .backend_process import BackendError, BackendProcess
8
+ from .json_types import JsonObject
9
+ from .native.registry import NativeRegistry
10
+ from .native.standard import standard_functions
11
+ from .preferences import ShellPreferences
12
+ from .settings import Settings
13
+ from .smoke import SmokeReport
14
+ from .splash import SplashPage
15
+ from .watchdog import BackendWatchdog
16
+
17
+
18
+ class Shell(ABC):
19
+ """Template: show a splash at once, start the backend off the UI thread, then show the frontend.
20
+
21
+ `boot()` runs on a worker thread, so every method it calls on a subclass must be safe to call from there.
22
+ """
23
+
24
+ name: str = "shell"
25
+
26
+ def __init__(
27
+ self,
28
+ settings: Settings,
29
+ backend: BackendProcess,
30
+ preferences: ShellPreferences | None = None,
31
+ smoke: bool = False,
32
+ ):
33
+ self.settings = settings
34
+ self.preferences = preferences or ShellPreferences(None)
35
+ self.backend = backend
36
+ self.smoke = smoke
37
+ self.backend_version: JsonObject = {}
38
+ self.failed = False
39
+ self.splash = SplashPage(settings.identity.name, settings.icon_png, settings.boot_colors)
40
+ self.watchdog = BackendWatchdog(backend, self.report_error, settings.identity.name, settings.config.messages)
41
+ self.native = NativeRegistry((*standard_functions(), *settings.config.native_functions))
42
+
43
+ @property
44
+ def window_title(self) -> str:
45
+ """The program's name. Running from the source it also names the shell and the backend version, for the team."""
46
+
47
+ name = self.settings.identity.name
48
+ if self.settings.installation.frozen:
49
+ return name
50
+ return f"{name} ({self.name}) · backend {self.backend_version.get('version', '?')}"
51
+
52
+ def run(self) -> int:
53
+ try:
54
+ self.event_loop(self.boot)
55
+ finally:
56
+ self.watchdog.stop()
57
+ self.backend.stop()
58
+ return 1 if self.failed else 0
59
+
60
+ def boot(self) -> None:
61
+ started = time.monotonic()
62
+ try:
63
+ self.backend.start()
64
+ self.backend_version = self.backend.version()
65
+ except BackendError as error:
66
+ self.report_error(str(error))
67
+ return
68
+ self.hold_splash(started)
69
+ self.set_title(self.window_title)
70
+ self.navigate(self.backend.launch_url)
71
+ self.on_navigated()
72
+ if not self.smoke:
73
+ self.watchdog.start()
74
+
75
+ def hold_splash(
76
+ self,
77
+ started: float,
78
+ now: Callable[[], float] = time.monotonic,
79
+ sleep: Callable[[float], None] = time.sleep,
80
+ ) -> None:
81
+ """Keep the splash up until `splash_seconds` have passed since `started` (the backend may be ready sooner)."""
82
+ remaining = self.settings.splash_seconds - (now() - started)
83
+ if remaining > 0:
84
+ sleep(remaining)
85
+
86
+ def report_error(self, message: str) -> None:
87
+ self.failed = True
88
+ print(message)
89
+ self.show_page(self.splash.error(message))
90
+ if self.smoke:
91
+ self.quit()
92
+
93
+ def finish_smoke(self, page: str) -> None:
94
+ print(SmokeReport(self.name, self.backend_version, page))
95
+
96
+ # --- what a concrete shell provides ---------------------------------------------------------
97
+
98
+ @abstractmethod
99
+ def event_loop(self, boot: Callable[[], None]) -> None:
100
+ """Show the window with the loading splash, call `boot()` on a worker thread, block until closed."""
101
+
102
+ @abstractmethod
103
+ def navigate(self, url: str) -> None:
104
+ """Load `url` in the window. Thread-safe."""
105
+
106
+ @abstractmethod
107
+ def show_page(self, html: str) -> None:
108
+ """Replace the window content with an HTML page. Thread-safe."""
109
+
110
+ @abstractmethod
111
+ def set_title(self, title: str) -> None:
112
+ """Thread-safe."""
113
+
114
+ @abstractmethod
115
+ def quit(self) -> None:
116
+ """Close the window. Thread-safe."""
117
+
118
+ def on_navigated(self) -> None: # noqa: B027 - an optional hook, empty on purpose
119
+ """Hook run on the worker thread after `navigate`; shells use it for the headless self-check."""
webapp_shell/smoke.py ADDED
@@ -0,0 +1,30 @@
1
+ """The headless self-check both shells share: what to ask the loaded page and how to report it."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .json_types import JsonObject
6
+
7
+
8
+ def page_probe_js(native_global: str) -> str:
9
+ """What to ask the loaded page: its title, whether the app rendered, and which native bridge it can see."""
10
+ return (
11
+ "JSON.stringify({"
12
+ "title: document.title,"
13
+ "rootChildren: document.getElementById('root') ? document.getElementById('root').children.length : -1,"
14
+ "hash: location.hash,"
15
+ f"native: window.{native_global} ? '{native_global}' : "
16
+ "(window.pywebview && window.pywebview.api ? 'pywebview' : 'none')})"
17
+ )
18
+
19
+
20
+ SETTLE_SECONDS = 2.5
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SmokeReport:
25
+ shell: str
26
+ backend: JsonObject
27
+ page: str
28
+
29
+ def __str__(self) -> str:
30
+ return f"{self.shell}: backend={self.backend} page={self.page}"
webapp_shell/splash.py ADDED
@@ -0,0 +1,52 @@
1
+ """The two pages the shell shows itself, before and instead of the frontend. Deliberately text-light: the real
2
+ user interface (and its translations) belongs to the frontend."""
3
+
4
+ import base64
5
+ from html import escape
6
+ from pathlib import Path
7
+
8
+ from .preferences import BootColors
9
+
10
+ # A soft two-glow gradient over the base colour; a project's frontend can use the same geometry for its hero.
11
+ HERO_GRADIENT = (
12
+ "radial-gradient(900px 600px at 20% 10%, {glow_a} 0, transparent 60%), "
13
+ "radial-gradient(700px 500px at 90% 90%, {glow_b} 0, transparent 55%), {base}"
14
+ )
15
+
16
+ _PAGE = """<!doctype html><meta charset="utf-8"><title>%(title)s</title>
17
+ <style>
18
+ html,body{height:100%%;margin:0}
19
+ body{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;
20
+ font:15px -apple-system,'Segoe UI',system-ui,sans-serif;background:%(background)s;color:#fff}
21
+ .mark{width:64px;height:64px;image-rendering:auto}
22
+ .spin{width:26px;height:26px;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%%;animation:s 0.9s linear infinite}
23
+ @keyframes s{to{transform:rotate(360deg)}}
24
+ .err{max-width:520px;text-align:center;color:#ffb4ab}
25
+ </style>
26
+ %(icon)s%(body)s"""
27
+
28
+
29
+ class SplashPage:
30
+ def __init__(self, title: str, icon_png: Path | None, colors: BootColors):
31
+ self._title = escape(title)
32
+ self._icon_png = icon_png
33
+ self._colors = colors
34
+
35
+ def loading(self) -> str:
36
+ return self._render('<div class="spin"></div>')
37
+
38
+ def error(self, message: str) -> str:
39
+ return self._render(f'<div class="err">{escape(message)}</div>')
40
+
41
+ def _render(self, body: str) -> str:
42
+ return _PAGE % {"title": self._title, "background": self._background(), "icon": self._icon_html(), "body": body}
43
+
44
+ def _background(self) -> str:
45
+ c = self._colors
46
+ return HERO_GRADIENT.format(base=c.base, glow_a=c.glow_a, glow_b=c.glow_b)
47
+
48
+ def _icon_html(self) -> str:
49
+ if not self._icon_png:
50
+ return ""
51
+ data = base64.b64encode(self._icon_png.read_bytes()).decode()
52
+ return f'<img class="mark" alt="" src="data:image/png;base64,{data}">'
@@ -0,0 +1,60 @@
1
+ """Notices when the engine has stopped by itself and starts it again, a few times, before giving up."""
2
+
3
+ import threading
4
+ import time
5
+ from collections import deque
6
+ from collections.abc import Callable
7
+
8
+ from .backend_process import BackendError, BackendProcess
9
+ from .messages import ENGLISH, MessageKey, ShellMessages
10
+
11
+
12
+ class BackendWatchdog:
13
+ def __init__(
14
+ self,
15
+ backend: BackendProcess,
16
+ give_up: Callable[[str], None],
17
+ name: str = "the program",
18
+ messages: ShellMessages = ENGLISH,
19
+ interval: float = 1.0,
20
+ max_restarts: int = 3,
21
+ window: float = 120.0,
22
+ clock: Callable[[], float] = time.monotonic,
23
+ ):
24
+ self.backend, self.give_up = backend, give_up
25
+ self._give_up_message = messages.text(MessageKey.BACKEND_GAVE_UP, name=name)
26
+ self.interval, self.max_restarts, self.window = interval, max_restarts, window
27
+ self._clock = clock
28
+ self._restarts: deque[float] = deque()
29
+ self._stopped = threading.Event()
30
+ self._thread: threading.Thread | None = None
31
+
32
+ def start(self) -> None:
33
+ self._thread = threading.Thread(target=self._loop, name="backend-watchdog", daemon=True)
34
+ self._thread.start()
35
+
36
+ def stop(self) -> None:
37
+ self._stopped.set()
38
+
39
+ def check(self) -> None:
40
+ """One look. A dead engine is restarted unless that already happened `max_restarts` times within `window`."""
41
+ if not self.backend.died:
42
+ return
43
+ now = self._clock()
44
+ while self._restarts and now - self._restarts[0] > self.window:
45
+ self._restarts.popleft()
46
+ if len(self._restarts) >= self.max_restarts:
47
+ return self._give_up(self._give_up_message)
48
+ self._restarts.append(now)
49
+ try:
50
+ self.backend.restart()
51
+ except BackendError as error:
52
+ self._give_up(str(error))
53
+
54
+ def _give_up(self, message: str) -> None:
55
+ self.stop()
56
+ self.give_up(message)
57
+
58
+ def _loop(self) -> None:
59
+ while not self._stopped.wait(self.interval):
60
+ self.check()
@@ -0,0 +1,80 @@
1
+ """On Windows the pywebview window is drawn by the Microsoft WebView2 runtime (on Windows 11 and on most up-to-date
2
+ Windows 10). Without it the window cannot open, and the person would see nothing at all; this finds it first, so the
3
+ program can say what is missing in plain words."""
4
+
5
+ import sys
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+
10
+ from .messages import MessageKey, ShellMessages
11
+
12
+ DOWNLOAD_URL = "https://developer.microsoft.com/microsoft-edge/webview2/"
13
+ RUNTIME_CLIENT = "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" # how the Edge updater lists the WebView2 runtime
14
+ NOT_INSTALLED = "0.0.0.0"
15
+
16
+
17
+ class RegistryHive(StrEnum):
18
+ MACHINE = "HKEY_LOCAL_MACHINE"
19
+ USER = "HKEY_CURRENT_USER"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class RegistryLocation:
24
+ hive: RegistryHive
25
+ subkey: str # holds the value `pv`: the runtime's version
26
+
27
+
28
+ RUNTIME_LOCATIONS = (
29
+ RegistryLocation(RegistryHive.MACHINE, rf"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{RUNTIME_CLIENT}"),
30
+ RegistryLocation(RegistryHive.MACHINE, rf"SOFTWARE\Microsoft\EdgeUpdate\Clients\{RUNTIME_CLIENT}"),
31
+ RegistryLocation(RegistryHive.USER, rf"Software\Microsoft\EdgeUpdate\Clients\{RUNTIME_CLIENT}"),
32
+ )
33
+
34
+
35
+ def read_registry_version(location: RegistryLocation) -> str | None:
36
+ """The runtime's version at `location`, or None when the key or the value is not there."""
37
+ if sys.platform != "win32":
38
+ return None
39
+ import winreg
40
+
41
+ hive = winreg.HKEY_LOCAL_MACHINE if location.hive is RegistryHive.MACHINE else winreg.HKEY_CURRENT_USER
42
+ try:
43
+ with winreg.OpenKey(hive, location.subkey) as key:
44
+ value, _kind = winreg.QueryValueEx(key, "pv")
45
+ except OSError:
46
+ return None
47
+ return value if isinstance(value, str) else None
48
+
49
+
50
+ class Webview2Check:
51
+ def __init__(
52
+ self,
53
+ platform: str = sys.platform,
54
+ read_version: Callable[[RegistryLocation], str | None] = read_registry_version,
55
+ ):
56
+ self._platform = platform
57
+ self._read_version = read_version
58
+
59
+ def is_missing(self) -> bool:
60
+ """True only on Windows, when no WebView2 runtime is listed."""
61
+ if self._platform != "win32":
62
+ return False
63
+ versions = (self._read_version(location) for location in RUNTIME_LOCATIONS)
64
+ return not any(version and version != NOT_INSTALLED for version in versions)
65
+
66
+ def problem(self, name: str, messages: ShellMessages) -> str | None:
67
+ """The sentence to show when the runtime is missing, else None."""
68
+ if not self.is_missing():
69
+ return None
70
+ return messages.text(MessageKey.WEBVIEW2_MISSING, name=name, url=DOWNLOAD_URL)
71
+
72
+
73
+ def show_message_box(title: str, text: str) -> None:
74
+ """A windowed program has no console, so a problem before any window exists is shown in the system's own box."""
75
+ if sys.platform == "win32":
76
+ import ctypes
77
+
78
+ ctypes.windll.user32.MessageBoxW(0, text, title, 0x10) # 0x10: the error icon
79
+ else:
80
+ print(text, file=sys.stderr)
@@ -0,0 +1,86 @@
1
+ """Shell 2: pywebview (no Qt): the operating system's own web view."""
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+
6
+ import webview
7
+
8
+ from .backend_process import BackendProcess
9
+ from .enums import ShellKind
10
+ from .native.os_actions import OsActions
11
+ from .native.webview_background import WebviewBackground
12
+ from .native.webview_bridge import WebviewBridge
13
+ from .native.webview_icon import WebviewIcon
14
+ from .native.webview_window import WebviewWindow
15
+ from .native.window import NativeContext
16
+ from .preferences import ShellPreferences
17
+ from .settings import Settings
18
+ from .shell import Shell
19
+ from .smoke import SETTLE_SECONDS, page_probe_js
20
+
21
+
22
+ class WebviewShell(Shell):
23
+ name = ShellKind.WEBVIEW
24
+
25
+ def __init__(
26
+ self,
27
+ settings: Settings,
28
+ backend: BackendProcess,
29
+ preferences: ShellPreferences | None = None,
30
+ smoke: bool = False,
31
+ ):
32
+ super().__init__(settings, backend, preferences, smoke)
33
+ self._window: webview.Window | None = None
34
+
35
+ @property
36
+ def window(self) -> webview.Window:
37
+ if self._window is None:
38
+ raise RuntimeError("the window is not created yet")
39
+ return self._window
40
+
41
+ def event_loop(self, boot: Callable[[], None]) -> None:
42
+ native_window = WebviewWindow()
43
+ bridge = WebviewBridge(self.native, NativeContext(native_window, self.preferences, OsActions()))
44
+ window = webview.create_window( # pyright: ignore[reportUnknownMemberType] # pywebview's `url` is left open
45
+ self.settings.identity.name,
46
+ html=self.splash.loading(),
47
+ js_api=bridge,
48
+ background_color=self.settings.background_color,
49
+ width=self.settings.window_size[0],
50
+ height=self.settings.window_size[1],
51
+ )
52
+ if window is None:
53
+ raise RuntimeError("pywebview did not create the window")
54
+ self._window = window
55
+ native_window.bind(window)
56
+ icon = str(self.settings.window_icon) if self.settings.window_icon else None
57
+ background = WebviewBackground(self.settings.background_color)
58
+ dock_icon = WebviewIcon(self.settings.icon_png)
59
+
60
+ def start() -> None:
61
+ background.apply(window)
62
+ dock_icon.apply()
63
+ boot()
64
+
65
+ webview.start(
66
+ start, icon=icon
67
+ ) # `boot` runs on a worker thread once the GUI is up; the icon is a .ico on Windows, a PNG elsewhere
68
+
69
+ def navigate(self, url: str) -> None:
70
+ self.window.load_url(url)
71
+
72
+ def show_page(self, html: str) -> None:
73
+ self.window.load_html(html)
74
+
75
+ def set_title(self, title: str) -> None:
76
+ self.window.set_title(title)
77
+
78
+ def quit(self) -> None:
79
+ self.window.destroy()
80
+
81
+ def on_navigated(self) -> None:
82
+ if not self.smoke:
83
+ return
84
+ time.sleep(SETTLE_SECONDS)
85
+ self.finish_smoke(self.window.evaluate_js(page_probe_js(self.settings.identity.native_global)))
86
+ self.quit()
@@ -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,59 @@
1
+ webapp_shell/__init__.py,sha256=oix7R1hN5tTcIBf4yQusfma4dVQj3KGQoyjzDzh52gM,1519
2
+ webapp_shell/__main__.py,sha256=bj_37hL3oNMtSCalRVjFDGmUn7g6aYvflpfy4E3NJf0,2162
3
+ webapp_shell/backend.py,sha256=Ge_BJwPL-Em3yamR9CtlDxLu9N0L6HiW6oQ22-Yq418,1391
4
+ webapp_shell/backend_process.py,sha256=BUFG1ULeeu8n2EbGQqypioIgrRAl7B0EEgAb3BMSX_0,5418
5
+ webapp_shell/cli.py,sha256=XorAL_WK-aDjxSF1sU1ch-eQY-TZ30getR0p0gan1_8,3414
6
+ webapp_shell/config.py,sha256=xwttvGnV3p-66TOCuSa4Kx_CJRzkzm1EgJQY-H8KMZM,1300
7
+ webapp_shell/enums.py,sha256=3ITML33MUF24bU_UdVO0pu1X6sdOTIK3BlvxGaLZvKI,525
8
+ webapp_shell/identity.py,sha256=8QbetY8VtpqUniScVzdWp88C1u6UcMfzv_1o10175t8,2077
9
+ webapp_shell/installation.py,sha256=DG3PGtkwcEr-BYFXYB_2vZgRNcRVvKdG7WPHC0dyYN8,2077
10
+ webapp_shell/json_types.py,sha256=z0hS169QEBLsERucHjF6OMj3BU_bcKUl_itmqOaj2QI,247
11
+ webapp_shell/library_check.py,sha256=Qewe0T596bAdkPe1CXqGxmOvBuobujOrWtTh6hI-wtU,868
12
+ webapp_shell/messages.py,sha256=dJNgziyTm5zxlG8Jmh_sR23_9Kn_NwBQR2Zn8IYUgjI,2601
13
+ webapp_shell/preferences.py,sha256=QiSApswZfnZKeRarqk1HcG7o_Dl4suDWb0vvKABFxyU,3070
14
+ webapp_shell/protocol.py,sha256=en_z8aZ8EAQ1sECSV5HceSya9xLVyVysxCGzZmYNrK8,1602
15
+ webapp_shell/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ webapp_shell/qt_shell.py,sha256=FaWYSSL9Rq3xi1iSKlz9Vrh7LxyjseqEvfjKpbV0rzo,5072
17
+ webapp_shell/registry.py,sha256=cfdZ-qOWrjqykLvB-aA5S-pWmm_xbiVtP5jwyj-EAms,511
18
+ webapp_shell/settings.py,sha256=DDLwM2y6krMZSzbuP-q-ssVTccLTpyYBQDbTZ-7Z3is,2683
19
+ webapp_shell/shell.py,sha256=f2Bjg9mdbM2ymuipopdUzEfNrFul0DuOAwhMDcDREDA,4256
20
+ webapp_shell/smoke.py,sha256=U_lnQxevLHuCLTYfJnAQsc1gkrrPA06vtXfUFj9uccs,919
21
+ webapp_shell/splash.py,sha256=yO63XD7BU-JUTM7pwv1RKAZfYS-oWzTpqyrMYxsafr0,2153
22
+ webapp_shell/watchdog.py,sha256=khXvB_xPy8fPcjcjxRBFLbM9AGu-DQ8Xnj3Gwy9gjqA,2098
23
+ webapp_shell/webview2.py,sha256=f-DGvR-SYzLHZkomJeWBDTznp-eboGzbZe5yfKEQ_Zw,3053
24
+ webapp_shell/webview_shell.py,sha256=i0IxfTgeg0WirqSuCKSf8BDKVg4znempAnPy9NUjELQ,2972
25
+ webapp_shell/bridge/webapp-shell-bridge.d.ts,sha256=6M62jAe74dnvrsS0OyoOOosDpRFQ5MJJGbCarFMQ43I,1860
26
+ webapp_shell/bridge/webapp-shell-bridge.js,sha256=ZJpYs82tiS7aqhS_OXWWlZf5TtYup-ueHYE1S0tf42s,4890
27
+ webapp_shell/contract/__init__.py,sha256=JvlMHds4WVEGzE1rKfNCfCBCpmqT4Z1Llme835pL0mk,519
28
+ webapp_shell/contract/guard.py,sha256=2_4Er6OyBvMPAXhvY48RQEIl0QwqvTQvs1H_cwyd1OY,4405
29
+ webapp_shell/contract/launch.py,sha256=SQtLfuH1CV9wpa20ARnlazL1tSeXmwuXZLsXTlAEmEU,1920
30
+ webapp_shell/contract/tokens.py,sha256=YK3RxGp4kiYBpzJpWVU-bsArtiP7RLzPFy6dz6YHtv8,1699
31
+ webapp_shell/freezing/__init__.py,sha256=BmWyjr4GZuPzpZlHSxkBgkbi9xNoJF2a3Poqo7Rx0Tg,383
32
+ webapp_shell/freezing/app.spec,sha256=ji5UEaazfgHSkbDhgQDBtoPat35zLpZInHieGp_qHOM,206
33
+ webapp_shell/freezing/builder.py,sha256=11oaGWkxvHrm1uX5OVGkV24vC3LUWfiqXR2IJY8K4jU,4175
34
+ webapp_shell/freezing/entry.py,sha256=ouHpSrYIIuq6Lpcjfo3EkOV5AF46gM6akPOP6PObJHE,869
35
+ webapp_shell/freezing/plan.py,sha256=I_RzWJuU4n0Oe7lxxjGv8Cn1A0mvVQ7g8jIs27JOCHI,1800
36
+ webapp_shell/freezing/profile.py,sha256=o80oY06P5fXZNM2yQPIOUQYHtyt3-7Ge_KpFkU1bA_o,1875
37
+ webapp_shell/freezing/spec.py,sha256=TUHILhcFOa0i-lo6Bfs-9Y3isCocifULeQm0SZkjyJc,2299
38
+ webapp_shell/freezing/verify.py,sha256=LeDUncKtqOC0BJTZy-Lm4-cXmX4V5bRluFXVi3aqo8M,1729
39
+ webapp_shell/native/__init__.py,sha256=64okxF2yLE1XQrYhEl0JCaGwTK-L2oLPywavQH8GP54,206
40
+ webapp_shell/native/arguments.py,sha256=HXew_DJJBC8C_Q_sIDaRuQ5BH7PTgUwNGKLeteyGY5o,5735
41
+ webapp_shell/native/errors.py,sha256=bG-p4iwMYbZstJTBd0C9HJF8r2Br2ZgZDf8Lrp_Mp3c,193
42
+ webapp_shell/native/filters.py,sha256=ybLhGmiFy7-V-gYPWPXs047KNmUVU2u92ndY4ARluzY,744
43
+ webapp_shell/native/os_actions.py,sha256=ezphiwMGEmWb8CEyZ3QTqmgwlET5_SOgd9PXHuPXDYk,1171
44
+ webapp_shell/native/qt_bridge.py,sha256=BtcIa2_hXenE8NuHhfw8orpEy8uQA_9L5UyCyr3mRPE,2427
45
+ webapp_shell/native/qt_window.py,sha256=PRK5K3vTFrVe2V64Ytd8_3YnihC6apxqTxHbCcZYx6g,1427
46
+ webapp_shell/native/registry.py,sha256=hn2J0SWAupxHjA2ufKmKoQxxbjKuvZWVEtnD6TMS130,3467
47
+ webapp_shell/native/standard.py,sha256=NykWqjK5spwQ5z9qgrdp9Y5FhA_sLGPTz2gLCe-MBjQ,2724
48
+ webapp_shell/native/start_location.py,sha256=5PNbAGLcWw3bCfGsC9vyxUxN9vtLpcBZjUAu8zaWS0s,1095
49
+ webapp_shell/native/webview_background.py,sha256=OMuq8L54ku2l18Q04eqckMb28c3YQak1Yl7I5T6azuk,1744
50
+ webapp_shell/native/webview_bridge.py,sha256=KZ10cl7oHnooBASPVBWNUuPTaLKg0tkc_6oXvc0JaGs,1098
51
+ webapp_shell/native/webview_icon.py,sha256=lpU7RcMwWoRCIHRrEbPz3MQeoAG5FX93haFXNSFlxqg,1428
52
+ webapp_shell/native/webview_window.py,sha256=OydIhmuyPxM3pPmQWi2FzsuqDBMZH_7mjG9_avDsT6s,1977
53
+ webapp_shell/native/window.py,sha256=CQB1KzZvBsEuhYyojJyvcWi7z9jVOMKugpCaQLMoHuc,1045
54
+ webapp_shell/native/zoom.py,sha256=kbIhLQ2xNljrZ-Zv3spghI6Q9-uTmt9SDgMqZOVHyGU,3667
55
+ webapp_shell/native/zoom_range.py,sha256=cR5nvZQa7E0WvXDagPZAsBSM_U4Zg5xqHdxYXOgEIXg,330
56
+ webapp_shell-0.1.dist-info/METADATA,sha256=1Gnx7KWsmEWEusY57rQBLN64LIB4cyb8ki9TAaqeoYA,6046
57
+ webapp_shell-0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
58
+ webapp_shell-0.1.dist-info/licenses/LICENSE,sha256=JKHQ_y0Zji_sAXiKzQcHaU0g0PnYKifYY87trR5Si5w,1082
59
+ webapp_shell-0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.