pyweb-stack 0.2.0__tar.gz → 0.3.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.
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/PKG-INFO +23 -7
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/README.md +22 -6
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyproject.toml +1 -1
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/__init__.py +4 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/ai/guide.md +19 -7
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/app_loader.py +46 -15
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/asgi.py +25 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/auth.py +25 -3
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/bench.py +31 -7
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/build.py +7 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/cli/__init__.py +10 -9
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/lower.py +97 -22
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/pipeline.py +96 -9
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/pyjs.py +3 -1
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/db/__init__.py +1 -1
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/hosting.py +37 -1
- pyweb_stack-0.3.0/pyweb/lsp.py +398 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/mcp.py +177 -9
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/models.py +2 -2
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/realtime.py +192 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/runtime/browser/runtime.js +181 -36
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/runtime/server/__init__.py +38 -31
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/serve.py +5 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/ssr.py +24 -6
- pyweb_stack-0.3.0/pyweb/templates/app.css +32 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/templates/chat.pyweb +9 -11
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/testing.py +4 -8
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/PKG-INFO +23 -7
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/SOURCES.txt +1 -1
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_auth.py +11 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_codegen.py +16 -0
- pyweb_stack-0.3.0/tests/test_lsp.py +187 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_mcp.py +24 -2
- pyweb_stack-0.3.0/tests/test_multifile.py +182 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_plugins_platform_bench.py +8 -0
- pyweb_stack-0.3.0/tests/test_realtime_transport.py +181 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_website.py +21 -1
- pyweb_stack-0.2.0/pyweb/lsp.py +0 -214
- pyweb_stack-0.2.0/pyweb/templates/app.css +0 -28
- pyweb_stack-0.2.0/tests/test_lsp.py +0 -81
- pyweb_stack-0.2.0/tests/test_lsp_state.py +0 -76
- pyweb_stack-0.2.0/tests/test_realtime_transport.py +0 -77
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/LICENSE +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/app.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/browser.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/cache.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/cli/__main__.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/__init__.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/ast.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/__init__.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/ir.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/errors.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/parser.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/compiler/rpc.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/context.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/css.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/db/migrate.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/decorators.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/deploy.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/forms.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/jobs.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/live.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/npm.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/observability.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/platform.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/plugins.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/py.typed +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/rpc.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/security.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/sync.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/templates/auth.pyweb +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/templates/blog.pyweb +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/templates/counter.pyweb +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/templates/todo.pyweb +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb/uploads.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/requires.txt +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/top_level.txt +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/setup.cfg +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_auth_contract.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_auth_security.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_auth_v1.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_backplane.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_browser_build.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_budget.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_cli_deploy.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_compiler.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_db_production.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_deploy_upload_render.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_docs.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_dts.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_e2e.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_examples.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_forms_security_obs.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_jobs_cache_realtime.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_models_db.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_observability.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_parser.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_parser_v1.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_production_gaps.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_pyjs_semantics.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_reactivity.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_reference_apps.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_rpc_placement.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_rpc_production.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_runtime_signals.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_server_db.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_static_hashing.py +0 -0
- {pyweb_stack-0.2.0 → pyweb_stack-0.3.0}/tests/test_sync_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyweb-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Full-stack web apps in one Python file: server-rendered pages, reactive browser UI, typed RPC.
|
|
5
5
|
Author-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>, Claude <noreply@anthropic.com>
|
|
6
6
|
Maintainer-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>
|
|
@@ -64,6 +64,9 @@ functions, without a JavaScript toolchain.
|
|
|
64
64
|

|
|
65
65
|

|
|
66
66
|
|
|
67
|
+
**[Try it in your browser](https://maanavkrishna.github.io/PyWeb/playground.html)**: the playground runs
|
|
68
|
+
the real PyWeb, server functions included, on Python compiled to WebAssembly.
|
|
69
|
+
|
|
67
70
|
```pyweb
|
|
68
71
|
from pyweb import App, server
|
|
69
72
|
|
|
@@ -143,8 +146,8 @@ and [current limitations](docs/16-limitations-roadmap.md).
|
|
|
143
146
|
## Build it with AI
|
|
144
147
|
|
|
145
148
|
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
146
|
-
render and test your app, with errors that come back as line
|
|
147
|
-
fix hints:
|
|
149
|
+
render, screenshot and test your app, with errors that come back as line
|
|
150
|
+
numbers and fix hints:
|
|
148
151
|
|
|
149
152
|
```bash
|
|
150
153
|
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
@@ -185,7 +188,7 @@ a real browser by the test suite.
|
|
|
185
188
|
| [`todo`](examples/todo/app.pyweb) | components, list mutation, filters, keyed lists |
|
|
186
189
|
| [`blog`](examples/blog/app.pyweb) | SQL database, server functions, route params, 404s, validation errors |
|
|
187
190
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
188
|
-
| [`chat`](examples/chat/app.pyweb) | route params, shared server state,
|
|
191
|
+
| [`chat`](examples/chat/app.pyweb) | route params, shared server state, live updates with `publish`/`subscribe` |
|
|
189
192
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
190
193
|
|
|
191
194
|
## Command line
|
|
@@ -198,13 +201,26 @@ pyweb check app.pyweb # compile + security checks for
|
|
|
198
201
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
199
202
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
200
203
|
pyweb mcp # MCP server for AI assistants (stdio)
|
|
204
|
+
pyweb lsp # language server for editors (stdio)
|
|
201
205
|
```
|
|
202
206
|
|
|
207
|
+
Editors: the [VS Code extension](editors/vscode) adds highlighting, errors as you
|
|
208
|
+
type, hover that shows where code runs, completion and go to definition. Any
|
|
209
|
+
other LSP editor can run `pyweb lsp` ([setup](docs/14-cli.md#editor-support)).
|
|
210
|
+
|
|
203
211
|
## Status
|
|
204
212
|
|
|
205
|
-
PyWeb 0.
|
|
206
|
-
RPC protocol and CLI are documented and tested,
|
|
207
|
-
announced in the changelog
|
|
213
|
+
PyWeb is in beta (0.x; see the PyPI badge above for the latest version).
|
|
214
|
+
The language, server API, RPC protocol and CLI are documented and tested,
|
|
215
|
+
and changes to them are announced in the [changelog](CHANGELOG.md) (see
|
|
216
|
+
[stability](docs/15-toolkit.md#stability)). Upgrade with
|
|
217
|
+
`pip install -U pyweb-stack`.
|
|
218
|
+
|
|
219
|
+
| Version | Highlights |
|
|
220
|
+
|---|---|
|
|
221
|
+
| 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
|
|
222
|
+
| 0.2 | MCP server for AI assistants, AI guide, project templates, `AGENTS.md`/`CLAUDE.md`, `llms.txt` |
|
|
223
|
+
| 0.1 | First public release: compiler, reactive runtime, server rendering, typed RPC, sessions, databases, CLI | The test suite covers the
|
|
208
224
|
parser, the Python→JavaScript translation (differentially, against
|
|
209
225
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
210
226
|
example app in Chromium, and the database/Redis layers against real
|
|
@@ -10,6 +10,9 @@ functions, without a JavaScript toolchain.
|
|
|
10
10
|

|
|
11
11
|

|
|
12
12
|
|
|
13
|
+
**[Try it in your browser](https://maanavkrishna.github.io/PyWeb/playground.html)**: the playground runs
|
|
14
|
+
the real PyWeb, server functions included, on Python compiled to WebAssembly.
|
|
15
|
+
|
|
13
16
|
```pyweb
|
|
14
17
|
from pyweb import App, server
|
|
15
18
|
|
|
@@ -89,8 +92,8 @@ and [current limitations](docs/16-limitations-roadmap.md).
|
|
|
89
92
|
## Build it with AI
|
|
90
93
|
|
|
91
94
|
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
92
|
-
render and test your app, with errors that come back as line
|
|
93
|
-
fix hints:
|
|
95
|
+
render, screenshot and test your app, with errors that come back as line
|
|
96
|
+
numbers and fix hints:
|
|
94
97
|
|
|
95
98
|
```bash
|
|
96
99
|
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
@@ -131,7 +134,7 @@ a real browser by the test suite.
|
|
|
131
134
|
| [`todo`](examples/todo/app.pyweb) | components, list mutation, filters, keyed lists |
|
|
132
135
|
| [`blog`](examples/blog/app.pyweb) | SQL database, server functions, route params, 404s, validation errors |
|
|
133
136
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
134
|
-
| [`chat`](examples/chat/app.pyweb) | route params, shared server state,
|
|
137
|
+
| [`chat`](examples/chat/app.pyweb) | route params, shared server state, live updates with `publish`/`subscribe` |
|
|
135
138
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
136
139
|
|
|
137
140
|
## Command line
|
|
@@ -144,13 +147,26 @@ pyweb check app.pyweb # compile + security checks for
|
|
|
144
147
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
145
148
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
146
149
|
pyweb mcp # MCP server for AI assistants (stdio)
|
|
150
|
+
pyweb lsp # language server for editors (stdio)
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
Editors: the [VS Code extension](editors/vscode) adds highlighting, errors as you
|
|
154
|
+
type, hover that shows where code runs, completion and go to definition. Any
|
|
155
|
+
other LSP editor can run `pyweb lsp` ([setup](docs/14-cli.md#editor-support)).
|
|
156
|
+
|
|
149
157
|
## Status
|
|
150
158
|
|
|
151
|
-
PyWeb 0.
|
|
152
|
-
RPC protocol and CLI are documented and tested,
|
|
153
|
-
announced in the changelog
|
|
159
|
+
PyWeb is in beta (0.x; see the PyPI badge above for the latest version).
|
|
160
|
+
The language, server API, RPC protocol and CLI are documented and tested,
|
|
161
|
+
and changes to them are announced in the [changelog](CHANGELOG.md) (see
|
|
162
|
+
[stability](docs/15-toolkit.md#stability)). Upgrade with
|
|
163
|
+
`pip install -U pyweb-stack`.
|
|
164
|
+
|
|
165
|
+
| Version | Highlights |
|
|
166
|
+
|---|---|
|
|
167
|
+
| 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
|
|
168
|
+
| 0.2 | MCP server for AI assistants, AI guide, project templates, `AGENTS.md`/`CLAUDE.md`, `llms.txt` |
|
|
169
|
+
| 0.1 | First public release: compiler, reactive runtime, server rendering, typed RPC, sessions, databases, CLI | The test suite covers the
|
|
154
170
|
parser, the Python→JavaScript translation (differentially, against
|
|
155
171
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
156
172
|
example app in Chromium, and the database/Redis layers against real
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyweb-stack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Full-stack web apps in one Python file: server-rendered pages, reactive browser UI, typed RPC."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -10,6 +10,7 @@ from .jobs import task
|
|
|
10
10
|
from .context import NotFound, redirect, request, session
|
|
11
11
|
from .rpc import RPCError
|
|
12
12
|
from .decorators import component, edge, server, worker
|
|
13
|
+
from .realtime import channel, publish, subscribe
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"App",
|
|
@@ -22,6 +23,9 @@ __all__ = [
|
|
|
22
23
|
"edge",
|
|
23
24
|
"server",
|
|
24
25
|
"worker",
|
|
26
|
+
"channel",
|
|
27
|
+
"publish",
|
|
28
|
+
"subscribe",
|
|
25
29
|
"Email",
|
|
26
30
|
"Model",
|
|
27
31
|
"task",
|
|
@@ -135,8 +135,13 @@ def Home():
|
|
|
135
135
|
|
|
136
136
|
Props are parameters (defaults = optional). Pass callbacks as props
|
|
137
137
|
(`on_delete={lambda: delete(i)}`) and use them as handlers inside
|
|
138
|
-
(`onclick={on_delete}`).
|
|
139
|
-
|
|
138
|
+
(`onclick={on_delete}`). A component's initial state must be computable in
|
|
139
|
+
the browser (pass server data as props).
|
|
140
|
+
|
|
141
|
+
Bigger apps can split into files: put components, `@server` functions and
|
|
142
|
+
constants in e.g. `widgets.pyweb` (or `ui/cards.pyweb`) next to `app.pyweb`
|
|
143
|
+
and `from widgets import Card, save`. Pages stay in `app.pyweb`; import
|
|
144
|
+
server functions without `as`; each file keeps its own constants.
|
|
140
145
|
|
|
141
146
|
## 6. Server functions, sessions, routing
|
|
142
147
|
|
|
@@ -167,8 +172,12 @@ def Item(item_id: int):
|
|
|
167
172
|
`db.execute("select ... where id = ?", (x,)).dicts()`; always use `?`
|
|
168
173
|
parameters; `with db.transaction(): ...`.
|
|
169
174
|
- In handlers, navigate with `window.location.href = "/path"`.
|
|
170
|
-
- Run code after load with a handler named `on_mount
|
|
171
|
-
|
|
175
|
+
- Run code after load with a handler named `on_mount`.
|
|
176
|
+
- Live updates: in a server function `publish("room:1", data)`; in the
|
|
177
|
+
page `feed = channel("room:1")` (runs on the server); in `on_mount`
|
|
178
|
+
`subscribe(feed, handler)`, where `handler(message)` assigns page
|
|
179
|
+
variables. Import all three from `pyweb`. Never poll with
|
|
180
|
+
`setInterval` when `publish` fits.
|
|
172
181
|
|
|
173
182
|
## 7. Python that compiles to the browser
|
|
174
183
|
|
|
@@ -195,7 +204,7 @@ JS functions, server-only names. Move such code into `@server` functions.
|
|
|
195
204
|
| `cannot assign to ... derived/read-only` | Assign to a variable the handler owns (make it state), not a computed/constant. |
|
|
196
205
|
| `server secret ... would be sent to the browser` | Keep the value inside `@server` functions; don't read it in markup/handlers. |
|
|
197
206
|
| `bind={x} must name a local variable` | Declare `x = ""` (or a number/bool) in the page before the markup. |
|
|
198
|
-
| `unknown component <X>` | Define `def X(...)` with markup in
|
|
207
|
+
| `unknown component <X>` | Define `def X(...)` with markup (capitalized) in this file, or import it: `from widgets import X`. |
|
|
199
208
|
| `mismatched </tag>` / `is never closed` | Close every tag; void tags (`input`, `img`, `br`) need no close (`<input ... />`). |
|
|
200
209
|
| `... is not supported in browser code` | Rewrite with supported constructs or move it to `@server`. |
|
|
201
210
|
|
|
@@ -208,8 +217,11 @@ JS functions, server-only names. Move such code into `@server` functions.
|
|
|
208
217
|
3. Use `pyweb inspect` (MCP: `pyweb_inspect`) to confirm what runs in the
|
|
209
218
|
browser vs server and what is sent to the browser.
|
|
210
219
|
4. Verify behaviour: render pages (`pyweb_render`) and call server
|
|
211
|
-
functions (`pyweb_call`)
|
|
212
|
-
|
|
220
|
+
functions (`pyweb_call`). See the page and try interactions in a real
|
|
221
|
+
browser with `pyweb_screenshot` (steps: click, fill, press, ...).
|
|
222
|
+
5. Test: new apps include `test_app.py` (`pyweb.testing.TestClient`); add
|
|
223
|
+
tests for what you change and run `pytest` (MCP: `pyweb_test`).
|
|
224
|
+
6. Ship: `pyweb build app.pyweb --out dist --production` then
|
|
213
225
|
`pyweb serve dist` (set `PYWEB_AUTH_SECRET` in production).
|
|
214
226
|
|
|
215
227
|
Full docs: https://maanavkrishna.github.io/PyWeb/
|
|
@@ -18,7 +18,7 @@ from .compiler import compile_source
|
|
|
18
18
|
from .compiler import parser as P
|
|
19
19
|
from .compiler.lower import is_ui_stmt
|
|
20
20
|
from .context import NotFound, Redirect
|
|
21
|
-
from .ssr import Renderer, page_html
|
|
21
|
+
from .ssr import NS_KEY, Renderer, page_html
|
|
22
22
|
|
|
23
23
|
STATE_PREFIX = "__pyweb_state_"
|
|
24
24
|
|
|
@@ -53,12 +53,30 @@ class LoadedApp:
|
|
|
53
53
|
self.extra_stylesheets = list(stylesheets or [])
|
|
54
54
|
self.compiled = compile_source(source, filename=self.filename)
|
|
55
55
|
self.ctx = self.compiled["context"]
|
|
56
|
-
|
|
56
|
+
# Other .pyweb files the app imports run as real modules, registered
|
|
57
|
+
# under their import name so `from widgets import Card` works.
|
|
58
|
+
self.libraries = self.compiled["libraries"]
|
|
59
|
+
self.lib_modules = {}
|
|
60
|
+
for lib in self.libraries:
|
|
61
|
+
with open(lib.path, encoding="utf-8") as fh:
|
|
62
|
+
lib_source = fh.read()
|
|
63
|
+
infos = list(lib.components.values())
|
|
64
|
+
self.lib_modules[lib.path] = self._exec(lib_source, lib.path, infos, lib.name)
|
|
65
|
+
self.files = [os.path.abspath(path)] if path else []
|
|
66
|
+
self.files += [lib.path for lib in self.libraries]
|
|
67
|
+
infos = [p["info"] for p in self.compiled["pages"].values()]
|
|
68
|
+
infos += list(self.compiled["components"].values())
|
|
69
|
+
self.module = self._exec(source, self.filename, infos)
|
|
57
70
|
self.rpc = {}
|
|
58
71
|
for spec in self.compiled["rpc"]:
|
|
59
72
|
fn = self.module.__dict__.get(spec["name"])
|
|
60
73
|
if callable(fn):
|
|
61
74
|
self.rpc[spec["name"]] = fn
|
|
75
|
+
for lib in self.libraries:
|
|
76
|
+
for spec in lib.rpc:
|
|
77
|
+
fn = self.lib_modules[lib.path].__dict__.get(spec["name"])
|
|
78
|
+
if callable(fn):
|
|
79
|
+
self.rpc[spec["name"]] = fn
|
|
62
80
|
app_obj = next((v for v in self.module.__dict__.values()
|
|
63
81
|
if type(v).__name__ == "App" and type(v).__module__.startswith("pyweb")), None)
|
|
64
82
|
self.app = app_obj
|
|
@@ -67,29 +85,42 @@ class LoadedApp:
|
|
|
67
85
|
self.lang = getattr(app_obj, "lang", "en") or "en"
|
|
68
86
|
|
|
69
87
|
# ----------------------------------------------------------- module
|
|
70
|
-
def _exec(self):
|
|
71
|
-
py_source, _ui = P.split_sources(
|
|
72
|
-
tree = ast.parse(py_source, filename=
|
|
73
|
-
infos = [p["info"] for p in self.compiled["pages"].values()]
|
|
74
|
-
infos += list(self.compiled["components"].values())
|
|
88
|
+
def _exec(self, source, filename, infos, module_name=None):
|
|
89
|
+
py_source, _ui = P.split_sources(source)
|
|
90
|
+
tree = ast.parse(py_source, filename=filename)
|
|
75
91
|
for info in infos:
|
|
76
92
|
tree.body.append(_state_function(info.node))
|
|
77
|
-
stem = os.path.splitext(os.path.basename(
|
|
78
|
-
mod = types.ModuleType(f"pyweb_app_{stem}")
|
|
79
|
-
mod.__file__ = os.path.abspath(
|
|
93
|
+
stem = os.path.splitext(os.path.basename(filename))[0] or "app"
|
|
94
|
+
mod = types.ModuleType(module_name or f"pyweb_app_{stem}")
|
|
95
|
+
mod.__file__ = os.path.abspath(filename) if self.path else filename
|
|
80
96
|
mod.__dict__["__pyweb_ui__"] = lambda *_a: None
|
|
81
97
|
app_dir = os.path.dirname(os.path.abspath(self.path)) if self.path else None
|
|
82
98
|
if app_dir and app_dir not in sys.path:
|
|
83
99
|
sys.path.insert(0, app_dir)
|
|
84
100
|
sys.modules[mod.__name__] = mod
|
|
85
|
-
exec(compile(tree,
|
|
101
|
+
exec(compile(tree, filename, "exec"), mod.__dict__) # noqa: S102 - the app itself
|
|
86
102
|
return mod
|
|
87
103
|
|
|
88
104
|
# -------------------------------------------------------- rendering
|
|
89
|
-
def _component_state(self, name, props):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
def _component_state(self, name, props, ns=None):
|
|
106
|
+
"""Run component ``name`` as used by the file ``ns`` (None: the app)."""
|
|
107
|
+
ctx = ns or self.ctx
|
|
108
|
+
source = ctx.imported_components.get(name)
|
|
109
|
+
if source is not None:
|
|
110
|
+
lib, name = source
|
|
111
|
+
ctx, module = lib.ctx, self.lib_modules[lib.path]
|
|
112
|
+
else:
|
|
113
|
+
module = self.module if ctx is self.ctx else next(
|
|
114
|
+
self.lib_modules[lib.path] for lib in self.libraries if lib.ctx is ctx)
|
|
115
|
+
info = ctx.components[name]
|
|
116
|
+
fn = module.__dict__[STATE_PREFIX + name]
|
|
117
|
+
local = fn(**{k: v for k, v in props.items() if k in info.params})
|
|
118
|
+
if module is self.module:
|
|
119
|
+
return info.ui, local
|
|
120
|
+
# Markup in another file sees that file's globals.
|
|
121
|
+
env = {k: v for k, v in module.__dict__.items() if not k.startswith("__")}
|
|
122
|
+
env.update(local)
|
|
123
|
+
env[NS_KEY] = ctx
|
|
93
124
|
return info.ui, env
|
|
94
125
|
|
|
95
126
|
def renderer(self):
|
|
@@ -20,6 +20,27 @@ import asyncio
|
|
|
20
20
|
from .hosting import Site
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
async def _stream(stream, receive, send):
|
|
24
|
+
"""Send an event stream until it ends or the client disconnects."""
|
|
25
|
+
async def watch():
|
|
26
|
+
while (await receive())["type"] != "http.disconnect":
|
|
27
|
+
pass
|
|
28
|
+
stream.close()
|
|
29
|
+
|
|
30
|
+
watcher = asyncio.ensure_future(watch())
|
|
31
|
+
try:
|
|
32
|
+
async for chunk in stream.aiter():
|
|
33
|
+
if watcher.done():
|
|
34
|
+
break
|
|
35
|
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
36
|
+
await send({"type": "http.response.body", "body": b""})
|
|
37
|
+
except OSError:
|
|
38
|
+
pass
|
|
39
|
+
finally:
|
|
40
|
+
stream.close()
|
|
41
|
+
watcher.cancel()
|
|
42
|
+
|
|
43
|
+
|
|
23
44
|
def create_app(target="app.pyweb", *, debug=False, max_body=1_048_576, **server_kwargs):
|
|
24
45
|
"""Return an ASGI 3 application serving ``target`` (a `.pyweb` file or dist dir)."""
|
|
25
46
|
site = Site(target, debug=debug, max_body=max_body, **server_kwargs)
|
|
@@ -62,6 +83,10 @@ def create_app(target="app.pyweb", *, debug=False, max_body=1_048_576, **server_
|
|
|
62
83
|
status, hdrs, body = await asyncio.to_thread(
|
|
63
84
|
site.respond, scope.get("method", "GET"), path, headers, b"".join(chunks))
|
|
64
85
|
raw_headers = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in hdrs]
|
|
86
|
+
if hasattr(body, "aiter"): # Server-Sent Events
|
|
87
|
+
await send({"type": "http.response.start", "status": status, "headers": raw_headers})
|
|
88
|
+
await _stream(body, receive, send)
|
|
89
|
+
return
|
|
65
90
|
if not any(k == b"content-length" for k, _ in raw_headers):
|
|
66
91
|
raw_headers.append((b"content-length", str(len(body)).encode()))
|
|
67
92
|
await send({"type": "http.response.start", "status": status, "headers": raw_headers})
|
|
@@ -37,14 +37,36 @@ def _b64d(data: str) -> bytes:
|
|
|
37
37
|
return base64.urlsafe_b64decode(data + "=" * (-len(data) % 4))
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _pbkdf2_sha256(password: bytes, salt: bytes, iterations: int) -> bytes:
|
|
41
|
+
"""PBKDF2-HMAC-SHA256 (32-byte key). Uses hashlib's C version when it
|
|
42
|
+
exists; Pythons built without OpenSSL (e.g. Pyodide) get this pure
|
|
43
|
+
Python one, which gives the same result."""
|
|
44
|
+
fast = getattr(hashlib, "pbkdf2_hmac", None)
|
|
45
|
+
if fast is not None:
|
|
46
|
+
return fast("sha256", password, salt, iterations)
|
|
47
|
+
mac = hmac.new(password, digestmod=hashlib.sha256)
|
|
48
|
+
|
|
49
|
+
def prf(data):
|
|
50
|
+
m = mac.copy()
|
|
51
|
+
m.update(data)
|
|
52
|
+
return m.digest()
|
|
53
|
+
|
|
54
|
+
u = prf(salt + b"\x00\x00\x00\x01")
|
|
55
|
+
acc = int.from_bytes(u, "big")
|
|
56
|
+
for _ in range(iterations - 1):
|
|
57
|
+
u = prf(u)
|
|
58
|
+
acc ^= int.from_bytes(u, "big")
|
|
59
|
+
return acc.to_bytes(32, "big")
|
|
60
|
+
|
|
61
|
+
|
|
40
62
|
def hash_password(password, *, salt=None, rounds=None, iterations=_ITERATIONS):
|
|
41
63
|
if salt is None:
|
|
42
64
|
salt_bytes = secrets.token_bytes(16)
|
|
43
|
-
dk =
|
|
65
|
+
dk = _pbkdf2_sha256(password.encode(), salt_bytes, iterations)
|
|
44
66
|
return f"{_HASH_ALGO}${iterations}${_b64e(salt_bytes)}${_b64e(dk)}"
|
|
45
67
|
salt_bytes = salt if isinstance(salt, bytes) else bytes.fromhex(salt)
|
|
46
68
|
n = rounds if rounds is not None else iterations
|
|
47
|
-
dk =
|
|
69
|
+
dk = _pbkdf2_sha256(password.encode(), salt_bytes, n)
|
|
48
70
|
return f"pbkdf2${n}${salt_bytes.hex()}${dk.hex()}"
|
|
49
71
|
|
|
50
72
|
|
|
@@ -63,7 +85,7 @@ def verify_password(password, stored):
|
|
|
63
85
|
expected = bytes.fromhex(dk_part)
|
|
64
86
|
except (ValueError, base64.binascii.Error):
|
|
65
87
|
return False
|
|
66
|
-
candidate =
|
|
88
|
+
candidate = _pbkdf2_sha256(password.encode(), salt, iters_i)
|
|
67
89
|
if algo == _HASH_ALGO:
|
|
68
90
|
return hmac.compare_digest(candidate, expected)
|
|
69
91
|
return hmac.compare_digest(candidate.hex(), expected.hex() if isinstance(expected, bytes) else dk_part)
|
|
@@ -87,15 +87,39 @@ def run(apps=None) -> dict:
|
|
|
87
87
|
return {name: bench_app(name, src) for name, src in apps.items()}
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def
|
|
90
|
+
def over_budget(results, *, max_ssr_ms=None, max_compile_ms=None) -> list:
|
|
91
|
+
"""Lines describing every app that exceeds a budget."""
|
|
92
|
+
out = []
|
|
93
|
+
for r in results.values():
|
|
94
|
+
if max_ssr_ms is not None and r["ssr_ms"] > max_ssr_ms:
|
|
95
|
+
out.append(f"{r['app']}: server render {r['ssr_ms']} ms > {max_ssr_ms} ms")
|
|
96
|
+
if max_compile_ms is not None and r["compile_ms"] > max_compile_ms:
|
|
97
|
+
out.append(f"{r['app']}: compile {r['compile_ms']} ms > {max_compile_ms} ms")
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main(argv=None):
|
|
102
|
+
import argparse
|
|
103
|
+
import sys
|
|
104
|
+
ap = argparse.ArgumentParser(prog="python -m pyweb.bench", description="Measure compile time, "
|
|
105
|
+
"shipped JavaScript and server render time.")
|
|
106
|
+
ap.add_argument("--json", action="store_true", help="print only JSON")
|
|
107
|
+
ap.add_argument("--max-ssr-ms", type=float, help="fail if any server render is slower")
|
|
108
|
+
ap.add_argument("--max-compile-ms", type=float, help="fail if any compile is slower")
|
|
109
|
+
args = ap.parse_args(argv)
|
|
91
110
|
results = run()
|
|
92
111
|
print(json.dumps(results, indent=2))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
if not args.json:
|
|
113
|
+
rt = runtime_sizes()
|
|
114
|
+
print(f"\nshared runtime: {rt['bytes']} B minified, {rt['gzip_bytes']} B gzip "
|
|
115
|
+
"(downloaded once, cached across pages)")
|
|
116
|
+
for r in results.values():
|
|
117
|
+
print(f"{r['app']:>8}: page JS {r['page_js_gzip']} B gzip, server render {r['ssr_ms']} ms")
|
|
118
|
+
failed = over_budget(results, max_ssr_ms=args.max_ssr_ms, max_compile_ms=args.max_compile_ms)
|
|
119
|
+
for line in failed:
|
|
120
|
+
print("over budget:", line, file=sys.stderr)
|
|
121
|
+
return 1 if failed else 0
|
|
98
122
|
|
|
99
123
|
|
|
100
124
|
if __name__ == "__main__":
|
|
101
|
-
main()
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -235,6 +235,13 @@ def build(compiled: dict, out: str, *, minifier=None, extract_css=True,
|
|
|
235
235
|
if source is not None:
|
|
236
236
|
with open(f"{out}/app.pyweb", "w", encoding="utf-8") as fh:
|
|
237
237
|
fh.write(source)
|
|
238
|
+
# Other .pyweb files the app imports, at the same relative paths.
|
|
239
|
+
for lib in compiled.get("libraries", []):
|
|
240
|
+
rel = os.path.relpath(lib.path, app_dir) if app_dir else os.path.basename(lib.path)
|
|
241
|
+
if rel.startswith(".."):
|
|
242
|
+
rel = os.path.basename(lib.path)
|
|
243
|
+
os.makedirs(os.path.dirname(os.path.join(out, rel)) or out, exist_ok=True)
|
|
244
|
+
shutil.copyfile(lib.path, os.path.join(out, rel))
|
|
238
245
|
try:
|
|
239
246
|
from pyweb import __version__ as pyweb_version
|
|
240
247
|
except ImportError: # pragma: no cover
|
|
@@ -139,14 +139,8 @@ def cmd_dev(args):
|
|
|
139
139
|
|
|
140
140
|
class H(http.server.BaseHTTPRequestHandler):
|
|
141
141
|
def _send(self, status, headers, body):
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if k.lower() != "content-length":
|
|
145
|
-
self.send_header(k, v)
|
|
146
|
-
self.send_header("Content-Length", str(len(body)))
|
|
147
|
-
self.end_headers()
|
|
148
|
-
if self.command != "HEAD":
|
|
149
|
-
self.wfile.write(body)
|
|
142
|
+
from pyweb.hosting import write_http
|
|
143
|
+
write_http(self, status, headers, body, head=self.command == "HEAD")
|
|
150
144
|
|
|
151
145
|
def _handle(self, method, body=b""):
|
|
152
146
|
if self.path == "/__pyweb/dev/version":
|
|
@@ -157,7 +151,7 @@ def cmd_dev(args):
|
|
|
157
151
|
return self._send(500, [("Content-Type", "text/html; charset=utf-8")], html)
|
|
158
152
|
status, headers, raw = state.site.respond(method, self.path, dict(self.headers), body)
|
|
159
153
|
ctype = next((v for k, v in headers if k.lower() == "content-type"), "")
|
|
160
|
-
if ctype.startswith("text/html") and b"</body>" in raw:
|
|
154
|
+
if ctype.startswith("text/html") and isinstance(raw, bytes) and b"</body>" in raw:
|
|
161
155
|
raw = raw.replace(b"</body>", DEV_RELOAD_JS.encode() + b"</body>", 1)
|
|
162
156
|
return self._send(status, headers, raw)
|
|
163
157
|
|
|
@@ -393,6 +387,12 @@ def cmd_new(args):
|
|
|
393
387
|
print(f"next: cd {args.name} && pyweb dev app.pyweb")
|
|
394
388
|
|
|
395
389
|
|
|
390
|
+
def cmd_lsp(args):
|
|
391
|
+
"""Language server for editors (stdio)."""
|
|
392
|
+
from pyweb.lsp import serve_stdio
|
|
393
|
+
serve_stdio()
|
|
394
|
+
|
|
395
|
+
|
|
396
396
|
def cmd_mcp(args):
|
|
397
397
|
from pyweb.mcp import serve_stdio
|
|
398
398
|
serve_stdio()
|
|
@@ -410,6 +410,7 @@ def main(argv=None):
|
|
|
410
410
|
p = sub.add_parser("db"); p.add_argument("db_action", choices=["migrate", "new", "status", "rollback"]); p.add_argument("--database", default=None); p.add_argument("--migrations", default="migrations"); p.add_argument("--name", default="migration"); p.add_argument("--steps", type=int, default=1, help="rollback: how many applied migrations to revert"); p.add_argument("--to", default=None, help="rollback: revert everything applied after this label"); p.set_defaults(fn=cmd_db)
|
|
411
411
|
p = sub.add_parser("new"); p.add_argument("name"); p.add_argument("--template", default="counter", choices=["blank", "counter", "todo", "blog", "auth", "chat"], help="starter app"); p.set_defaults(fn=cmd_new)
|
|
412
412
|
p = sub.add_parser("mcp", help="run the MCP server (stdio) for AI assistants"); p.set_defaults(fn=cmd_mcp)
|
|
413
|
+
p = sub.add_parser("lsp", help="run the language server (stdio) for editors"); p.set_defaults(fn=cmd_lsp)
|
|
413
414
|
p = sub.add_parser("check"); p.add_argument("file"); p.set_defaults(fn=cmd_check)
|
|
414
415
|
p = sub.add_parser("npm"); p.add_argument("dts"); p.add_argument("-o", "--out", default=None); p.set_defaults(fn=cmd_npm)
|
|
415
416
|
p = sub.add_parser("deploy")
|