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.
Files changed (109) hide show
  1. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/PKG-INFO +45 -7
  2. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/README.md +43 -6
  3. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyproject.toml +3 -3
  4. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/__init__.py +4 -0
  5. pyweb_stack-0.3.0/pyweb/ai/guide.md +227 -0
  6. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/app_loader.py +46 -15
  7. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/asgi.py +25 -0
  8. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/auth.py +25 -3
  9. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/bench.py +31 -7
  10. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/build.py +7 -0
  11. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cli/__init__.py +22 -37
  12. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/lower.py +97 -22
  13. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/pipeline.py +96 -9
  14. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/pyjs.py +3 -1
  15. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/db/__init__.py +1 -1
  16. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/hosting.py +37 -1
  17. pyweb_stack-0.3.0/pyweb/lsp.py +398 -0
  18. pyweb_stack-0.3.0/pyweb/mcp.py +693 -0
  19. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/models.py +2 -2
  20. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/realtime.py +192 -0
  21. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/runtime/browser/runtime.js +181 -36
  22. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/runtime/server/__init__.py +38 -31
  23. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/serve.py +5 -0
  24. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/ssr.py +24 -6
  25. pyweb_stack-0.3.0/pyweb/templates/app.css +32 -0
  26. pyweb_stack-0.3.0/pyweb/templates/auth.pyweb +111 -0
  27. pyweb_stack-0.3.0/pyweb/templates/blog.pyweb +84 -0
  28. pyweb_stack-0.3.0/pyweb/templates/chat.pyweb +81 -0
  29. pyweb_stack-0.3.0/pyweb/templates/counter.pyweb +26 -0
  30. pyweb_stack-0.3.0/pyweb/templates/todo.pyweb +72 -0
  31. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/testing.py +4 -8
  32. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/PKG-INFO +45 -7
  33. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/SOURCES.txt +10 -1
  34. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/requires.txt +1 -0
  35. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth.py +11 -0
  36. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_codegen.py +16 -0
  37. pyweb_stack-0.3.0/tests/test_lsp.py +187 -0
  38. pyweb_stack-0.3.0/tests/test_mcp.py +184 -0
  39. pyweb_stack-0.3.0/tests/test_multifile.py +182 -0
  40. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_plugins_platform_bench.py +8 -0
  41. pyweb_stack-0.3.0/tests/test_realtime_transport.py +181 -0
  42. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_website.py +21 -1
  43. pyweb_stack-0.1.0/pyweb/lsp.py +0 -214
  44. pyweb_stack-0.1.0/tests/test_lsp.py +0 -81
  45. pyweb_stack-0.1.0/tests/test_lsp_state.py +0 -76
  46. pyweb_stack-0.1.0/tests/test_realtime_transport.py +0 -77
  47. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/LICENSE +0 -0
  48. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/app.py +0 -0
  49. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/browser.py +0 -0
  50. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cache.py +0 -0
  51. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/cli/__main__.py +0 -0
  52. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/__init__.py +0 -0
  53. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/ast.py +0 -0
  54. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/__init__.py +0 -0
  55. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/codegen/ir.py +0 -0
  56. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/errors.py +0 -0
  57. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/parser.py +0 -0
  58. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/compiler/rpc.py +0 -0
  59. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/context.py +0 -0
  60. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/css.py +0 -0
  61. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/db/migrate.py +0 -0
  62. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/decorators.py +0 -0
  63. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/deploy.py +0 -0
  64. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/forms.py +0 -0
  65. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/jobs.py +0 -0
  66. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/live.py +0 -0
  67. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/npm.py +0 -0
  68. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/observability.py +0 -0
  69. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/platform.py +0 -0
  70. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/plugins.py +0 -0
  71. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/py.typed +0 -0
  72. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/rpc.py +0 -0
  73. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/security.py +0 -0
  74. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/sync.py +0 -0
  75. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb/uploads.py +0 -0
  76. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  77. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
  78. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/pyweb_stack.egg-info/top_level.txt +0 -0
  79. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/setup.cfg +0 -0
  80. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_contract.py +0 -0
  81. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_security.py +0 -0
  82. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_auth_v1.py +0 -0
  83. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_backplane.py +0 -0
  84. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_browser_build.py +0 -0
  85. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_budget.py +0 -0
  86. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_cli_deploy.py +0 -0
  87. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_compiler.py +0 -0
  88. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_db_production.py +0 -0
  89. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_deploy_upload_render.py +0 -0
  90. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_docs.py +0 -0
  91. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_dts.py +0 -0
  92. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_e2e.py +0 -0
  93. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_examples.py +0 -0
  94. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_forms_security_obs.py +0 -0
  95. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_jobs_cache_realtime.py +0 -0
  96. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_models_db.py +0 -0
  97. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_observability.py +0 -0
  98. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_parser.py +0 -0
  99. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_parser_v1.py +0 -0
  100. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_production_gaps.py +0 -0
  101. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_pyjs_semantics.py +0 -0
  102. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_reactivity.py +0 -0
  103. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_reference_apps.py +0 -0
  104. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_rpc_placement.py +0 -0
  105. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_rpc_production.py +0 -0
  106. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_runtime_signals.py +0 -0
  107. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_server_db.py +0 -0
  108. {pyweb_stack-0.1.0 → pyweb_stack-0.3.0}/tests/test_static_hashing.py +0 -0
  109. {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.1.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
  ![PyPI](https://img.shields.io/pypi/v/pyweb-stack)
64
65
  ![License](https://img.shields.io/badge/license-MIT-lightgrey)
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, polling with `on_mount` |
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 # scaffold
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.1 is the first public release (beta). The language, server API,
184
- RPC protocol and CLI are documented and tested, and changes to them are
185
- announced in the changelog (see [stability](docs/15-toolkit.md#stability)). The test suite covers the
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
  ![PyPI](https://img.shields.io/pypi/v/pyweb-stack)
11
11
  ![License](https://img.shields.io/badge/license-MIT-lightgrey)
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, polling with `on_mount` |
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 # scaffold
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.1 is the first public release (beta). The language, server API,
131
- RPC protocol and CLI are documented and tested, and changes to them are
132
- announced in the changelog (see [stability](docs/15-toolkit.md#stability)). The test suite covers the
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.1.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
- self.module = self._exec()
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(self.source)
72
- tree = ast.parse(py_source, filename=self.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(self.filename))[0] or "app"
78
- mod = types.ModuleType(f"pyweb_app_{stem}")
79
- mod.__file__ = os.path.abspath(self.filename) if self.path else self.filename
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, self.filename, "exec"), mod.__dict__) # noqa: S102 - the app itself
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
- info = self.compiled["components"][name]
91
- fn = self.module.__dict__[STATE_PREFIX + name]
92
- env = fn(**{k: v for k, v in props.items() if k in info.params})
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 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt_bytes, iterations)
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 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt_bytes, n)
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 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iters_i)
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)