wesktop 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wesktop-0.4.0/.rlsbl/changes/.validated +1 -0
- wesktop-0.4.0/.rlsbl/changes/0.4.0.jsonl +5 -0
- wesktop-0.4.0/.rlsbl/changes/0.4.0.md +6 -0
- wesktop-0.4.0/.rlsbl/releases/v0.4.0.toml +3 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.strictcli/schema.json +1 -1
- {wesktop-0.3.2 → wesktop-0.4.0}/CHANGELOG.md +7 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/PKG-INFO +1 -1
- {wesktop-0.3.2 → wesktop-0.4.0}/pyproject.toml +1 -1
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/__init__.py +29 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/desktop.py +2 -0
- wesktop-0.4.0/src/wesktop/dev.py +95 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_desktop.py +82 -0
- wesktop-0.4.0/tests/test_dev.py +279 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase4.py +7 -2
- {wesktop-0.3.2 → wesktop-0.4.0}/uv.lock +1 -1
- wesktop-0.3.2/.rlsbl/changes/.validated +0 -1
- {wesktop-0.3.2 → wesktop-0.4.0}/.claude/settings.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.github/workflows/ci.yml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.github/workflows/publish.yml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.gitignore +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.claude/settings.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.gitignore +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/CHANGELOG.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/CLAUDE.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/bases/LICENSE +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.1.0.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.1.0.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.1.1.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.1.1.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.2.0.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.2.0.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.2.1.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.2.1.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.0.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.0.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.1.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.1.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.2.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/0.3.2.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/changes/unreleased.jsonl +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/config.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/hashes.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/hooks/post-release.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/hooks/pre-checks.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/hooks/pre-release.sh +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/lint/go.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/lint/npm.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/lint/python.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/releases/unreleased.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/releases/v0.3.0.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/releases/v0.3.1.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/releases/v0.3.2.toml +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.rlsbl/version +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/.selfdoc/hashes/hashes.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/CLAUDE.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/LICENSE +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/README.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/bin/cli.js +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/docs/api.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/docs/index.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/package.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/selfdoc.json +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/__main__.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/asgi.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/audit.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/auth.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/cli.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/config.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/di.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/entries.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/error_log.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/features.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/logging.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/__init__.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/ask_user.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/deployment.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/filesystem.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/git.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/review.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/mcp_tools/testing.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/middleware.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/sdui.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/server.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/sse.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/tasks.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/src/wesktop/testing.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/__init__.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/conftest.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_asgi.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_cli.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_entries.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_followup.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_import.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase1.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase2.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase3.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase5.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase6.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase7.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_phase8.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_server.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/tests/test_sse.py +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/todo/.done/claudetimeline-migration-decisions.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/todo/.done/codehome-ct-migration-plan.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/todo/platform-vision.md +0 -0
- {wesktop-0.3.2 → wesktop-0.4.0}/todo/route-metadata-api.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cafbbdcedbfae32677c2f02fd5bddfdd082cc628
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"commits":["be870a9f1bf8e89d54b819e5bc08191a0e9bc6f5"],"user_facing":true,"description":"**New feature.** `run()` accepts a `js_api` parameter, passed through to pywebview's `create_window()` for exposing Python methods to JavaScript.","type":"feature"}
|
|
2
|
+
{"commits":["6b2f343b0a419b86a68bbe1774243b803ffa7ffc"],"user_facing":false}
|
|
3
|
+
{"commits":["81c19888eb4ab2945245f841802b0a537fd3f90d"],"user_facing":false}
|
|
4
|
+
{"commits":["a9108962c657aec782b52a67fc6a45cd2dfb346f"],"user_facing":true,"description":"**New feature.** `dev()` starts a Vite dev server alongside the wesktop server in a single command, with ViteDevProxy for unified port access and automatic Vite lifecycle management.","type":"feature"}
|
|
5
|
+
{"commits":["e16c8adee976582c1a68d94c16089598eb13ffaf"],"user_facing":false}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## 0.4.0
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **New feature.** `run()` accepts a `js_api` parameter, passed through to pywebview's `create_window()` for exposing Python methods to JavaScript.
|
|
6
|
+
- **New feature.** `dev()` starts a Vite dev server alongside the wesktop server in a single command, with ViteDevProxy for unified port access and automatic Vite lifecycle management.
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **New feature.** `run()` accepts a `js_api` parameter, passed through to pywebview's `create_window()` for exposing Python methods to JavaScript.
|
|
10
|
+
- **New feature.** `dev()` starts a Vite dev server alongside the wesktop server in a single command, with ViteDevProxy for unified port access and automatic Vite lifecycle management.
|
|
11
|
+
|
|
5
12
|
## 0.3.2
|
|
6
13
|
|
|
7
14
|
### Features
|
|
@@ -192,6 +192,7 @@ __all__ = [
|
|
|
192
192
|
"status",
|
|
193
193
|
"ServerStatus",
|
|
194
194
|
"run",
|
|
195
|
+
"dev",
|
|
195
196
|
# features
|
|
196
197
|
"FeatureFlags",
|
|
197
198
|
# audit
|
|
@@ -275,6 +276,7 @@ def run(
|
|
|
275
276
|
name: str = "WESKTOP",
|
|
276
277
|
pre_serve: Callable[[], None] | None = None,
|
|
277
278
|
reload: bool = False,
|
|
279
|
+
js_api: object | None = None,
|
|
278
280
|
) -> None:
|
|
279
281
|
"""Start server + native desktop window."""
|
|
280
282
|
from wesktop.desktop import run as _run
|
|
@@ -291,6 +293,7 @@ def run(
|
|
|
291
293
|
name=name,
|
|
292
294
|
pre_serve=pre_serve,
|
|
293
295
|
reload=reload,
|
|
296
|
+
js_api=js_api,
|
|
294
297
|
)
|
|
295
298
|
|
|
296
299
|
|
|
@@ -332,3 +335,29 @@ def status(pid_path: Path, health_url: str | None = None) -> ServerStatus:
|
|
|
332
335
|
from wesktop.server import status as _status
|
|
333
336
|
|
|
334
337
|
return _status(pid_path, health_url=health_url)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def dev(
|
|
341
|
+
target: str | Callable,
|
|
342
|
+
*,
|
|
343
|
+
vite_command: str = "npm run dev",
|
|
344
|
+
vite_port: int = 5173,
|
|
345
|
+
host: str | None = None,
|
|
346
|
+
port: int | None = None,
|
|
347
|
+
pid_path: Path | None = None,
|
|
348
|
+
name: str = "WESKTOP",
|
|
349
|
+
pre_serve: Callable[[], None] | None = None,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Development mode: Vite + server. See :func:`wesktop.dev.dev`."""
|
|
352
|
+
from wesktop.dev import dev as _dev
|
|
353
|
+
|
|
354
|
+
_dev(
|
|
355
|
+
target,
|
|
356
|
+
vite_command=vite_command,
|
|
357
|
+
vite_port=vite_port,
|
|
358
|
+
host=host,
|
|
359
|
+
port=port,
|
|
360
|
+
pid_path=pid_path,
|
|
361
|
+
name=name,
|
|
362
|
+
pre_serve=pre_serve,
|
|
363
|
+
)
|
|
@@ -19,6 +19,7 @@ def run(
|
|
|
19
19
|
name: str = "WESKTOP",
|
|
20
20
|
pre_serve: Callable[[], None] | None = None,
|
|
21
21
|
reload: bool = False,
|
|
22
|
+
js_api: object | None = None,
|
|
22
23
|
) -> None:
|
|
23
24
|
"""Start server + open native desktop window. Blocks until window closes."""
|
|
24
25
|
from wesktop.server import serve
|
|
@@ -42,6 +43,7 @@ def run(
|
|
|
42
43
|
url=url,
|
|
43
44
|
width=width,
|
|
44
45
|
height=height,
|
|
46
|
+
js_api=js_api,
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
webview.start(icon=icon)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Development mode: Vite + wesktop server in a single command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def dev(
|
|
17
|
+
target: str | Callable,
|
|
18
|
+
*,
|
|
19
|
+
vite_command: str = "npm run dev",
|
|
20
|
+
vite_port: int = 5173,
|
|
21
|
+
host: str | None = None,
|
|
22
|
+
port: int | None = None,
|
|
23
|
+
pid_path: Path | None = None,
|
|
24
|
+
name: str = "WESKTOP",
|
|
25
|
+
pre_serve: Callable[[], None] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Start Vite dev server + wesktop ASGI server for development.
|
|
28
|
+
|
|
29
|
+
Spawns Vite as a subprocess, waits for it to be ready, then starts
|
|
30
|
+
the wesktop server with ViteDevProxy middleware. All frontend
|
|
31
|
+
requests are proxied to Vite (with HMR), API requests are handled
|
|
32
|
+
by the wesktop router. Kills Vite on shutdown.
|
|
33
|
+
"""
|
|
34
|
+
from wesktop.middleware import ViteDevProxy
|
|
35
|
+
from wesktop.server import serve
|
|
36
|
+
|
|
37
|
+
# Resolve the target to a callable ASGI app
|
|
38
|
+
if isinstance(target, str):
|
|
39
|
+
module_path, attr = target.rsplit(":", 1)
|
|
40
|
+
module = importlib.import_module(module_path)
|
|
41
|
+
app = getattr(module, attr)
|
|
42
|
+
else:
|
|
43
|
+
app = target
|
|
44
|
+
|
|
45
|
+
# Wrap with ViteDevProxy
|
|
46
|
+
wrapped = ViteDevProxy(app, vite_port=vite_port)
|
|
47
|
+
|
|
48
|
+
# Spawn Vite
|
|
49
|
+
logger.info("Starting Vite dev server: %s", vite_command)
|
|
50
|
+
vite_proc = subprocess.Popen(
|
|
51
|
+
vite_command,
|
|
52
|
+
shell=True,
|
|
53
|
+
stdout=subprocess.DEVNULL,
|
|
54
|
+
stderr=subprocess.PIPE,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Wait for Vite to be ready
|
|
58
|
+
deadline = time.monotonic() + 15 # 15s timeout
|
|
59
|
+
ready = False
|
|
60
|
+
while time.monotonic() < deadline:
|
|
61
|
+
try:
|
|
62
|
+
sock = socket.create_connection(("127.0.0.1", vite_port), timeout=1)
|
|
63
|
+
sock.close()
|
|
64
|
+
ready = True
|
|
65
|
+
break
|
|
66
|
+
except (ConnectionRefusedError, OSError):
|
|
67
|
+
if vite_proc.poll() is not None:
|
|
68
|
+
stderr = vite_proc.stderr.read().decode() if vite_proc.stderr else ""
|
|
69
|
+
raise RuntimeError(f"Vite process exited with code {vite_proc.returncode}: {stderr}")
|
|
70
|
+
time.sleep(0.3)
|
|
71
|
+
|
|
72
|
+
if not ready:
|
|
73
|
+
vite_proc.terminate()
|
|
74
|
+
raise RuntimeError(f"Vite dev server did not start within 15s on port {vite_port}")
|
|
75
|
+
|
|
76
|
+
logger.info("Vite ready on port %d", vite_port)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
serve(
|
|
80
|
+
wrapped,
|
|
81
|
+
foreground=True,
|
|
82
|
+
host=host,
|
|
83
|
+
port=port,
|
|
84
|
+
pid_path=pid_path,
|
|
85
|
+
name=name,
|
|
86
|
+
pre_serve=pre_serve,
|
|
87
|
+
)
|
|
88
|
+
finally:
|
|
89
|
+
logger.info("Stopping Vite dev server")
|
|
90
|
+
vite_proc.terminate()
|
|
91
|
+
try:
|
|
92
|
+
vite_proc.wait(timeout=5)
|
|
93
|
+
except subprocess.TimeoutExpired:
|
|
94
|
+
vite_proc.kill()
|
|
95
|
+
vite_proc.wait()
|
|
@@ -48,6 +48,7 @@ def test_run_calls_webview(
|
|
|
48
48
|
url=f"http://127.0.0.1:{port}",
|
|
49
49
|
width=800,
|
|
50
50
|
height=600,
|
|
51
|
+
js_api=None,
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
# webview.start() called to enter the event loop with icon=None
|
|
@@ -93,6 +94,87 @@ def test_run_without_icon(
|
|
|
93
94
|
mock_wv_start.assert_called_once_with(icon=None)
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
@patch("webview.start")
|
|
98
|
+
@patch("webview.create_window")
|
|
99
|
+
@patch("wesktop.server.serve")
|
|
100
|
+
def test_run_with_js_api(
|
|
101
|
+
mock_serve: MagicMock,
|
|
102
|
+
mock_create_window: MagicMock,
|
|
103
|
+
mock_wv_start: MagicMock,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""js_api object is forwarded to webview.create_window(js_api=...)."""
|
|
106
|
+
port = _free_port()
|
|
107
|
+
mock_serve.return_value = f"http://127.0.0.1:{port}"
|
|
108
|
+
|
|
109
|
+
class MyAPI:
|
|
110
|
+
def greet(self, name: str) -> str:
|
|
111
|
+
return f"Hello, {name}"
|
|
112
|
+
|
|
113
|
+
api = MyAPI()
|
|
114
|
+
|
|
115
|
+
from wesktop.desktop import run
|
|
116
|
+
|
|
117
|
+
run("myapp:app", host="127.0.0.1", port=port, js_api=api)
|
|
118
|
+
|
|
119
|
+
mock_create_window.assert_called_once_with(
|
|
120
|
+
title="wesktop",
|
|
121
|
+
url=f"http://127.0.0.1:{port}",
|
|
122
|
+
width=1280,
|
|
123
|
+
height=800,
|
|
124
|
+
js_api=api,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@patch("webview.start")
|
|
129
|
+
@patch("webview.create_window")
|
|
130
|
+
@patch("wesktop.server.serve")
|
|
131
|
+
def test_run_without_js_api(
|
|
132
|
+
mock_serve: MagicMock,
|
|
133
|
+
mock_create_window: MagicMock,
|
|
134
|
+
mock_wv_start: MagicMock,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""When no js_api is provided, webview.create_window(js_api=None) is called."""
|
|
137
|
+
port = _free_port()
|
|
138
|
+
mock_serve.return_value = f"http://127.0.0.1:{port}"
|
|
139
|
+
|
|
140
|
+
from wesktop.desktop import run
|
|
141
|
+
|
|
142
|
+
run("myapp:app", host="127.0.0.1", port=port)
|
|
143
|
+
|
|
144
|
+
mock_create_window.assert_called_once_with(
|
|
145
|
+
title="wesktop",
|
|
146
|
+
url=f"http://127.0.0.1:{port}",
|
|
147
|
+
width=1280,
|
|
148
|
+
height=800,
|
|
149
|
+
js_api=None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@patch("webview.start")
|
|
154
|
+
@patch("webview.create_window")
|
|
155
|
+
@patch("wesktop.server.serve")
|
|
156
|
+
def test_run_js_api_via_wrapper(
|
|
157
|
+
mock_serve: MagicMock,
|
|
158
|
+
mock_create_window: MagicMock,
|
|
159
|
+
mock_wv_start: MagicMock,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""js_api passes through the wesktop.run() wrapper to desktop.run()."""
|
|
162
|
+
port = _free_port()
|
|
163
|
+
mock_serve.return_value = f"http://127.0.0.1:{port}"
|
|
164
|
+
|
|
165
|
+
class BridgeAPI:
|
|
166
|
+
def ping(self) -> str:
|
|
167
|
+
return "pong"
|
|
168
|
+
|
|
169
|
+
api = BridgeAPI()
|
|
170
|
+
|
|
171
|
+
wesktop.run("myapp:app", host="127.0.0.1", port=port, js_api=api)
|
|
172
|
+
|
|
173
|
+
mock_create_window.assert_called_once()
|
|
174
|
+
call_kwargs = mock_create_window.call_args[1]
|
|
175
|
+
assert call_kwargs["js_api"] is api
|
|
176
|
+
|
|
177
|
+
|
|
96
178
|
@patch("wesktop.server.Granian")
|
|
97
179
|
def test_serve_calls_granian(mock_granian_cls: MagicMock) -> None:
|
|
98
180
|
"""wesktop.serve() delegates to the server module with correct params."""
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import subprocess
|
|
5
|
+
from unittest.mock import MagicMock, call, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fake_app(scope, receive, send):
|
|
11
|
+
"""Dummy ASGI app for testing."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestDevSpawnsVite:
|
|
16
|
+
"""dev() spawns a subprocess with the vite_command."""
|
|
17
|
+
|
|
18
|
+
@patch("wesktop.server.serve")
|
|
19
|
+
@patch("socket.create_connection")
|
|
20
|
+
@patch("subprocess.Popen")
|
|
21
|
+
def test_spawns_vite_subprocess(
|
|
22
|
+
self,
|
|
23
|
+
mock_popen: MagicMock,
|
|
24
|
+
mock_connect: MagicMock,
|
|
25
|
+
mock_serve: MagicMock,
|
|
26
|
+
) -> None:
|
|
27
|
+
proc = MagicMock()
|
|
28
|
+
proc.poll.return_value = None
|
|
29
|
+
mock_popen.return_value = proc
|
|
30
|
+
|
|
31
|
+
# create_connection succeeds immediately (Vite is "ready")
|
|
32
|
+
mock_sock = MagicMock()
|
|
33
|
+
mock_connect.return_value = mock_sock
|
|
34
|
+
|
|
35
|
+
from wesktop.dev import dev
|
|
36
|
+
|
|
37
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
38
|
+
|
|
39
|
+
mock_popen.assert_called_once_with(
|
|
40
|
+
"npm run dev",
|
|
41
|
+
shell=True,
|
|
42
|
+
stdout=subprocess.DEVNULL,
|
|
43
|
+
stderr=subprocess.PIPE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@patch("wesktop.server.serve")
|
|
47
|
+
@patch("socket.create_connection")
|
|
48
|
+
@patch("subprocess.Popen")
|
|
49
|
+
def test_custom_vite_command(
|
|
50
|
+
self,
|
|
51
|
+
mock_popen: MagicMock,
|
|
52
|
+
mock_connect: MagicMock,
|
|
53
|
+
mock_serve: MagicMock,
|
|
54
|
+
) -> None:
|
|
55
|
+
proc = MagicMock()
|
|
56
|
+
proc.poll.return_value = None
|
|
57
|
+
mock_popen.return_value = proc
|
|
58
|
+
mock_connect.return_value = MagicMock()
|
|
59
|
+
|
|
60
|
+
from wesktop.dev import dev
|
|
61
|
+
|
|
62
|
+
dev(_fake_app, vite_command="pnpm dev", host="127.0.0.1", port=8000)
|
|
63
|
+
|
|
64
|
+
mock_popen.assert_called_once_with(
|
|
65
|
+
"pnpm dev",
|
|
66
|
+
shell=True,
|
|
67
|
+
stdout=subprocess.DEVNULL,
|
|
68
|
+
stderr=subprocess.PIPE,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestDevWrapsWithViteDevProxy:
|
|
73
|
+
"""dev() wraps the app with ViteDevProxy before passing to serve()."""
|
|
74
|
+
|
|
75
|
+
@patch("wesktop.server.serve")
|
|
76
|
+
@patch("socket.create_connection")
|
|
77
|
+
@patch("subprocess.Popen")
|
|
78
|
+
def test_wraps_app_with_proxy(
|
|
79
|
+
self,
|
|
80
|
+
mock_popen: MagicMock,
|
|
81
|
+
mock_connect: MagicMock,
|
|
82
|
+
mock_serve: MagicMock,
|
|
83
|
+
) -> None:
|
|
84
|
+
proc = MagicMock()
|
|
85
|
+
proc.poll.return_value = None
|
|
86
|
+
mock_popen.return_value = proc
|
|
87
|
+
mock_connect.return_value = MagicMock()
|
|
88
|
+
|
|
89
|
+
from wesktop.dev import dev
|
|
90
|
+
from wesktop.middleware import ViteDevProxy
|
|
91
|
+
|
|
92
|
+
dev(_fake_app, vite_port=3000, host="127.0.0.1", port=8000)
|
|
93
|
+
|
|
94
|
+
# serve() was called with a ViteDevProxy wrapping the original app
|
|
95
|
+
args, kwargs = mock_serve.call_args
|
|
96
|
+
wrapped = args[0]
|
|
97
|
+
assert isinstance(wrapped, ViteDevProxy)
|
|
98
|
+
assert wrapped.app is _fake_app
|
|
99
|
+
assert wrapped.vite_port == 3000
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestDevCallsServe:
|
|
103
|
+
"""dev() calls serve() with the wrapped app and foreground=True."""
|
|
104
|
+
|
|
105
|
+
@patch("wesktop.server.serve")
|
|
106
|
+
@patch("socket.create_connection")
|
|
107
|
+
@patch("subprocess.Popen")
|
|
108
|
+
def test_calls_serve_foreground(
|
|
109
|
+
self,
|
|
110
|
+
mock_popen: MagicMock,
|
|
111
|
+
mock_connect: MagicMock,
|
|
112
|
+
mock_serve: MagicMock,
|
|
113
|
+
) -> None:
|
|
114
|
+
proc = MagicMock()
|
|
115
|
+
proc.poll.return_value = None
|
|
116
|
+
mock_popen.return_value = proc
|
|
117
|
+
mock_connect.return_value = MagicMock()
|
|
118
|
+
|
|
119
|
+
from wesktop.dev import dev
|
|
120
|
+
|
|
121
|
+
dev(_fake_app, host="127.0.0.1", port=9000, name="MYAPP")
|
|
122
|
+
|
|
123
|
+
mock_serve.assert_called_once()
|
|
124
|
+
_, kwargs = mock_serve.call_args
|
|
125
|
+
assert kwargs["foreground"] is True
|
|
126
|
+
assert kwargs["host"] == "127.0.0.1"
|
|
127
|
+
assert kwargs["port"] == 9000
|
|
128
|
+
assert kwargs["name"] == "MYAPP"
|
|
129
|
+
assert kwargs["pid_path"] is None
|
|
130
|
+
assert kwargs["pre_serve"] is None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestDevTerminatesVite:
|
|
134
|
+
"""dev() terminates the Vite process on shutdown."""
|
|
135
|
+
|
|
136
|
+
@patch("wesktop.server.serve")
|
|
137
|
+
@patch("socket.create_connection")
|
|
138
|
+
@patch("subprocess.Popen")
|
|
139
|
+
def test_terminates_vite_on_normal_exit(
|
|
140
|
+
self,
|
|
141
|
+
mock_popen: MagicMock,
|
|
142
|
+
mock_connect: MagicMock,
|
|
143
|
+
mock_serve: MagicMock,
|
|
144
|
+
) -> None:
|
|
145
|
+
proc = MagicMock()
|
|
146
|
+
proc.poll.return_value = None
|
|
147
|
+
mock_popen.return_value = proc
|
|
148
|
+
mock_connect.return_value = MagicMock()
|
|
149
|
+
|
|
150
|
+
from wesktop.dev import dev
|
|
151
|
+
|
|
152
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
153
|
+
|
|
154
|
+
proc.terminate.assert_called_once()
|
|
155
|
+
proc.wait.assert_called_once_with(timeout=5)
|
|
156
|
+
|
|
157
|
+
@patch("wesktop.server.serve", side_effect=KeyboardInterrupt)
|
|
158
|
+
@patch("socket.create_connection")
|
|
159
|
+
@patch("subprocess.Popen")
|
|
160
|
+
def test_terminates_vite_on_exception(
|
|
161
|
+
self,
|
|
162
|
+
mock_popen: MagicMock,
|
|
163
|
+
mock_connect: MagicMock,
|
|
164
|
+
mock_serve: MagicMock,
|
|
165
|
+
) -> None:
|
|
166
|
+
proc = MagicMock()
|
|
167
|
+
proc.poll.return_value = None
|
|
168
|
+
mock_popen.return_value = proc
|
|
169
|
+
mock_connect.return_value = MagicMock()
|
|
170
|
+
|
|
171
|
+
from wesktop.dev import dev
|
|
172
|
+
|
|
173
|
+
with pytest.raises(KeyboardInterrupt):
|
|
174
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
175
|
+
|
|
176
|
+
proc.terminate.assert_called_once()
|
|
177
|
+
|
|
178
|
+
@patch("wesktop.server.serve")
|
|
179
|
+
@patch("socket.create_connection")
|
|
180
|
+
@patch("subprocess.Popen")
|
|
181
|
+
def test_kills_vite_if_terminate_times_out(
|
|
182
|
+
self,
|
|
183
|
+
mock_popen: MagicMock,
|
|
184
|
+
mock_connect: MagicMock,
|
|
185
|
+
mock_serve: MagicMock,
|
|
186
|
+
) -> None:
|
|
187
|
+
proc = MagicMock()
|
|
188
|
+
proc.poll.return_value = None
|
|
189
|
+
proc.wait.side_effect = [subprocess.TimeoutExpired(cmd="npm", timeout=5), None]
|
|
190
|
+
mock_popen.return_value = proc
|
|
191
|
+
mock_connect.return_value = MagicMock()
|
|
192
|
+
|
|
193
|
+
from wesktop.dev import dev
|
|
194
|
+
|
|
195
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
196
|
+
|
|
197
|
+
proc.terminate.assert_called_once()
|
|
198
|
+
proc.kill.assert_called_once()
|
|
199
|
+
# wait called twice: once with timeout (raises), once after kill
|
|
200
|
+
assert proc.wait.call_count == 2
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestDevViteFailsToStart:
|
|
204
|
+
"""dev() raises RuntimeError if Vite fails to start."""
|
|
205
|
+
|
|
206
|
+
@patch("socket.create_connection", side_effect=ConnectionRefusedError)
|
|
207
|
+
@patch("subprocess.Popen")
|
|
208
|
+
def test_raises_if_vite_exits_immediately(
|
|
209
|
+
self,
|
|
210
|
+
mock_popen: MagicMock,
|
|
211
|
+
mock_connect: MagicMock,
|
|
212
|
+
) -> None:
|
|
213
|
+
proc = MagicMock()
|
|
214
|
+
proc.poll.return_value = 1 # process exited
|
|
215
|
+
proc.returncode = 1
|
|
216
|
+
proc.stderr = MagicMock()
|
|
217
|
+
proc.stderr.read.return_value = b"vite: command not found"
|
|
218
|
+
mock_popen.return_value = proc
|
|
219
|
+
|
|
220
|
+
from wesktop.dev import dev
|
|
221
|
+
|
|
222
|
+
with pytest.raises(RuntimeError, match="Vite process exited with code 1"):
|
|
223
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
224
|
+
|
|
225
|
+
@patch("time.monotonic")
|
|
226
|
+
@patch("time.sleep")
|
|
227
|
+
@patch("socket.create_connection", side_effect=ConnectionRefusedError)
|
|
228
|
+
@patch("subprocess.Popen")
|
|
229
|
+
def test_raises_if_vite_never_ready(
|
|
230
|
+
self,
|
|
231
|
+
mock_popen: MagicMock,
|
|
232
|
+
mock_connect: MagicMock,
|
|
233
|
+
mock_sleep: MagicMock,
|
|
234
|
+
mock_monotonic: MagicMock,
|
|
235
|
+
) -> None:
|
|
236
|
+
proc = MagicMock()
|
|
237
|
+
proc.poll.return_value = None # process still running but never ready
|
|
238
|
+
mock_popen.return_value = proc
|
|
239
|
+
|
|
240
|
+
# Simulate time passing beyond the 15s deadline
|
|
241
|
+
mock_monotonic.side_effect = [0.0, 0.0, 16.0]
|
|
242
|
+
|
|
243
|
+
from wesktop.dev import dev
|
|
244
|
+
|
|
245
|
+
with pytest.raises(RuntimeError, match="did not start within 15s"):
|
|
246
|
+
dev(_fake_app, host="127.0.0.1", port=8000)
|
|
247
|
+
|
|
248
|
+
proc.terminate.assert_called_once()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestDevStringTarget:
|
|
252
|
+
"""dev() resolves string targets via importlib."""
|
|
253
|
+
|
|
254
|
+
@patch("wesktop.server.serve")
|
|
255
|
+
@patch("socket.create_connection")
|
|
256
|
+
@patch("subprocess.Popen")
|
|
257
|
+
def test_resolves_string_target(
|
|
258
|
+
self,
|
|
259
|
+
mock_popen: MagicMock,
|
|
260
|
+
mock_connect: MagicMock,
|
|
261
|
+
mock_serve: MagicMock,
|
|
262
|
+
) -> None:
|
|
263
|
+
proc = MagicMock()
|
|
264
|
+
proc.poll.return_value = None
|
|
265
|
+
mock_popen.return_value = proc
|
|
266
|
+
mock_connect.return_value = MagicMock()
|
|
267
|
+
|
|
268
|
+
from wesktop.dev import dev
|
|
269
|
+
from wesktop.middleware import ViteDevProxy
|
|
270
|
+
|
|
271
|
+
# Use a real importable module:attribute
|
|
272
|
+
dev("wesktop.asgi:create_app", host="127.0.0.1", port=8000)
|
|
273
|
+
|
|
274
|
+
args, kwargs = mock_serve.call_args
|
|
275
|
+
wrapped = args[0]
|
|
276
|
+
assert isinstance(wrapped, ViteDevProxy)
|
|
277
|
+
# The resolved app should be the actual create_app function
|
|
278
|
+
from wesktop.asgi import create_app
|
|
279
|
+
assert wrapped.app is create_app
|
|
@@ -70,8 +70,13 @@ class TestJWTTokens:
|
|
|
70
70
|
def test_tampered_token_returns_none(self):
|
|
71
71
|
secret = "test-secret-key-that-is-at-least-thirty-two-bytes-long"
|
|
72
72
|
token = create_token("alice", "admin", secret)
|
|
73
|
-
# Flip a character in the signature
|
|
74
|
-
|
|
73
|
+
# Flip a character in the middle of the signature (not the end,
|
|
74
|
+
# where base64url padding bits can absorb the change)
|
|
75
|
+
parts = token.split(".")
|
|
76
|
+
sig = list(parts[2])
|
|
77
|
+
mid = len(sig) // 2
|
|
78
|
+
sig[mid] = "X" if sig[mid] != "X" else "Y"
|
|
79
|
+
tampered = parts[0] + "." + parts[1] + "." + "".join(sig)
|
|
75
80
|
assert verify_token(tampered, secret) is None
|
|
76
81
|
|
|
77
82
|
def test_wrong_secret_returns_none(self):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1cf0504c7c65adf18fb01877fd2cc105334f46f5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|