pyweb-stack 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyweb_stack-0.1.0/pyweb_stack.egg-info → pyweb_stack-0.2.0}/PKG-INFO +25 -3
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/README.md +23 -2
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyproject.toml +3 -3
- pyweb_stack-0.2.0/pyweb/ai/guide.md +215 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cli/__init__.py +14 -30
- pyweb_stack-0.2.0/pyweb/mcp.py +525 -0
- pyweb_stack-0.2.0/pyweb/templates/app.css +28 -0
- pyweb_stack-0.2.0/pyweb/templates/auth.pyweb +111 -0
- pyweb_stack-0.2.0/pyweb/templates/blog.pyweb +84 -0
- pyweb_stack-0.2.0/pyweb/templates/chat.pyweb +83 -0
- pyweb_stack-0.2.0/pyweb/templates/counter.pyweb +26 -0
- pyweb_stack-0.2.0/pyweb/templates/todo.pyweb +72 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0/pyweb_stack.egg-info}/PKG-INFO +25 -3
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/SOURCES.txt +9 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/requires.txt +1 -0
- pyweb_stack-0.2.0/tests/test_mcp.py +162 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/LICENSE +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/app.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/app_loader.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/asgi.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/auth.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/bench.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/browser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/build.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cache.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cli/__main__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/ast.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/codegen/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/codegen/ir.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/errors.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/lower.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/parser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/pipeline.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/pyjs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/rpc.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/context.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/css.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/db/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/db/migrate.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/decorators.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/deploy.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/forms.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/hosting.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/jobs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/live.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/lsp.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/models.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/npm.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/observability.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/platform.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/plugins.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/py.typed +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/realtime.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/rpc.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/runtime/browser/runtime.js +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/runtime/server/__init__.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/security.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/serve.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/ssr.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/sync.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/testing.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/uploads.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/top_level.txt +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/setup.cfg +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_contract.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_security.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_v1.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_backplane.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_browser_build.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_budget.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_cli_deploy.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_codegen.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_compiler.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_db_production.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_deploy_upload_render.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_docs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_dts.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_e2e.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_examples.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_forms_security_obs.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_jobs_cache_realtime.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_lsp.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_lsp_state.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_models_db.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_observability.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_parser.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_parser_v1.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_plugins_platform_bench.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_production_gaps.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_pyjs_semantics.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_reactivity.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_realtime_transport.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_reference_apps.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_rpc_placement.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_rpc_production.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_runtime_signals.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_server_db.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_static_hashing.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_sync_live.py +0 -0
- {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_website.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyweb-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -139,11 +140,31 @@ browser (use Pyodide/PyScript). See the
|
|
|
139
140
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
140
141
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
141
142
|
|
|
143
|
+
## Build it with AI
|
|
144
|
+
|
|
145
|
+
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
146
|
+
render and test your app, with errors that come back as line numbers and
|
|
147
|
+
fix hints:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{ "mcpServers": { "pyweb": { "command": "pyweb", "args": ["mcp"] } } }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
(the JSON is for Cursor, Claude Desktop, VS Code and other MCP clients).
|
|
158
|
+
`pyweb new myapp --template todo` also writes `AGENTS.md` and `CLAUDE.md`
|
|
159
|
+
so coding agents follow PyWeb's rules, and the docs site publishes
|
|
160
|
+
[`llms-full.txt`](https://maanavkrishna.github.io/PyWeb/llms-full.txt).
|
|
161
|
+
See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
162
|
+
|
|
142
163
|
## Documentation
|
|
143
164
|
|
|
144
165
|
| | |
|
|
145
166
|
|---|---|
|
|
146
|
-
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) |
|
|
167
|
+
| 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
168
|
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
148
169
|
| 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
170
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
@@ -170,12 +191,13 @@ a real browser by the test suite.
|
|
|
170
191
|
## Command line
|
|
171
192
|
|
|
172
193
|
```bash
|
|
173
|
-
pyweb new myapp
|
|
194
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
174
195
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
175
196
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
176
197
|
pyweb check app.pyweb # compile + security checks for CI
|
|
177
198
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
178
199
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
200
|
+
pyweb mcp # MCP server for AI assistants (stdio)
|
|
179
201
|
```
|
|
180
202
|
|
|
181
203
|
## Status
|
|
@@ -86,11 +86,31 @@ browser (use Pyodide/PyScript). See the
|
|
|
86
86
|
[comparison](https://maanavkrishna.github.io/PyWeb/introduction.html)
|
|
87
87
|
and [current limitations](docs/16-limitations-roadmap.md).
|
|
88
88
|
|
|
89
|
+
## Build it with AI
|
|
90
|
+
|
|
91
|
+
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect,
|
|
92
|
+
render and test your app, with errors that come back as line numbers and
|
|
93
|
+
fix hints:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add pyweb -- pyweb mcp # Claude Code
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "mcpServers": { "pyweb": { "command": "pyweb", "args": ["mcp"] } } }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
(the JSON is for Cursor, Claude Desktop, VS Code and other MCP clients).
|
|
104
|
+
`pyweb new myapp --template todo` also writes `AGENTS.md` and `CLAUDE.md`
|
|
105
|
+
so coding agents follow PyWeb's rules, and the docs site publishes
|
|
106
|
+
[`llms-full.txt`](https://maanavkrishna.github.io/PyWeb/llms-full.txt).
|
|
107
|
+
See [AI assistants & MCP](docs/17-ai-assistants.md).
|
|
108
|
+
|
|
89
109
|
## Documentation
|
|
90
110
|
|
|
91
111
|
| | |
|
|
92
112
|
|---|---|
|
|
93
|
-
| Start | [Introduction](docs/01-introduction.md) · [Quickstart](docs/02-quickstart.md) · [Tutorial](docs/03-tutorial.md) |
|
|
113
|
+
| 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
114
|
| Language | [`.pyweb` files](docs/04-pyweb-files.md) · [State & reactivity](docs/05-reactivity.md) · [Python in the browser](docs/07-browser-python.md) |
|
|
95
115
|
| 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
116
|
| Ship | [Testing](docs/11-testing.md) · [Deployment](docs/12-deployment.md) · [Security](docs/13-security.md) · [CLI](docs/14-cli.md) |
|
|
@@ -117,12 +137,13 @@ a real browser by the test suite.
|
|
|
117
137
|
## Command line
|
|
118
138
|
|
|
119
139
|
```bash
|
|
120
|
-
pyweb new myapp
|
|
140
|
+
pyweb new myapp --template todo # scaffold (blank|counter|todo|blog|auth|chat)
|
|
121
141
|
pyweb dev app.pyweb # dev server: live reload + error overlay
|
|
122
142
|
pyweb inspect app.pyweb # where each name runs, and why
|
|
123
143
|
pyweb check app.pyweb # compile + security checks for CI
|
|
124
144
|
pyweb build app.pyweb --out dist --production # self-contained, hashed, minified dist/
|
|
125
145
|
pyweb serve dist # production server (/healthz, CSP, graceful shutdown)
|
|
146
|
+
pyweb mcp # MCP server for AI assistants (stdio)
|
|
126
147
|
```
|
|
127
148
|
|
|
128
149
|
## Status
|
|
@@ -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.2.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"]
|
|
@@ -0,0 +1,215 @@
|
|
|
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}`). Components must be in the same file and their
|
|
139
|
+
initial state must be computable in the browser (pass server data as props).
|
|
140
|
+
|
|
141
|
+
## 6. Server functions, sessions, routing
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from pyweb import App, RPCError, NotFound, redirect, request, server, session
|
|
145
|
+
|
|
146
|
+
@server
|
|
147
|
+
def update(item_id: int, title: str) -> dict: # annotations validate/coerce args
|
|
148
|
+
user = session.require() # 401 if not signed in
|
|
149
|
+
if not title.strip():
|
|
150
|
+
raise RPCError("validation_error", "Title is required.") # browser: except RPCError as e: str(e)
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
@app.page("/items/{item_id}") # typed route param; bad int -> 404
|
|
154
|
+
def Item(item_id: int):
|
|
155
|
+
if not session.user():
|
|
156
|
+
return redirect("/login")
|
|
157
|
+
row = find(item_id)
|
|
158
|
+
if row is None:
|
|
159
|
+
raise NotFound()
|
|
160
|
+
...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `session.login(user_id, **claims)`, `session.user()`, `session.logout()`,
|
|
164
|
+
`session.require("admin")`.
|
|
165
|
+
- `pyweb.auth.hash_password` / `verify_password` for passwords.
|
|
166
|
+
- Database: `from pyweb.db import connect; db = connect("sqlite:///app.db")`;
|
|
167
|
+
`db.execute("select ... where id = ?", (x,)).dicts()`; always use `?`
|
|
168
|
+
parameters; `with db.transaction(): ...`.
|
|
169
|
+
- In handlers, navigate with `window.location.href = "/path"`.
|
|
170
|
+
- Run code after load with a handler named `on_mount` (e.g.
|
|
171
|
+
`setInterval(refresh, 2000)` for polling).
|
|
172
|
+
|
|
173
|
+
## 7. Python that compiles to the browser
|
|
174
|
+
|
|
175
|
+
Supported in handlers/markup: literals, f-strings (with format specs),
|
|
176
|
+
arithmetic with Python semantics, comparisons, `in`, `and/or/not` with
|
|
177
|
+
Python truthiness, comprehensions, lambdas, slicing/negative indexes,
|
|
178
|
+
`if/for/while/try/except/raise/return/del`, builtins (`len str int float
|
|
179
|
+
bool abs min max sum round range sorted reversed enumerate zip list dict
|
|
180
|
+
set tuple any all isinstance print`), common str/list/dict/set methods.
|
|
181
|
+
JS globals are available directly: `window`, `document`, `localStorage`,
|
|
182
|
+
`console`, `setTimeout`, `setInterval`, `fetch`, `Math`, `JSON`, `Date`.
|
|
183
|
+
|
|
184
|
+
Not supported in browser code: classes, imports, `with`, generators,
|
|
185
|
+
walrus, `*args/**kwargs` parameters, slice assignment, keyword arguments to
|
|
186
|
+
JS functions, server-only names. Move such code into `@server` functions.
|
|
187
|
+
|
|
188
|
+
## 8. Errors and fixes
|
|
189
|
+
|
|
190
|
+
| Error text contains | Fix |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `only exists on the server` | Move that logic into an `@server` function and call it from the handler. |
|
|
193
|
+
| `is not defined in browser code` | Define it at module level (literal or helper function), pass it in, or use an `@server` function. |
|
|
194
|
+
| `markup expressions must be synchronous` | Assign the server call's result to a page variable or call it in a handler. |
|
|
195
|
+
| `cannot assign to ... derived/read-only` | Assign to a variable the handler owns (make it state), not a computed/constant. |
|
|
196
|
+
| `server secret ... would be sent to the browser` | Keep the value inside `@server` functions; don't read it in markup/handlers. |
|
|
197
|
+
| `bind={x} must name a local variable` | Declare `x = ""` (or a number/bool) in the page before the markup. |
|
|
198
|
+
| `unknown component <X>` | Define `def X(...)` with markup in the same file (capitalized). |
|
|
199
|
+
| `mismatched </tag>` / `is never closed` | Close every tag; void tags (`input`, `img`, `br`) need no close (`<input ... />`). |
|
|
200
|
+
| `... is not supported in browser code` | Rewrite with supported constructs or move it to `@server`. |
|
|
201
|
+
|
|
202
|
+
## 9. Workflow for agents
|
|
203
|
+
|
|
204
|
+
1. Start from a template: `pyweb new NAME --template todo` (or the
|
|
205
|
+
`pyweb_new_app` MCP tool). Templates: blank, counter, todo, blog, auth, chat.
|
|
206
|
+
2. Edit `app.pyweb`. After every edit run `pyweb check app.pyweb`
|
|
207
|
+
(MCP: `pyweb_check`) and fix errors by line number.
|
|
208
|
+
3. Use `pyweb inspect` (MCP: `pyweb_inspect`) to confirm what runs in the
|
|
209
|
+
browser vs server and what is sent to the browser.
|
|
210
|
+
4. Verify behaviour: render pages (`pyweb_render`) and call server
|
|
211
|
+
functions (`pyweb_call`), or write tests with `pyweb.testing.TestClient`.
|
|
212
|
+
5. Ship: `pyweb build app.pyweb --out dist --production` then
|
|
213
|
+
`pyweb serve dist` (set `PYWEB_AUTH_SECRET` in production).
|
|
214
|
+
|
|
215
|
+
Full docs: https://maanavkrishna.github.io/PyWeb/
|
|
@@ -382,37 +382,20 @@ def _parse_budget(spec):
|
|
|
382
382
|
return int(float(spec))
|
|
383
383
|
|
|
384
384
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def increment():
|
|
395
|
-
count += 1
|
|
396
|
-
|
|
397
|
-
<main>
|
|
398
|
-
<h1>Counter</h1>
|
|
399
|
-
<button onclick={{increment}}>
|
|
400
|
-
Count: {{count}}
|
|
401
|
-
</button>
|
|
402
|
-
</main>
|
|
403
|
-
"""
|
|
385
|
+
def cmd_new(args):
|
|
386
|
+
from pyweb.mcp import scaffold
|
|
387
|
+
try:
|
|
388
|
+
files = scaffold(args.name, template=args.template)
|
|
389
|
+
except (FileExistsError, ValueError) as exc:
|
|
390
|
+
raise SystemExit(f"error: {exc}")
|
|
391
|
+
for f in files:
|
|
392
|
+
print(f"created {f}")
|
|
393
|
+
print(f"next: cd {args.name} && pyweb dev app.pyweb")
|
|
404
394
|
|
|
405
395
|
|
|
406
|
-
def
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
os.makedirs(os.path.join(args.name, "static"), exist_ok=True)
|
|
410
|
-
title = os.path.basename(os.path.abspath(args.name)).replace("-", " ").replace("_", " ").title()
|
|
411
|
-
with open(os.path.join(args.name, "app.pyweb"), "w", encoding="utf-8") as fh:
|
|
412
|
-
fh.write(NEW_APP.format(title=title))
|
|
413
|
-
with open(os.path.join(args.name, ".gitignore"), "w", encoding="utf-8") as fh:
|
|
414
|
-
fh.write("dist/\n__pycache__/\n*.db\n")
|
|
415
|
-
print(f"created {args.name}/app.pyweb\nnext: cd {args.name} && pyweb dev app.pyweb")
|
|
396
|
+
def cmd_mcp(args):
|
|
397
|
+
from pyweb.mcp import serve_stdio
|
|
398
|
+
serve_stdio()
|
|
416
399
|
|
|
417
400
|
|
|
418
401
|
def main(argv=None):
|
|
@@ -425,7 +408,8 @@ def main(argv=None):
|
|
|
425
408
|
p = sub.add_parser("dev"); p.add_argument("file"); p.add_argument("--port", type=int, default=8000); p.add_argument("--host", default="127.0.0.1"); p.add_argument("--no-reload", action="store_true", help="disable hot-reload watcher"); p.set_defaults(fn=cmd_dev)
|
|
426
409
|
p = sub.add_parser("serve"); p.add_argument("dir", default="dist", nargs="?"); p.add_argument("--host", default="0.0.0.0"); p.add_argument("--port", type=int, default=8000); p.add_argument("--app", default=None, help="live RPC factory module:attr"); p.set_defaults(fn=cmd_serve)
|
|
427
410
|
p = sub.add_parser("db"); p.add_argument("db_action", choices=["migrate", "new", "status", "rollback"]); p.add_argument("--database", default=None); p.add_argument("--migrations", default="migrations"); p.add_argument("--name", default="migration"); p.add_argument("--steps", type=int, default=1, help="rollback: how many applied migrations to revert"); p.add_argument("--to", default=None, help="rollback: revert everything applied after this label"); p.set_defaults(fn=cmd_db)
|
|
428
|
-
p = sub.add_parser("new"); p.add_argument("name"); p.set_defaults(fn=cmd_new)
|
|
411
|
+
p = sub.add_parser("new"); p.add_argument("name"); p.add_argument("--template", default="counter", choices=["blank", "counter", "todo", "blog", "auth", "chat"], help="starter app"); p.set_defaults(fn=cmd_new)
|
|
412
|
+
p = sub.add_parser("mcp", help="run the MCP server (stdio) for AI assistants"); p.set_defaults(fn=cmd_mcp)
|
|
429
413
|
p = sub.add_parser("check"); p.add_argument("file"); p.set_defaults(fn=cmd_check)
|
|
430
414
|
p = sub.add_parser("npm"); p.add_argument("dts"); p.add_argument("-o", "--out", default=None); p.set_defaults(fn=cmd_npm)
|
|
431
415
|
p = sub.add_parser("deploy")
|