pyweb-stack 0.1.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.1.0 → pyweb_stack-0.3.0}/PKG-INFO +45 -7
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/README.md +43 -6
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyproject.toml +3 -3
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/__init__.py +4 -0
- pyweb_stack-0.3.0/pyweb/ai/guide.md +227 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/app_loader.py +46 -15
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/asgi.py +25 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/auth.py +25 -3
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/bench.py +31 -7
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/build.py +7 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cli/__init__.py +22 -37
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/lower.py +97 -22
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/pipeline.py +96 -9
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/pyjs.py +3 -1
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/db/__init__.py +1 -1
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/hosting.py +37 -1
- pyweb_stack-0.3.0/pyweb/lsp.py +398 -0
- pyweb_stack-0.3.0/pyweb/mcp.py +693 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/models.py +2 -2
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/realtime.py +192 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/runtime/browser/runtime.js +181 -36
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/runtime/server/__init__.py +38 -31
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/serve.py +5 -0
- {pyweb_stack-0.1.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.3.0/pyweb/templates/auth.pyweb +111 -0
- pyweb_stack-0.3.0/pyweb/templates/blog.pyweb +84 -0
- pyweb_stack-0.3.0/pyweb/templates/chat.pyweb +81 -0
- pyweb_stack-0.3.0/pyweb/templates/counter.pyweb +26 -0
- pyweb_stack-0.3.0/pyweb/templates/todo.pyweb +72 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/testing.py +4 -8
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/PKG-INFO +45 -7
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/SOURCES.txt +10 -1
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/requires.txt +1 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth.py +11 -0
- {pyweb_stack-0.1.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.3.0/tests/test_mcp.py +184 -0
- pyweb_stack-0.3.0/tests/test_multifile.py +182 -0
- {pyweb_stack-0.1.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.1.0 → pyweb_stack-0.3.0}/tests/test_website.py +21 -1
- pyweb_stack-0.1.0/pyweb/lsp.py +0 -214
- pyweb_stack-0.1.0/tests/test_lsp.py +0 -81
- pyweb_stack-0.1.0/tests/test_lsp_state.py +0 -76
- pyweb_stack-0.1.0/tests/test_realtime_transport.py +0 -77
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/LICENSE +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/app.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/browser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cache.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cli/__main__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/ast.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/ir.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/errors.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/parser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/rpc.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/context.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/css.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/db/migrate.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/decorators.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/deploy.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/forms.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/jobs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/live.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/npm.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/observability.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/platform.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/plugins.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/py.typed +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/rpc.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/security.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/sync.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/uploads.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/top_level.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/setup.cfg +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_contract.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_security.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_v1.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_backplane.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_browser_build.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_budget.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_cli_deploy.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_compiler.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_db_production.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_deploy_upload_render.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_docs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_dts.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_e2e.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_examples.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_forms_security_obs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_jobs_cache_realtime.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_models_db.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_observability.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_parser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_parser_v1.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_production_gaps.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_pyjs_semantics.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_reactivity.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_reference_apps.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_rpc_placement.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_rpc_production.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_runtime_signals.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_server_db.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_static_hashing.py +0 -0
- {pyweb_stack-0.1.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>
|
|
@@ -49,6 +49,7 @@ Requires-Dist: uvicorn>=0.30; extra == "all"
|
|
|
49
49
|
Provides-Extra: test
|
|
50
50
|
Requires-Dist: pytest>=8; extra == "test"
|
|
51
51
|
Requires-Dist: playwright>=1.45; extra == "test"
|
|
52
|
+
Requires-Dist: mcp>=1.2; extra == "test"
|
|
52
53
|
Dynamic: license-file
|
|
53
54
|
|
|
54
55
|
# PyWeb
|
|
@@ -63,6 +64,9 @@ functions, without a JavaScript toolchain.
|
|
|
63
64
|

|
|
64
65
|

|
|
65
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
|
+
|
|
66
70
|
```pyweb
|
|
67
71
|
from pyweb import App, server
|
|
68
72
|
|
|
@@ -139,11 +143,31 @@ browser (use Pyodide/PyScript). See the
|
|
|
139
143
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
140
144
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
141
145
|
|
|
146
|
+
## Build it with AI
|
|
147
|
+
|
|
148
|
+
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
149
|
+
render, screenshot and test your app, with errors that come back as line
|
|
150
|
+
numbers and fix hints:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{ "mcpServers": { "pyweb": { "command": "pyweb", "args": ["mcp"] } } }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
(the JSON is for Cursor, Claude Desktop, VS Code and other MCP clients).
|
|
161
|
+
`pyweb new myapp --template todo` also writes `AGENTS.md` and `CLAUDE.md`
|
|
162
|
+
so coding agents follow PyWeb's rules, and the docs site publishes
|
|
163
|
+
[`llms-full.txt`](https://maanavkrishna.github.io/PyWeb/llms-full.txt).
|
|
164
|
+
See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
165
|
+
|
|
142
166
|
## Documentation
|
|
143
167
|
|
|
144
168
|
| | |
|
|
145
169
|
|---|---|
|
|
146
|
-
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) |
|
|
170
|
+
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) · [AI assistants & MCP](docs/17-ai-assistants.md) |
|
|
147
171
|
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
148
172
|
| Server | [Server functions & RPC](docs/06-server-functions.md) · [Pages & routing](docs/08-pages-routing-assets.md) · [Data](docs/09-data.md) · [Auth](docs/10-auth.md) |
|
|
149
173
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
@@ -164,25 +188,39 @@ a real browser by the test suite.
|
|
|
164
188
|
| [`todo`](examples/todo/app.pyweb) | components, list mutation, filters, keyed lists |
|
|
165
189
|
| [`blog`](examples/blog/app.pyweb) | SQL database, server functions, route params, 404s, validation errors |
|
|
166
190
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
167
|
-
| [`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` |
|
|
168
192
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
169
193
|
|
|
170
194
|
## Command line
|
|
171
195
|
|
|
172
196
|
```bash
|
|
173
|
-
pyweb new myapp
|
|
197
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
174
198
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
175
199
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
176
200
|
pyweb check app.pyweb # compile + security checks for CI
|
|
177
201
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
178
202
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
203
|
+
pyweb mcp # MCP server for AI assistants (stdio)
|
|
204
|
+
pyweb lsp # language server for editors (stdio)
|
|
179
205
|
```
|
|
180
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
|
+
|
|
181
211
|
## Status
|
|
182
212
|
|
|
183
|
-
PyWeb 0.
|
|
184
|
-
RPC protocol and CLI are documented and tested,
|
|
185
|
-
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
|
|
186
224
|
parser, the Python→JavaScript translation (differentially, against
|
|
187
225
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
188
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
|
|
|
@@ -86,11 +89,31 @@ browser (use Pyodide/PyScript). See the
|
|
|
86
89
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
87
90
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
88
91
|
|
|
92
|
+
## Build it with AI
|
|
93
|
+
|
|
94
|
+
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
95
|
+
render, screenshot and test your app, with errors that come back as line
|
|
96
|
+
numbers and fix hints:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "mcpServers": { "pyweb": { "command": "pyweb", "args": ["mcp"] } } }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
(the JSON is for Cursor, Claude Desktop, VS Code and other MCP clients).
|
|
107
|
+
`pyweb new myapp --template todo` also writes `AGENTS.md` and `CLAUDE.md`
|
|
108
|
+
so coding agents follow PyWeb's rules, and the docs site publishes
|
|
109
|
+
[`llms-full.txt`](https://maanavkrishna.github.io/PyWeb/llms-full.txt).
|
|
110
|
+
See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
111
|
+
|
|
89
112
|
## Documentation
|
|
90
113
|
|
|
91
114
|
| | |
|
|
92
115
|
|---|---|
|
|
93
|
-
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) |
|
|
116
|
+
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) · [AI assistants & MCP](docs/17-ai-assistants.md) |
|
|
94
117
|
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
95
118
|
| Server | [Server functions & RPC](docs/06-server-functions.md) · [Pages & routing](docs/08-pages-routing-assets.md) · [Data](docs/09-data.md) · [Auth](docs/10-auth.md) |
|
|
96
119
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
@@ -111,25 +134,39 @@ a real browser by the test suite.
|
|
|
111
134
|
| [`todo`](examples/todo/app.pyweb) | components, list mutation, filters, keyed lists |
|
|
112
135
|
| [`blog`](examples/blog/app.pyweb) | SQL database, server functions, route params, 404s, validation errors |
|
|
113
136
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
114
|
-
| [`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` |
|
|
115
138
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
116
139
|
|
|
117
140
|
## Command line
|
|
118
141
|
|
|
119
142
|
```bash
|
|
120
|
-
pyweb new myapp
|
|
143
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
121
144
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
122
145
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
123
146
|
pyweb check app.pyweb # compile + security checks for CI
|
|
124
147
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
125
148
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
149
|
+
pyweb mcp # MCP server for AI assistants (stdio)
|
|
150
|
+
pyweb lsp # language server for editors (stdio)
|
|
126
151
|
```
|
|
127
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
|
+
|
|
128
157
|
## Status
|
|
129
158
|
|
|
130
|
-
PyWeb 0.
|
|
131
|
-
RPC protocol and CLI are documented and tested,
|
|
132
|
-
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
|
|
133
170
|
parser, the Python→JavaScript translation (differentially, against
|
|
134
171
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
135
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" }
|
|
@@ -42,7 +42,7 @@ redis = ["redis>=5"]
|
|
|
42
42
|
crypto = ["cryptography>=42"]
|
|
43
43
|
asgi = ["uvicorn>=0.30"]
|
|
44
44
|
all = ["psycopg[binary]>=3.1", "PyMySQL>=1.1", "redis>=5", "cryptography>=42", "uvicorn>=0.30"]
|
|
45
|
-
test = ["pytest>=8", "playwright>=1.45"]
|
|
45
|
+
test = ["pytest>=8", "playwright>=1.45", "mcp>=1.2"]
|
|
46
46
|
|
|
47
47
|
[project.urls]
|
|
48
48
|
Homepage = "https://maanavkrishna.github.io/PyWeb/"
|
|
@@ -58,7 +58,7 @@ pyweb = "pyweb.cli:main"
|
|
|
58
58
|
include = ["pyweb*"]
|
|
59
59
|
|
|
60
60
|
[tool.setuptools.package-data]
|
|
61
|
-
pyweb = ["runtime/browser/*.js", "py.typed"]
|
|
61
|
+
pyweb = ["runtime/browser/*.js", "py.typed", "ai/*.md", "templates/*.pyweb", "templates/*.css"]
|
|
62
62
|
|
|
63
63
|
[tool.pytest.ini_options]
|
|
64
64
|
testpaths = ["tests"]
|
|
@@ -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",
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# PyWeb guide for AI assistants
|
|
2
|
+
|
|
3
|
+
PyWeb (`pip install pyweb-stack`, `import pyweb`, command `pyweb`) builds a
|
|
4
|
+
full-stack web app from ONE `.pyweb` file: Python plus HTML-like markup.
|
|
5
|
+
Pages render on the server; event handlers compile to JavaScript; functions
|
|
6
|
+
marked `@server` run on the server and are called from the browser over RPC.
|
|
7
|
+
|
|
8
|
+
Follow these rules exactly. When unsure, run the `pyweb_check` tool (or
|
|
9
|
+
`pyweb check app.pyweb`) and fix what it reports.
|
|
10
|
+
|
|
11
|
+
## 1. Skeleton
|
|
12
|
+
|
|
13
|
+
```pyweb
|
|
14
|
+
from pyweb import App, server
|
|
15
|
+
|
|
16
|
+
app = App(title="My app", stylesheets=["/static/app.css"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@server
|
|
20
|
+
def save_item(text: str) -> list: # runs on the server; called over RPC
|
|
21
|
+
ITEMS.append(text)
|
|
22
|
+
return ITEMS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ITEMS = []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.page("/")
|
|
29
|
+
def Home():
|
|
30
|
+
items = list(ITEMS) # computed on the server per request
|
|
31
|
+
draft = "" # browser state (bound below)
|
|
32
|
+
|
|
33
|
+
def add(): # event handler -> compiled to JavaScript
|
|
34
|
+
items = save_item(draft) # server call: awaited automatically
|
|
35
|
+
draft = ""
|
|
36
|
+
|
|
37
|
+
<main>
|
|
38
|
+
<h1>Items ({len(items)})</h1>
|
|
39
|
+
<form onsubmit={add}>
|
|
40
|
+
<input bind={draft} placeholder="New item" />
|
|
41
|
+
<button disabled={not draft.strip()}>Add</button>
|
|
42
|
+
</form>
|
|
43
|
+
<ul>
|
|
44
|
+
for item in items:
|
|
45
|
+
<li>{item}</li>
|
|
46
|
+
</ul>
|
|
47
|
+
</main>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run: `pyweb dev app.pyweb` (http://localhost:8000, live reload).
|
|
51
|
+
Static files go in `static/` next to `app.pyweb`, served at `/static/...`.
|
|
52
|
+
|
|
53
|
+
## 2. What runs where (most important rules)
|
|
54
|
+
|
|
55
|
+
| Code | Runs |
|
|
56
|
+
|---|---|
|
|
57
|
+
| imports, classes, DB connections, module objects | server only |
|
|
58
|
+
| `NAME = <literal>` at module level | both (inlined into browser JS) |
|
|
59
|
+
| `@server` functions | server; browser calls become RPC |
|
|
60
|
+
| undecorated module functions | server; compiled to JS only if browser code calls them |
|
|
61
|
+
| page function body (top to the markup) | server, every request |
|
|
62
|
+
| nested `def` inside a page (handlers) | browser (JavaScript) |
|
|
63
|
+
| `{expressions}` in markup | server (first render) and browser (updates) |
|
|
64
|
+
|
|
65
|
+
Consequences:
|
|
66
|
+
- Handlers must NOT use imports, DB handles, files, `os`, models, or other
|
|
67
|
+
server-only names. Put that work in an `@server` function and call it.
|
|
68
|
+
- Markup expressions must not call `@server` functions. Load data into a
|
|
69
|
+
page variable instead (`rows = load_rows()` at the top of the page).
|
|
70
|
+
- Never put secrets in page variables read by markup or handlers; the
|
|
71
|
+
compiler rejects names like `token`, `secret`, `password`, `api_key`
|
|
72
|
+
that would reach the browser (an empty `password = ""` bound to an input
|
|
73
|
+
is fine).
|
|
74
|
+
- Everything the browser reads (markup, JS, page state) is public.
|
|
75
|
+
Authorize inside `@server` functions with `session.require(...)`.
|
|
76
|
+
|
|
77
|
+
## 3. State
|
|
78
|
+
|
|
79
|
+
Plain local variables in a page are the state. No `useState`, no `nonlocal`.
|
|
80
|
+
|
|
81
|
+
- A variable assigned/mutated in a handler, or used with `bind={x}`, is a
|
|
82
|
+
**signal**: reactive, updating only the DOM that reads it.
|
|
83
|
+
- A variable computed from signals and never assigned in a handler is
|
|
84
|
+
**computed**: `total = price * quantity`.
|
|
85
|
+
- Everything else is a constant.
|
|
86
|
+
- Inside handlers, assigning a page variable updates the page:
|
|
87
|
+
`count += 1`, `draft = ""`, `items.append(x)`, `items[i]["done"] = True`,
|
|
88
|
+
`del items[i]`, `items = [x for x in items if ...]` all work.
|
|
89
|
+
- You cannot assign to a computed value or constant from a handler.
|
|
90
|
+
- Initial values that call functions, read the session, or depend on
|
|
91
|
+
page-level logic are computed on the server; only values browser code
|
|
92
|
+
reads are sent to the browser.
|
|
93
|
+
|
|
94
|
+
## 4. Markup
|
|
95
|
+
|
|
96
|
+
- A line starting with `<tag` is markup; tags may span lines; every
|
|
97
|
+
non-void tag must be closed. Lowercase = HTML, Capitalized = component.
|
|
98
|
+
- `{expr}` inserts a value (None renders nothing). Text is escaped.
|
|
99
|
+
- Attributes: `class="x"` (literal), `href={url}` (expression),
|
|
100
|
+
`disabled={flag}` (True/False toggles), `class={{"done": t["done"]}}`
|
|
101
|
+
(dict of classes), `style={{"color": c}}` (dict of CSS).
|
|
102
|
+
- Events: `onclick={handler}`, `onclick={lambda: remove(item)}`, or
|
|
103
|
+
`onclick={remove(item)}` (runs when clicked). `onsubmit` prevents the
|
|
104
|
+
default submit. Any `on<event>` works: `oninput`, `onchange`, `onkeydown`.
|
|
105
|
+
- Binding: `bind={name}` on input/textarea/select; checkbox binds a bool;
|
|
106
|
+
`type="number"` with a numeric initial value binds a number.
|
|
107
|
+
- Control flow lines inside markup: `for x in xs:`, `if c:`, `elif c:`,
|
|
108
|
+
`else:` with markup bodies indented below.
|
|
109
|
+
- Whitespace between separate lines is dropped (like JSX); keep text that
|
|
110
|
+
needs a space on one line.
|
|
111
|
+
- Literal braces: `{"{"}`.
|
|
112
|
+
|
|
113
|
+
## 5. Components
|
|
114
|
+
|
|
115
|
+
```pyweb
|
|
116
|
+
from pyweb import App, component
|
|
117
|
+
|
|
118
|
+
app = App()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@component
|
|
122
|
+
def Card(title, subtitle="", children=None):
|
|
123
|
+
<section class="card">
|
|
124
|
+
<h2>{title}</h2>
|
|
125
|
+
if subtitle:
|
|
126
|
+
<p>{subtitle}</p>
|
|
127
|
+
{children}
|
|
128
|
+
</section>
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.page("/")
|
|
132
|
+
def Home():
|
|
133
|
+
<Card title="Hello"><p>Body</p></Card>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Props are parameters (defaults = optional). Pass callbacks as props
|
|
137
|
+
(`on_delete={lambda: delete(i)}`) and use them as handlers inside
|
|
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.
|
|
145
|
+
|
|
146
|
+
## 6. Server functions, sessions, routing
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from pyweb import App, RPCError, NotFound, redirect, request, server, session
|
|
150
|
+
|
|
151
|
+
@server
|
|
152
|
+
def update(item_id: int, title: str) -> dict: # annotations validate/coerce args
|
|
153
|
+
user = session.require() # 401 if not signed in
|
|
154
|
+
if not title.strip():
|
|
155
|
+
raise RPCError("validation_error", "Title is required.") # browser: except RPCError as e: str(e)
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
@app.page("/items/{item_id}") # typed route param; bad int -> 404
|
|
159
|
+
def Item(item_id: int):
|
|
160
|
+
if not session.user():
|
|
161
|
+
return redirect("/login")
|
|
162
|
+
row = find(item_id)
|
|
163
|
+
if row is None:
|
|
164
|
+
raise NotFound()
|
|
165
|
+
...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `session.login(user_id, **claims)`, `session.user()`, `session.logout()`,
|
|
169
|
+
`session.require("admin")`.
|
|
170
|
+
- `pyweb.auth.hash_password` / `verify_password` for passwords.
|
|
171
|
+
- Database: `from pyweb.db import connect; db = connect("sqlite:///app.db")`;
|
|
172
|
+
`db.execute("select ... where id = ?", (x,)).dicts()`; always use `?`
|
|
173
|
+
parameters; `with db.transaction(): ...`.
|
|
174
|
+
- In handlers, navigate with `window.location.href = "/path"`.
|
|
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.
|
|
181
|
+
|
|
182
|
+
## 7. Python that compiles to the browser
|
|
183
|
+
|
|
184
|
+
Supported in handlers/markup: literals, f-strings (with format specs),
|
|
185
|
+
arithmetic with Python semantics, comparisons, `in`, `and/or/not` with
|
|
186
|
+
Python truthiness, comprehensions, lambdas, slicing/negative indexes,
|
|
187
|
+
`if/for/while/try/except/raise/return/del`, builtins (`len str int float
|
|
188
|
+
bool abs min max sum round range sorted reversed enumerate zip list dict
|
|
189
|
+
set tuple any all isinstance print`), common str/list/dict/set methods.
|
|
190
|
+
JS globals are available directly: `window`, `document`, `localStorage`,
|
|
191
|
+
`console`, `setTimeout`, `setInterval`, `fetch`, `Math`, `JSON`, `Date`.
|
|
192
|
+
|
|
193
|
+
Not supported in browser code: classes, imports, `with`, generators,
|
|
194
|
+
walrus, `*args/**kwargs` parameters, slice assignment, keyword arguments to
|
|
195
|
+
JS functions, server-only names. Move such code into `@server` functions.
|
|
196
|
+
|
|
197
|
+
## 8. Errors and fixes
|
|
198
|
+
|
|
199
|
+
| Error text contains | Fix |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `only exists on the server` | Move that logic into an `@server` function and call it from the handler. |
|
|
202
|
+
| `is not defined in browser code` | Define it at module level (literal or helper function), pass it in, or use an `@server` function. |
|
|
203
|
+
| `markup expressions must be synchronous` | Assign the server call's result to a page variable or call it in a handler. |
|
|
204
|
+
| `cannot assign to ... derived/read-only` | Assign to a variable the handler owns (make it state), not a computed/constant. |
|
|
205
|
+
| `server secret ... would be sent to the browser` | Keep the value inside `@server` functions; don't read it in markup/handlers. |
|
|
206
|
+
| `bind={x} must name a local variable` | Declare `x = ""` (or a number/bool) in the page before the markup. |
|
|
207
|
+
| `unknown component <X>` | Define `def X(...)` with markup (capitalized) in this file, or import it: `from widgets import X`. |
|
|
208
|
+
| `mismatched </tag>` / `is never closed` | Close every tag; void tags (`input`, `img`, `br`) need no close (`<input ... />`). |
|
|
209
|
+
| `... is not supported in browser code` | Rewrite with supported constructs or move it to `@server`. |
|
|
210
|
+
|
|
211
|
+
## 9. Workflow for agents
|
|
212
|
+
|
|
213
|
+
1. Start from a template: `pyweb new NAME --template todo` (or the
|
|
214
|
+
`pyweb_new_app` MCP tool). Templates: blank, counter, todo, blog, auth, chat.
|
|
215
|
+
2. Edit `app.pyweb`. After every edit run `pyweb check app.pyweb`
|
|
216
|
+
(MCP: `pyweb_check`) and fix errors by line number.
|
|
217
|
+
3. Use `pyweb inspect` (MCP: `pyweb_inspect`) to confirm what runs in the
|
|
218
|
+
browser vs server and what is sent to the browser.
|
|
219
|
+
4. Verify behaviour: render pages (`pyweb_render`) and call server
|
|
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
|
|
225
|
+
`pyweb serve dist` (set `PYWEB_AUTH_SECRET` in production).
|
|
226
|
+
|
|
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)
|