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.
Files changed (116) hide show
  1. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/LICENSE +1 -1
  2. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/PKG-INFO +28 -12
  3. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/README.md +26 -10
  4. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyproject.toml +1 -2
  5. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/__init__.py +10 -3
  6. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/ai/guide.md +46 -4
  7. pyweb_stack-0.4.0/pyweb/app.py +78 -0
  8. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/app_loader.py +137 -28
  9. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/asgi.py +1 -1
  10. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/browser.py +25 -0
  11. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/build.py +54 -55
  12. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cli/__init__.py +46 -6
  13. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/ast.py +14 -0
  14. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/lower.py +291 -34
  15. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/pipeline.py +65 -8
  16. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/pyjs.py +45 -2
  17. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/rpc.py +2 -1
  18. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/context.py +25 -0
  19. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/db/__init__.py +37 -0
  20. pyweb_stack-0.3.0/pyweb/npm.py → pyweb_stack-0.4.0/pyweb/dts.py +8 -9
  21. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/hosting.py +7 -5
  22. pyweb_stack-0.4.0/pyweb/livedata.py +338 -0
  23. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/lsp.py +53 -4
  24. pyweb_stack-0.4.0/pyweb/markdown.py +254 -0
  25. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/mcp.py +21 -5
  26. pyweb_stack-0.4.0/pyweb/packages.py +748 -0
  27. pyweb_stack-0.4.0/pyweb/runtime/browser/markdown.js +195 -0
  28. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/runtime/browser/runtime.js +483 -51
  29. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/runtime/server/__init__.py +160 -4
  30. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/serve.py +18 -5
  31. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/ssr.py +110 -11
  32. pyweb_stack-0.4.0/pyweb/templates/ai-chat.pyweb +180 -0
  33. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/app.css +18 -0
  34. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/testing.py +15 -1
  35. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/PKG-INFO +28 -12
  36. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/SOURCES.txt +12 -2
  37. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_dts.py +5 -8
  38. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_forms_security_obs.py +3 -4
  39. pyweb_stack-0.4.0/tests/test_livedata.py +204 -0
  40. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_lsp.py +17 -0
  41. pyweb_stack-0.4.0/tests/test_markdown.py +81 -0
  42. pyweb_stack-0.4.0/tests/test_multipage.py +326 -0
  43. pyweb_stack-0.4.0/tests/test_packages.py +304 -0
  44. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_plugins_platform_bench.py +1 -1
  45. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_production_gaps.py +0 -15
  46. pyweb_stack-0.4.0/tests/test_streaming.py +276 -0
  47. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_sync_live.py +1 -1
  48. pyweb_stack-0.3.0/pyweb/app.py +0 -37
  49. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/auth.py +0 -0
  50. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/bench.py +0 -0
  51. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cache.py +0 -0
  52. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/cli/__main__.py +0 -0
  53. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/__init__.py +0 -0
  54. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/codegen/__init__.py +0 -0
  55. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/codegen/ir.py +0 -0
  56. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/errors.py +0 -0
  57. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/compiler/parser.py +0 -0
  58. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/css.py +0 -0
  59. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/db/migrate.py +0 -0
  60. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/decorators.py +0 -0
  61. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/deploy.py +0 -0
  62. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/forms.py +0 -0
  63. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/jobs.py +0 -0
  64. /pyweb_stack-0.3.0/pyweb/live.py → /pyweb_stack-0.4.0/pyweb/livetable.py +0 -0
  65. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/models.py +0 -0
  66. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/observability.py +0 -0
  67. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/platform.py +0 -0
  68. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/plugins.py +0 -0
  69. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/py.typed +0 -0
  70. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/realtime.py +0 -0
  71. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/rpc.py +0 -0
  72. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/security.py +0 -0
  73. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/sync.py +0 -0
  74. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/auth.pyweb +0 -0
  75. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/blog.pyweb +0 -0
  76. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/chat.pyweb +0 -0
  77. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/counter.pyweb +0 -0
  78. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/templates/todo.pyweb +0 -0
  79. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb/uploads.py +0 -0
  80. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  81. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
  82. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/requires.txt +0 -0
  83. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/pyweb_stack.egg-info/top_level.txt +0 -0
  84. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/setup.cfg +0 -0
  85. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth.py +0 -0
  86. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_contract.py +0 -0
  87. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_security.py +0 -0
  88. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_auth_v1.py +0 -0
  89. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_backplane.py +0 -0
  90. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_browser_build.py +0 -0
  91. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_budget.py +0 -0
  92. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_cli_deploy.py +0 -0
  93. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_codegen.py +0 -0
  94. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_compiler.py +0 -0
  95. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_db_production.py +0 -0
  96. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_deploy_upload_render.py +0 -0
  97. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_docs.py +0 -0
  98. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_e2e.py +0 -0
  99. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_examples.py +0 -0
  100. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_jobs_cache_realtime.py +0 -0
  101. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_mcp.py +0 -0
  102. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_models_db.py +0 -0
  103. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_multifile.py +0 -0
  104. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_observability.py +0 -0
  105. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_parser.py +0 -0
  106. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_parser_v1.py +0 -0
  107. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_pyjs_semantics.py +0 -0
  108. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_reactivity.py +0 -0
  109. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_realtime_transport.py +0 -0
  110. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_reference_apps.py +0 -0
  111. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_rpc_placement.py +0 -0
  112. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_rpc_production.py +0 -0
  113. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_runtime_signals.py +0 -0
  114. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_server_db.py +0 -0
  115. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_static_hashing.py +0 -0
  116. {pyweb_stack-0.3.0 → pyweb_stack-0.4.0}/tests/test_website.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 MaanavKrishna and Claude
3
+ Copyright (c) 2026 MaanavKrishna
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweb-stack
3
- Version: 0.3.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>, Claude <noreply@anthropic.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 ~10 KB
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 that need the npm
141
- ecosystem (use React/Svelte/Vue), or running scientific Python in the
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 | The test suite covers the
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) and Claude.
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 ~10 KB
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 that need the npm
87
- ecosystem (use React/Svelte/Vue), or running scientific Python in the
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 | The test suite covers the
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) and Claude.
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.3.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, live
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
- "live",
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
- - In handlers, navigate with `window.location.href = "/path"`.
175
- - Run code after load with a handler named `on_mount`.
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
- JS functions, server-only names. Move such code into `@server` functions.
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
- value = raw[pname]
137
- try:
138
- if ann in (int, "int"):
139
- value = int(value)
140
- elif ann in (float, "float"):
141
- value = float(value)
142
- except ValueError:
143
- raise NotFound(f"{pname}={value!r}") from None
144
- out[pname] = value
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 render(self, page_name, params=None):
148
- """Return ``html`` (str) or a :class:`~pyweb.context.Redirect`."""
149
- page = self.compiled["pages"][page_name]
150
- info = page["info"]
151
- fn = self.module.__dict__[STATE_PREFIX + page_name]
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
- body = self.renderer().render(info.ui, env)
161
- state = {k: env.get(k) for k in info.sent}
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