pyweb-stack 0.3.0__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.
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/LICENSE +1 -1
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/PKG-INFO +28 -12
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/README.md +26 -10
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyproject.toml +1 -2
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/__init__.py +10 -3
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/ai/guide.md +46 -4
- pyweb_stack-0.4.0/pyweb/app.py +78 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/app_loader.py +137 -28
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/asgi.py +1 -1
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/browser.py +25 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/build.py +54 -55
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cli/__init__.py +46 -6
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/ast.py +14 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/lower.py +291 -34
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/pipeline.py +65 -8
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/pyjs.py +45 -2
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/rpc.py +2 -1
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/context.py +25 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/db/__init__.py +37 -0
- pyweb_stack-0.3.0/pyweb/npm.py → pyweb_stack-0.4.0/pyweb/dts.py +8 -9
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/hosting.py +7 -5
- pyweb_stack-0.4.0/pyweb/livedata.py +338 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/lsp.py +53 -4
- pyweb_stack-0.4.0/pyweb/markdown.py +254 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/mcp.py +21 -5
- pyweb_stack-0.4.0/pyweb/packages.py +748 -0
- pyweb_stack-0.4.0/pyweb/runtime/browser/markdown.js +195 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/runtime/browser/runtime.js +483 -51
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/runtime/server/__init__.py +160 -4
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/serve.py +18 -5
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/ssr.py +110 -11
- pyweb_stack-0.4.0/pyweb/templates/ai-chat.pyweb +180 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/app.css +18 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/testing.py +15 -1
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/PKG-INFO +28 -12
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/SOURCES.txt +12 -2
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_dts.py +5 -8
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_forms_security_obs.py +3 -4
- pyweb_stack-0.4.0/tests/test_livedata.py +204 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_lsp.py +17 -0
- pyweb_stack-0.4.0/tests/test_markdown.py +81 -0
- pyweb_stack-0.4.0/tests/test_multipage.py +326 -0
- pyweb_stack-0.4.0/tests/test_packages.py +304 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_plugins_platform_bench.py +1 -1
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_production_gaps.py +0 -15
- pyweb_stack-0.4.0/tests/test_streaming.py +276 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_sync_live.py +1 -1
- pyweb_stack-0.3.0/pyweb/app.py +0 -37
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/auth.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/bench.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cache.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cli/__main__.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/__init__.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/codegen/__init__.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/codegen/ir.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/errors.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/parser.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/css.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/db/migrate.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/decorators.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/deploy.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/forms.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/jobs.py +0 -0
- /pyweb_stack-0.3.0/pyweb/live.py → /pyweb_stack-0.4.0/pyweb/livetable.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/models.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/observability.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/platform.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/plugins.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/py.typed +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/realtime.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/rpc.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/security.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/sync.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/auth.pyweb +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/blog.pyweb +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/chat.pyweb +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/counter.pyweb +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/todo.pyweb +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/uploads.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/requires.txt +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/top_level.txt +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/setup.cfg +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_contract.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_security.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_v1.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_backplane.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_browser_build.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_budget.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_cli_deploy.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_codegen.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_compiler.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_db_production.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_deploy_upload_render.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_docs.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_e2e.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_examples.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_jobs_cache_realtime.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_mcp.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_models_db.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_multifile.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_observability.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_parser.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_parser_v1.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_pyjs_semantics.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_reactivity.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_realtime_transport.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_reference_apps.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_rpc_placement.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_rpc_production.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_runtime_signals.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_server_db.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_static_hashing.py +0 -0
- {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_website.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyweb-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Full-stack web apps in one Python file: server-rendered pages, reactive browser UI, typed RPC.
|
|
5
|
-
Author-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com
|
|
5
|
+
Author-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>
|
|
6
6
|
Maintainer-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>
|
|
7
7
|
License: MIT
|
|
8
8
|
Project-URL: Homepage, https://maanavkrishna.github.io/PyWeb/
|
|
@@ -123,11 +123,20 @@ pyweb dev app.pyweb # http://localhost:8000
|
|
|
123
123
|
- **Boundaries are checked.** Database handles, imports and secrets can't
|
|
124
124
|
leak into browser code: it's a compile error with a line number.
|
|
125
125
|
`pyweb inspect` explains where every name runs and why.
|
|
126
|
+
- **Pages that keep up.** `live(db, "select ...")` keeps page data in
|
|
127
|
+
step with the database in every open window; `@server` functions that
|
|
128
|
+
`yield` stream to the browser (AI replies included, with a Stop
|
|
129
|
+
button that really stops).
|
|
130
|
+
- **Real multi-page apps.** Shared layouts that keep their state, links
|
|
131
|
+
that load without a full reload, typed query parameters, per-page
|
|
132
|
+
titles and social tags, error pages written in `.pyweb`.
|
|
133
|
+
- **npm without Node.** `pyweb add chart.js/auto` vendors a package for
|
|
134
|
+
browser code; no `node_modules`, no bundler.
|
|
126
135
|
- **Stateless servers.** Signed-cookie sessions and plain HTTP RPC scale
|
|
127
136
|
horizontally behind any load balancer. Deploy with `pyweb serve`,
|
|
128
137
|
uvicorn/gunicorn (ASGI) or the generated Dockerfile.
|
|
129
138
|
|
|
130
|
-
A typical interactive page ships under 1 KB of page code plus a ~
|
|
139
|
+
A typical interactive page ships under 1 KB of page code plus a ~14 KB
|
|
131
140
|
(gzip) runtime that's cached across pages. Pages without interactivity
|
|
132
141
|
ship no JavaScript.
|
|
133
142
|
|
|
@@ -137,9 +146,9 @@ ship no JavaScript.
|
|
|
137
146
|
small SaaS products and content sites with interactive parts, built by
|
|
138
147
|
people who'd rather stay in Python.
|
|
139
148
|
|
|
140
|
-
**Not a fit:** large client-heavy single-page apps
|
|
141
|
-
|
|
142
|
-
browser (use Pyodide/PyScript). See the
|
|
149
|
+
**Not a fit:** large client-heavy single-page apps built around a
|
|
150
|
+
JavaScript component framework (use React/Svelte/Vue), or running
|
|
151
|
+
scientific Python in the browser (use Pyodide/PyScript). See the
|
|
143
152
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
144
153
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
145
154
|
|
|
@@ -168,10 +177,10 @@ See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
|
168
177
|
| | |
|
|
169
178
|
|---|---|
|
|
170
179
|
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) · [AI assistants & MCP](docs/17-ai-assistants.md) |
|
|
171
|
-
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
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) |
|
|
180
|
+
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) · [npm packages](docs/18-npm-packages.md) |
|
|
181
|
+
| Server | [Server functions & RPC](docs/06-server-functions.md) · [Pages & routing](docs/08-pages-routing-assets.md) · [Layouts & navigation](docs/19-layouts-navigation.md) · [Data](docs/09-data.md) · [Live data](docs/21-live-data.md) · [Building AI apps](docs/20-ai-apps.md) · [Auth](docs/10-auth.md) |
|
|
173
182
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
174
|
-
| Reference | [Toolkit & stability](docs/15-toolkit.md) · [Limitations & roadmap](docs/16-limitations-roadmap.md) · [Architecture](ARCHITECTURE.md) · [Changelog](CHANGELOG.md) |
|
|
183
|
+
| Reference | [Recipe: wallets & web3](docs/22-recipe-web3.md) · [Toolkit & stability](docs/15-toolkit.md) · [Limitations & roadmap](docs/16-limitations-roadmap.md) · [Architecture](ARCHITECTURE.md) · [Changelog](CHANGELOG.md) |
|
|
175
184
|
|
|
176
185
|
The same docs are published at
|
|
177
186
|
**[maanavkrishna.github.io/PyWeb](https://maanavkrishna.github.io/PyWeb/)**,
|
|
@@ -190,11 +199,15 @@ a real browser by the test suite.
|
|
|
190
199
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
191
200
|
| [`chat`](examples/chat/app.pyweb) | route params, shared server state, live updates with `publish`/`subscribe` |
|
|
192
201
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
202
|
+
| [`site`](examples/site/app.pyweb) | a layout, client-side navigation, query parameters, page titles, a 404 page |
|
|
203
|
+
| [`dashboard`](examples/dashboard/app.pyweb) | live queries and a Chart.js chart from npm, in every open window |
|
|
204
|
+
| [`ai-chat`](examples/ai-chat/app.pyweb) | streaming AI replies with Stop and Markdown (Anthropic, OpenAI-compatible or a demo model) |
|
|
193
205
|
|
|
194
206
|
## Command line
|
|
195
207
|
|
|
196
208
|
```bash
|
|
197
|
-
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
209
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat|ai-chat)
|
|
210
|
+
pyweb add chart.js/auto # an npm package for browser code (no Node.js)
|
|
198
211
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
199
212
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
200
213
|
pyweb check app.pyweb # compile + security checks for CI
|
|
@@ -218,9 +231,12 @@ and changes to them are announced in the [changelog](CHANGELOG.md) (see
|
|
|
218
231
|
|
|
219
232
|
| Version | Highlights |
|
|
220
233
|
|---|---|
|
|
234
|
+
| 0.4 | npm packages without Node, layouts and client-side navigation, streaming server functions and `<Markdown>` for AI apps, live queries, page head tags, `.pyweb` error pages |
|
|
221
235
|
| 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
|
|
222
236
|
| 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 |
|
|
237
|
+
| 0.1 | First public release: compiler, reactive runtime, server rendering, typed RPC, sessions, databases, CLI |
|
|
238
|
+
|
|
239
|
+
The test suite covers the
|
|
224
240
|
parser, the Python→JavaScript translation (differentially, against
|
|
225
241
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
226
242
|
example app in Chromium, and the database/Redis layers against real
|
|
@@ -228,7 +244,7 @@ Postgres, MySQL and Redis servers, on Python 3.10–3.13.
|
|
|
228
244
|
|
|
229
245
|
## Authors
|
|
230
246
|
|
|
231
|
-
Built by [MaanavKrishna](https://github.com/MaanavKrishna)
|
|
247
|
+
Built by [MaanavKrishna](https://github.com/MaanavKrishna).
|
|
232
248
|
|
|
233
249
|
## Contributing
|
|
234
250
|
|
|
@@ -69,11 +69,20 @@ pyweb dev app.pyweb # http://localhost:8000
|
|
|
69
69
|
- **Boundaries are checked.** Database handles, imports and secrets can't
|
|
70
70
|
leak into browser code: it's a compile error with a line number.
|
|
71
71
|
`pyweb inspect` explains where every name runs and why.
|
|
72
|
+
- **Pages that keep up.** `live(db, "select ...")` keeps page data in
|
|
73
|
+
step with the database in every open window; `@server` functions that
|
|
74
|
+
`yield` stream to the browser (AI replies included, with a Stop
|
|
75
|
+
button that really stops).
|
|
76
|
+
- **Real multi-page apps.** Shared layouts that keep their state, links
|
|
77
|
+
that load without a full reload, typed query parameters, per-page
|
|
78
|
+
titles and social tags, error pages written in `.pyweb`.
|
|
79
|
+
- **npm without Node.** `pyweb add chart.js/auto` vendors a package for
|
|
80
|
+
browser code; no `node_modules`, no bundler.
|
|
72
81
|
- **Stateless servers.** Signed-cookie sessions and plain HTTP RPC scale
|
|
73
82
|
horizontally behind any load balancer. Deploy with `pyweb serve`,
|
|
74
83
|
uvicorn/gunicorn (ASGI) or the generated Dockerfile.
|
|
75
84
|
|
|
76
|
-
A typical interactive page ships under 1 KB of page code plus a ~
|
|
85
|
+
A typical interactive page ships under 1 KB of page code plus a ~14 KB
|
|
77
86
|
(gzip) runtime that's cached across pages. Pages without interactivity
|
|
78
87
|
ship no JavaScript.
|
|
79
88
|
|
|
@@ -83,9 +92,9 @@ ship no JavaScript.
|
|
|
83
92
|
small SaaS products and content sites with interactive parts, built by
|
|
84
93
|
people who'd rather stay in Python.
|
|
85
94
|
|
|
86
|
-
**Not a fit:** large client-heavy single-page apps
|
|
87
|
-
|
|
88
|
-
browser (use Pyodide/PyScript). See the
|
|
95
|
+
**Not a fit:** large client-heavy single-page apps built around a
|
|
96
|
+
JavaScript component framework (use React/Svelte/Vue), or running
|
|
97
|
+
scientific Python in the browser (use Pyodide/PyScript). See the
|
|
89
98
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
90
99
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
91
100
|
|
|
@@ -114,10 +123,10 @@ See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
|
114
123
|
| | |
|
|
115
124
|
|---|---|
|
|
116
125
|
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) · [AI assistants & MCP](docs/17-ai-assistants.md) |
|
|
117
|
-
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
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) |
|
|
126
|
+
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) · [npm packages](docs/18-npm-packages.md) |
|
|
127
|
+
| Server | [Server functions & RPC](docs/06-server-functions.md) · [Pages & routing](docs/08-pages-routing-assets.md) · [Layouts & navigation](docs/19-layouts-navigation.md) · [Data](docs/09-data.md) · [Live data](docs/21-live-data.md) · [Building AI apps](docs/20-ai-apps.md) · [Auth](docs/10-auth.md) |
|
|
119
128
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
120
|
-
| Reference | [Toolkit & stability](docs/15-toolkit.md) · [Limitations & roadmap](docs/16-limitations-roadmap.md) · [Architecture](ARCHITECTURE.md) · [Changelog](CHANGELOG.md) |
|
|
129
|
+
| Reference | [Recipe: wallets & web3](docs/22-recipe-web3.md) · [Toolkit & stability](docs/15-toolkit.md) · [Limitations & roadmap](docs/16-limitations-roadmap.md) · [Architecture](ARCHITECTURE.md) · [Changelog](CHANGELOG.md) |
|
|
121
130
|
|
|
122
131
|
The same docs are published at
|
|
123
132
|
**[maanavkrishna.github.io/PyWeb](https://maanavkrishna.github.io/PyWeb/)**,
|
|
@@ -136,11 +145,15 @@ a real browser by the test suite.
|
|
|
136
145
|
| [`auth`](examples/auth/app.pyweb) | registration, password hashing, sessions, protected pages |
|
|
137
146
|
| [`chat`](examples/chat/app.pyweb) | route params, shared server state, live updates with `publish`/`subscribe` |
|
|
138
147
|
| [`showcase`](examples/showcase/app.pyweb) | everything on one page, with a stylesheet |
|
|
148
|
+
| [`site`](examples/site/app.pyweb) | a layout, client-side navigation, query parameters, page titles, a 404 page |
|
|
149
|
+
| [`dashboard`](examples/dashboard/app.pyweb) | live queries and a Chart.js chart from npm, in every open window |
|
|
150
|
+
| [`ai-chat`](examples/ai-chat/app.pyweb) | streaming AI replies with Stop and Markdown (Anthropic, OpenAI-compatible or a demo model) |
|
|
139
151
|
|
|
140
152
|
## Command line
|
|
141
153
|
|
|
142
154
|
```bash
|
|
143
|
-
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
155
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat|ai-chat)
|
|
156
|
+
pyweb add chart.js/auto # an npm package for browser code (no Node.js)
|
|
144
157
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
145
158
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
146
159
|
pyweb check app.pyweb # compile + security checks for CI
|
|
@@ -164,9 +177,12 @@ and changes to them are announced in the [changelog](CHANGELOG.md) (see
|
|
|
164
177
|
|
|
165
178
|
| Version | Highlights |
|
|
166
179
|
|---|---|
|
|
180
|
+
| 0.4 | npm packages without Node, layouts and client-side navigation, streaming server functions and `<Markdown>` for AI apps, live queries, page head tags, `.pyweb` error pages |
|
|
167
181
|
| 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
|
|
168
182
|
| 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 |
|
|
183
|
+
| 0.1 | First public release: compiler, reactive runtime, server rendering, typed RPC, sessions, databases, CLI |
|
|
184
|
+
|
|
185
|
+
The test suite covers the
|
|
170
186
|
parser, the Python→JavaScript translation (differentially, against
|
|
171
187
|
CPython), the reactive runtime, server rendering, RPC, sessions, every
|
|
172
188
|
example app in Chromium, and the database/Redis layers against real
|
|
@@ -174,7 +190,7 @@ Postgres, MySQL and Redis servers, on Python 3.10–3.13.
|
|
|
174
190
|
|
|
175
191
|
## Authors
|
|
176
192
|
|
|
177
|
-
Built by [MaanavKrishna](https://github.com/MaanavKrishna)
|
|
193
|
+
Built by [MaanavKrishna](https://github.com/MaanavKrishna).
|
|
178
194
|
|
|
179
195
|
## Contributing
|
|
180
196
|
|
|
@@ -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.4.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" }
|
|
@@ -12,7 +12,6 @@ requires-python = ">=3.10"
|
|
|
12
12
|
dependencies = []
|
|
13
13
|
authors = [
|
|
14
14
|
{ name = "MaanavKrishna", email = "67054795+MaanavKrishna@users.noreply.github.com" },
|
|
15
|
-
{ name = "Claude", email = "noreply@anthropic.com" },
|
|
16
15
|
]
|
|
17
16
|
maintainers = [{ name = "MaanavKrishna", email = "67054795+MaanavKrishna@users.noreply.github.com" }]
|
|
18
17
|
keywords = ["web", "framework", "full-stack", "reactive", "rpc", "ssr", "compiler"]
|
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
from .app import App
|
|
4
4
|
from .models import Email, Model
|
|
5
5
|
from . import auth, cache, jobs, realtime, security, observability, forms, testing
|
|
6
|
-
from . import sync, deploy, uploads, lsp,
|
|
6
|
+
from . import sync, deploy, uploads, lsp, livetable
|
|
7
7
|
from . import browser as browser_api
|
|
8
8
|
from . import build, css, plugins, platform
|
|
9
9
|
from .jobs import task
|
|
10
|
-
from .context import NotFound, redirect, request, session
|
|
10
|
+
from .context import NotFound, head, redirect, request, session
|
|
11
11
|
from .rpc import RPCError
|
|
12
12
|
from .decorators import component, edge, server, worker
|
|
13
13
|
from .realtime import channel, publish, subscribe
|
|
14
|
+
from .packages import npm
|
|
15
|
+
from .markdown import Markdown
|
|
16
|
+
from .livedata import live
|
|
14
17
|
|
|
15
18
|
__all__ = [
|
|
16
19
|
"App",
|
|
17
20
|
"request",
|
|
18
21
|
"session",
|
|
19
22
|
"redirect",
|
|
23
|
+
"head",
|
|
20
24
|
"NotFound",
|
|
21
25
|
"RPCError",
|
|
22
26
|
"component",
|
|
@@ -26,6 +30,9 @@ __all__ = [
|
|
|
26
30
|
"channel",
|
|
27
31
|
"publish",
|
|
28
32
|
"subscribe",
|
|
33
|
+
"npm",
|
|
34
|
+
"Markdown",
|
|
35
|
+
"live",
|
|
29
36
|
"Email",
|
|
30
37
|
"Model",
|
|
31
38
|
"task",
|
|
@@ -38,7 +45,7 @@ __all__ = [
|
|
|
38
45
|
"forms",
|
|
39
46
|
"testing",
|
|
40
47
|
"sync",
|
|
41
|
-
"
|
|
48
|
+
"livetable",
|
|
42
49
|
"deploy",
|
|
43
50
|
"uploads",
|
|
44
51
|
"lsp",
|
|
@@ -171,8 +171,36 @@ def Item(item_id: int):
|
|
|
171
171
|
- Database: `from pyweb.db import connect; db = connect("sqlite:///app.db")`;
|
|
172
172
|
`db.execute("select ... where id = ?", (x,)).dicts()`; always use `?`
|
|
173
173
|
parameters; `with db.transaction(): ...`.
|
|
174
|
-
-
|
|
175
|
-
|
|
174
|
+
- Query parameters: page params not in the route come from the query
|
|
175
|
+
string, typed by annotation: `def Search(q: str = "", page: int = 1)`.
|
|
176
|
+
Missing required / bad values -> 400.
|
|
177
|
+
- Layouts: `@app.layout` (or `@app.layout("/admin")`) on a function with
|
|
178
|
+
markup containing `{children}` once wraps every page under the prefix;
|
|
179
|
+
`@app.page(..., layout=None)` opts out. Links between pages load without
|
|
180
|
+
a full reload and keep the layout's state; mark nothing yourself: links
|
|
181
|
+
to the current page get `aria-current="page"` automatically.
|
|
182
|
+
- In handlers, navigate with `navigate("/path")` (`from pyweb.browser import navigate`).
|
|
183
|
+
- Head tags: `@app.page("/", title=..., description=..., image=...)`, or
|
|
184
|
+
`head(title=..., description=...)` in the page body (server). `App(base_url=...)`
|
|
185
|
+
adds canonical URLs.
|
|
186
|
+
- Error pages: `@app.error(404)` on a page function taking `path` (and/or
|
|
187
|
+
`status`, `message`, `request_id`).
|
|
188
|
+
- Run code after load with a handler named `on_mount`; stop timers in
|
|
189
|
+
`on_unmount` (runs when the user navigates away). React to a value
|
|
190
|
+
changing with `watch(lambda: value, handler)` in `on_mount`
|
|
191
|
+
(`from pyweb.browser import watch`).
|
|
192
|
+
- Live data: `rows = live(db, "select ... where x = ?", (x,))` in a page
|
|
193
|
+
(`from pyweb import live`) renders the rows and keeps them current in
|
|
194
|
+
every open page when the tables are written through `pyweb.db`. No
|
|
195
|
+
publish/subscribe needed. Use `db.notify("table")` after writes made
|
|
196
|
+
outside `pyweb.db`. Keep live queries small (`LIMIT`).
|
|
197
|
+
- Streaming (AI replies, progress): a `@server` function that `yield`s;
|
|
198
|
+
in an `async def` handler `stream = fn(...)` then
|
|
199
|
+
`async for piece in stream: ...`; `stream.cancel()` stops it (closes the
|
|
200
|
+
generator on the server). Show model output with `<Markdown text={reply} />`
|
|
201
|
+
(`from pyweb import Markdown`): safe, renders as it streams. Template:
|
|
202
|
+
`pyweb new NAME --template ai-chat`. Keep API keys in server code
|
|
203
|
+
(`os.environ`), never in page variables.
|
|
176
204
|
- Live updates: in a server function `publish("room:1", data)`; in the
|
|
177
205
|
page `feed = channel("room:1")` (runs on the server); in `on_mount`
|
|
178
206
|
`subscribe(feed, handler)`, where `handler(message)` assigns page
|
|
@@ -192,7 +220,16 @@ JS globals are available directly: `window`, `document`, `localStorage`,
|
|
|
192
220
|
|
|
193
221
|
Not supported in browser code: classes, imports, `with`, generators,
|
|
194
222
|
walrus, `*args/**kwargs` parameters, slice assignment, keyword arguments to
|
|
195
|
-
|
|
223
|
+
browser globals, server-only names. Move such code into `@server` functions.
|
|
224
|
+
|
|
225
|
+
npm packages (no Node.js needed): run `pyweb add chart.js/auto` in the app
|
|
226
|
+
folder (writes `pyweb.lock` and `static/vendor/`; commit both), then bind at
|
|
227
|
+
module level with literal strings: `Chart = npm("chart.js/auto")`,
|
|
228
|
+
`Gauge = npm("pkg", "Gauge")` (named export), `lib = npm("pkg", "*")` (whole
|
|
229
|
+
module). Calling a class constructs it (`new`); keyword arguments become one
|
|
230
|
+
options object. npm names work only in browser code (handlers, `on_mount`,
|
|
231
|
+
lambdas), never directly in markup. Give libraries an element with
|
|
232
|
+
`ref={el}` (declare `el = None`; it is set before `on_mount`).
|
|
196
233
|
|
|
197
234
|
## 8. Errors and fixes
|
|
198
235
|
|
|
@@ -207,11 +244,16 @@ JS functions, server-only names. Move such code into `@server` functions.
|
|
|
207
244
|
| `unknown component <X>` | Define `def X(...)` with markup (capitalized) in this file, or import it: `from widgets import X`. |
|
|
208
245
|
| `mismatched </tag>` / `is never closed` | Close every tag; void tags (`input`, `img`, `br`) need no close (`<input ... />`). |
|
|
209
246
|
| `... is not supported in browser code` | Rewrite with supported constructs or move it to `@server`. |
|
|
247
|
+
| `npm package 'x' isn't installed` | Run `pyweb add x` in the app folder (the folder with `pyweb.lock`). |
|
|
248
|
+
| `uses X from npm(...), which only exists in the browser` | Use the package in a handler or `on_mount` and store the result in a page variable that markup shows. |
|
|
249
|
+
| `layout ... has no {children}` | Put `{children}` exactly once in the layout's markup where pages go. |
|
|
250
|
+
| `isn't a valid int` / `missing ?name=` (400) | Give the query parameter a default, or link with a valid value. |
|
|
251
|
+
| `pyweb add`: `is CommonJS` / `imports the Node.js module` | Pick an ES-module browser package (e.g. `lodash-es`), or do the work in an `@server` function. |
|
|
210
252
|
|
|
211
253
|
## 9. Workflow for agents
|
|
212
254
|
|
|
213
255
|
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.
|
|
256
|
+
`pyweb_new_app` MCP tool). Templates: blank, counter, todo, blog, auth, chat, ai-chat.
|
|
215
257
|
2. Edit `app.pyweb`. After every edit run `pyweb check app.pyweb`
|
|
216
258
|
(MCP: `pyweb_check`) and fix errors by line number.
|
|
217
259
|
3. Use `pyweb inspect` (MCP: `pyweb_inspect`) to confirm what runs in the
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Application object: page registry and configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class App:
|
|
7
|
+
"""The app: its pages, layouts, error pages and site-wide settings.
|
|
8
|
+
|
|
9
|
+
``base_url`` (e.g. ``"https://example.com"``) gives every page a
|
|
10
|
+
canonical URL and absolute social-card links; ``description`` and
|
|
11
|
+
``image`` are the defaults for pages that don't set their own.
|
|
12
|
+
``client_nav=False`` makes links do full page loads.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, database=None, cache=None, auth=None, plugins=(), *,
|
|
16
|
+
title="PyWeb", stylesheets=(), lang="en", base_url=None, description=None,
|
|
17
|
+
image=None, client_nav=True):
|
|
18
|
+
self.title = title
|
|
19
|
+
self.stylesheets = list(stylesheets)
|
|
20
|
+
self.lang = lang
|
|
21
|
+
self.base_url = base_url.rstrip("/") if base_url else None
|
|
22
|
+
self.description = description
|
|
23
|
+
self.image = image
|
|
24
|
+
self.client_nav = client_nav
|
|
25
|
+
self.database = database
|
|
26
|
+
self.cache = cache
|
|
27
|
+
self.auth = auth
|
|
28
|
+
self.pages: list[tuple[str, str, dict]] = []
|
|
29
|
+
self.layouts: list[tuple[str, str]] = []
|
|
30
|
+
self.errors: dict[int, str] = {}
|
|
31
|
+
from pyweb.plugins import Registry
|
|
32
|
+
self.plugins = Registry()
|
|
33
|
+
for plugin in plugins:
|
|
34
|
+
self.use(plugin)
|
|
35
|
+
|
|
36
|
+
def page(self, route, *, title=None, render="server", description=None, image=None, canonical=None,
|
|
37
|
+
noindex=False, layout=...):
|
|
38
|
+
"""Register a page at ``route``. ``{name}`` segments and other parameters of the
|
|
39
|
+
function (from the query string) become its arguments."""
|
|
40
|
+
def deco(fn):
|
|
41
|
+
self.pages.append((route, fn.__name__, {"render": render, "title": title}))
|
|
42
|
+
fn.__pyweb_route__ = route
|
|
43
|
+
fn.__pyweb_title__ = title
|
|
44
|
+
fn.__pyweb_render__ = render
|
|
45
|
+
return fn
|
|
46
|
+
|
|
47
|
+
return deco
|
|
48
|
+
|
|
49
|
+
def layout(self, prefix="/"):
|
|
50
|
+
"""Wrap every page under ``prefix`` in this function's markup (``{children}`` is the page).
|
|
51
|
+
|
|
52
|
+
Use as ``@app.layout`` or ``@app.layout("/admin")``.
|
|
53
|
+
"""
|
|
54
|
+
if callable(prefix):
|
|
55
|
+
self.layouts.append(("/", prefix.__name__))
|
|
56
|
+
return prefix
|
|
57
|
+
|
|
58
|
+
def deco(fn):
|
|
59
|
+
self.layouts.append((prefix, fn.__name__))
|
|
60
|
+
return fn
|
|
61
|
+
|
|
62
|
+
return deco
|
|
63
|
+
|
|
64
|
+
def error(self, status, *, title=None, layout=...):
|
|
65
|
+
"""Render this page for HTTP ``status`` (404, 500, ...) instead of the built-in one."""
|
|
66
|
+
def deco(fn):
|
|
67
|
+
self.errors[status] = fn.__name__
|
|
68
|
+
return fn
|
|
69
|
+
|
|
70
|
+
return deco
|
|
71
|
+
|
|
72
|
+
def use(self, plugin):
|
|
73
|
+
"""Attach a :class:`pyweb.plugins.Plugin`; merges its routes."""
|
|
74
|
+
self.plugins.add(plugin)
|
|
75
|
+
for route, fn in plugin.routes:
|
|
76
|
+
self.pages.append((route, fn.__name__, {"render": "server",
|
|
77
|
+
"plugin": plugin.name}))
|
|
78
|
+
return plugin
|
|
@@ -13,11 +13,12 @@ import inspect
|
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
15
|
import types
|
|
16
|
+
import typing
|
|
16
17
|
|
|
17
18
|
from .compiler import compile_source
|
|
18
19
|
from .compiler import parser as P
|
|
19
20
|
from .compiler.lower import is_ui_stmt
|
|
20
|
-
from .context import NotFound, Redirect
|
|
21
|
+
from .context import BadRequest, NotFound, Redirect, _page_head
|
|
21
22
|
from .ssr import NS_KEY, Renderer, page_html
|
|
22
23
|
|
|
23
24
|
STATE_PREFIX = "__pyweb_state_"
|
|
@@ -66,6 +67,7 @@ class LoadedApp:
|
|
|
66
67
|
self.files += [lib.path for lib in self.libraries]
|
|
67
68
|
infos = [p["info"] for p in self.compiled["pages"].values()]
|
|
68
69
|
infos += list(self.compiled["components"].values())
|
|
70
|
+
infos += [lay["info"] for lay in self.compiled.get("layouts", {}).values()]
|
|
69
71
|
self.module = self._exec(source, self.filename, infos)
|
|
70
72
|
self.rpc = {}
|
|
71
73
|
for spec in self.compiled["rpc"]:
|
|
@@ -80,6 +82,9 @@ class LoadedApp:
|
|
|
80
82
|
app_obj = next((v for v in self.module.__dict__.values()
|
|
81
83
|
if type(v).__name__ == "App" and type(v).__module__.startswith("pyweb")), None)
|
|
82
84
|
self.app = app_obj
|
|
85
|
+
if self.ctx.live_names or any(lib.ctx.live_names for lib in self.libraries):
|
|
86
|
+
from . import livedata
|
|
87
|
+
livedata.enable() # announce table writes from this process, even before a live() call
|
|
83
88
|
self.title = getattr(app_obj, "title", None) or "PyWeb"
|
|
84
89
|
self.stylesheets = list(getattr(app_obj, "stylesheets", []) or []) + self.extra_stylesheets
|
|
85
90
|
self.lang = getattr(app_obj, "lang", "en") or "en"
|
|
@@ -123,48 +128,152 @@ class LoadedApp:
|
|
|
123
128
|
env[NS_KEY] = ctx
|
|
124
129
|
return info.ui, env
|
|
125
130
|
|
|
126
|
-
def renderer(self):
|
|
127
|
-
return Renderer(self.module.__dict__, component_state=self._component_state, strict=True)
|
|
131
|
+
def renderer(self, path=None):
|
|
132
|
+
return Renderer(self.module.__dict__, component_state=self._component_state, strict=True, path=path)
|
|
128
133
|
|
|
129
|
-
def coerce_params(self, page_name, raw):
|
|
134
|
+
def coerce_params(self, page_name, raw, query=None):
|
|
135
|
+
"""Arguments for page ``page_name``: route segments from ``raw`` (bad values are a 404) and,
|
|
136
|
+
when ``query`` (``{name: [values]}``) is given, the other parameters from the query string
|
|
137
|
+
(missing or bad values are a 400)."""
|
|
130
138
|
fn = self.module.__dict__[STATE_PREFIX + page_name]
|
|
131
139
|
out = {}
|
|
132
140
|
for pname, param in inspect.signature(fn).parameters.items():
|
|
133
|
-
if pname not in raw:
|
|
134
|
-
continue
|
|
135
141
|
ann = param.annotation
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
if pname in raw:
|
|
143
|
+
try:
|
|
144
|
+
out[pname] = _convert(raw[pname], ann)
|
|
145
|
+
except ValueError:
|
|
146
|
+
raise NotFound(f"{pname}={raw[pname]!r}") from None
|
|
147
|
+
elif query is not None and pname in query:
|
|
148
|
+
values = query[pname]
|
|
149
|
+
try:
|
|
150
|
+
out[pname] = [_convert(v, _item_type(ann)) for v in values] if _is_list(ann) \
|
|
151
|
+
else _convert(values[-1], ann)
|
|
152
|
+
except ValueError:
|
|
153
|
+
raise BadRequest(f"?{pname}={values[-1]!r} isn't a valid {_type_name(ann)}") from None
|
|
154
|
+
elif query is not None and param.default is inspect.Parameter.empty:
|
|
155
|
+
raise BadRequest(f"missing ?{pname}=")
|
|
145
156
|
return out
|
|
146
157
|
|
|
147
|
-
def
|
|
148
|
-
"""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
args = self.coerce_params(page_name, params or {})
|
|
153
|
-
result = fn(**args)
|
|
158
|
+
def _layout(self, name, renderer):
|
|
159
|
+
"""Run layout ``name``; returns its page_html entry or a Redirect."""
|
|
160
|
+
lay = self.compiled["layouts"][name]
|
|
161
|
+
fn = self.module.__dict__[STATE_PREFIX + name]
|
|
162
|
+
result = fn(**({"children": None} if "children" in inspect.signature(fn).parameters else {}))
|
|
154
163
|
if isinstance(result, Redirect):
|
|
155
164
|
return result
|
|
156
|
-
if not isinstance(result, dict):
|
|
157
|
-
raise TypeError(f"page {page_name}() returned {type(result).__name__}; pages may only "
|
|
158
|
-
"return redirect(...)")
|
|
159
165
|
env = dict(result)
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
js_url = (self.asset_urls.get(name) or lay["js_url"]) if lay["js"] else None
|
|
167
|
+
return {"name": name, "body": renderer.render(lay["info"].ui, env), "js_url": js_url,
|
|
168
|
+
"state": _state(lay["info"], env), "version": lay["version"]}
|
|
169
|
+
|
|
170
|
+
def render(self, page_name, params=None, *, query=None, path=None, extra=None):
|
|
171
|
+
"""Return ``html`` (str) or a :class:`~pyweb.context.Redirect`.
|
|
172
|
+
|
|
173
|
+
``params`` are route segments, ``query`` the parsed query string
|
|
174
|
+
(``{name: [values]}``), ``path`` the request path (for canonical URLs
|
|
175
|
+
and marking current links), ``extra`` values offered to parameters of
|
|
176
|
+
the same name without conversion (error pages get ``status``, ``path``,
|
|
177
|
+
``message``).
|
|
178
|
+
"""
|
|
179
|
+
page = self.compiled["pages"][page_name]
|
|
180
|
+
info = page["info"]
|
|
181
|
+
fn = self.module.__dict__[STATE_PREFIX + page_name]
|
|
182
|
+
token = _page_head.set({})
|
|
183
|
+
try:
|
|
184
|
+
args = self.coerce_params(page_name, params or {}, query)
|
|
185
|
+
if extra:
|
|
186
|
+
wanted = inspect.signature(fn).parameters
|
|
187
|
+
args.update({k: v for k, v in extra.items() if k in wanted and k not in args})
|
|
188
|
+
renderer = self.renderer(path)
|
|
189
|
+
layouts = []
|
|
190
|
+
for name in page.get("layouts") or ():
|
|
191
|
+
entry = self._layout(name, renderer)
|
|
192
|
+
if isinstance(entry, Redirect):
|
|
193
|
+
return entry
|
|
194
|
+
layouts.append(entry)
|
|
195
|
+
result = fn(**args)
|
|
196
|
+
if isinstance(result, Redirect):
|
|
197
|
+
return result
|
|
198
|
+
if not isinstance(result, dict):
|
|
199
|
+
raise TypeError(f"page {page_name}() returned {type(result).__name__}; pages may only "
|
|
200
|
+
"return redirect(...)")
|
|
201
|
+
env = dict(result)
|
|
202
|
+
body = renderer.render(info.ui, env)
|
|
203
|
+
dynamic = _page_head.get()
|
|
204
|
+
finally:
|
|
205
|
+
_page_head.reset(token)
|
|
206
|
+
state = _state(info, env)
|
|
162
207
|
js_url = self.asset_urls.get(page_name)
|
|
163
208
|
if js_url is None and page["js"]:
|
|
164
209
|
js_url = page["js_url"]
|
|
165
210
|
title = info.title or self.title
|
|
166
211
|
if callable(getattr(self.app, "title_for", None)):
|
|
167
212
|
title = self.app.title_for(page_name) or title
|
|
213
|
+
title = dynamic.pop("title", None) or title
|
|
214
|
+
importmap = dict(page.get("importmap") or {})
|
|
168
215
|
return page_html(name=page_name, title=title, body=body, state=state,
|
|
169
216
|
js_url=js_url if page["js"] else None, css_urls=self.stylesheets,
|
|
170
|
-
lang=self.lang
|
|
217
|
+
lang=self.lang, importmap=importmap, layouts=layouts,
|
|
218
|
+
head=self.head_for(page, dynamic, path),
|
|
219
|
+
nav=getattr(self.app, "client_nav", True) is not False)
|
|
220
|
+
|
|
221
|
+
def head_for(self, page, dynamic, path):
|
|
222
|
+
"""Description, image and canonical URL: the page's own, then ``head()`` values, then the app's."""
|
|
223
|
+
app = self.app
|
|
224
|
+
base = getattr(app, "base_url", None)
|
|
225
|
+
out = {"description": getattr(app, "description", None), "image": getattr(app, "image", None)}
|
|
226
|
+
out.update(page.get("head") or {})
|
|
227
|
+
out.update(dynamic)
|
|
228
|
+
canonical = out.get("canonical")
|
|
229
|
+
if canonical is None and base and path and not page.get("error_status"):
|
|
230
|
+
canonical = path
|
|
231
|
+
if canonical is False:
|
|
232
|
+
canonical = None
|
|
233
|
+
for key, value in (("canonical", canonical), ("image", out.get("image"))):
|
|
234
|
+
if isinstance(value, str) and value.startswith("/") and base:
|
|
235
|
+
value = base + value
|
|
236
|
+
out[key] = value
|
|
237
|
+
return {k: v for k, v in out.items() if v not in (None, False, "")}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _state(info, env):
|
|
241
|
+
"""What the browser gets for a page or layout: its sent variables, plus how to follow live queries."""
|
|
242
|
+
state = {k: env.get(k) for k in info.sent}
|
|
243
|
+
for k in info.sent:
|
|
244
|
+
meta = getattr(env.get(k), "live", None)
|
|
245
|
+
if isinstance(meta, dict):
|
|
246
|
+
state["$live:" + k] = meta
|
|
247
|
+
return state
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _is_list(ann):
|
|
251
|
+
return ann is list or typing.get_origin(ann) is list or (isinstance(ann, str) and ann.startswith("list"))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _item_type(ann):
|
|
255
|
+
if isinstance(ann, str):
|
|
256
|
+
inner = ann[5:-1] if ann.startswith("list[") else "str"
|
|
257
|
+
return inner
|
|
258
|
+
args = typing.get_args(ann)
|
|
259
|
+
return args[0] if args else str
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _type_name(ann):
|
|
263
|
+
return ann if isinstance(ann, str) else getattr(ann, "__name__", str(ann))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _convert(value, ann):
|
|
267
|
+
"""A route or query-string value as the parameter's annotated type (ValueError if it isn't one)."""
|
|
268
|
+
if ann in (int, "int"):
|
|
269
|
+
return int(value)
|
|
270
|
+
if ann in (float, "float"):
|
|
271
|
+
return float(value)
|
|
272
|
+
if ann in (bool, "bool"):
|
|
273
|
+
low = str(value).strip().lower()
|
|
274
|
+
if low in ("1", "true", "yes", "on", ""):
|
|
275
|
+
return True
|
|
276
|
+
if low in ("0", "false", "no", "off"):
|
|
277
|
+
return False
|
|
278
|
+
raise ValueError(value)
|
|
279
|
+
return value
|
|
@@ -83,7 +83,7 @@ def create_app(target="app.pyweb", *, debug=False, max_body=1_048_576, **server_
|
|
|
83
83
|
status, hdrs, body = await asyncio.to_thread(
|
|
84
84
|
site.respond, scope.get("method", "GET"), path, headers, b"".join(chunks))
|
|
85
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
|
|
86
|
+
if hasattr(body, "aiter"): # Server-Sent Events, streamed RPC results
|
|
87
87
|
await send({"type": "http.response.start", "status": status, "headers": raw_headers})
|
|
88
88
|
await _stream(body, receive, send)
|
|
89
89
|
return
|