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.
Files changed (105) hide show
  1. {pyweb_stack-0.1.0/pyweb_stack.egg-info → pyweb_stack-0.2.0}/PKG-INFO +25 -3
  2. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/README.md +23 -2
  3. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyproject.toml +3 -3
  4. pyweb_stack-0.2.0/pyweb/ai/guide.md +215 -0
  5. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cli/__init__.py +14 -30
  6. pyweb_stack-0.2.0/pyweb/mcp.py +525 -0
  7. pyweb_stack-0.2.0/pyweb/templates/app.css +28 -0
  8. pyweb_stack-0.2.0/pyweb/templates/auth.pyweb +111 -0
  9. pyweb_stack-0.2.0/pyweb/templates/blog.pyweb +84 -0
  10. pyweb_stack-0.2.0/pyweb/templates/chat.pyweb +83 -0
  11. pyweb_stack-0.2.0/pyweb/templates/counter.pyweb +26 -0
  12. pyweb_stack-0.2.0/pyweb/templates/todo.pyweb +72 -0
  13. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0/pyweb_stack.egg-info}/PKG-INFO +25 -3
  14. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/SOURCES.txt +9 -0
  15. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/requires.txt +1 -0
  16. pyweb_stack-0.2.0/tests/test_mcp.py +162 -0
  17. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/LICENSE +0 -0
  18. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/__init__.py +0 -0
  19. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/app.py +0 -0
  20. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/app_loader.py +0 -0
  21. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/asgi.py +0 -0
  22. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/auth.py +0 -0
  23. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/bench.py +0 -0
  24. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/browser.py +0 -0
  25. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/build.py +0 -0
  26. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cache.py +0 -0
  27. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/cli/__main__.py +0 -0
  28. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/__init__.py +0 -0
  29. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/ast.py +0 -0
  30. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/codegen/__init__.py +0 -0
  31. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/codegen/ir.py +0 -0
  32. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/errors.py +0 -0
  33. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/lower.py +0 -0
  34. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/parser.py +0 -0
  35. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/pipeline.py +0 -0
  36. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/pyjs.py +0 -0
  37. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/compiler/rpc.py +0 -0
  38. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/context.py +0 -0
  39. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/css.py +0 -0
  40. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/db/__init__.py +0 -0
  41. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/db/migrate.py +0 -0
  42. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/decorators.py +0 -0
  43. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/deploy.py +0 -0
  44. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/forms.py +0 -0
  45. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/hosting.py +0 -0
  46. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/jobs.py +0 -0
  47. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/live.py +0 -0
  48. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/lsp.py +0 -0
  49. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/models.py +0 -0
  50. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/npm.py +0 -0
  51. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/observability.py +0 -0
  52. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/platform.py +0 -0
  53. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/plugins.py +0 -0
  54. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/py.typed +0 -0
  55. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/realtime.py +0 -0
  56. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/rpc.py +0 -0
  57. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/runtime/browser/runtime.js +0 -0
  58. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/runtime/server/__init__.py +0 -0
  59. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/security.py +0 -0
  60. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/serve.py +0 -0
  61. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/ssr.py +0 -0
  62. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/sync.py +0 -0
  63. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/testing.py +0 -0
  64. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb/uploads.py +0 -0
  65. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  66. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/entry_points.txt +0 -0
  67. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/pyweb_stack.egg-info/top_level.txt +0 -0
  68. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/setup.cfg +0 -0
  69. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth.py +0 -0
  70. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_contract.py +0 -0
  71. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_security.py +0 -0
  72. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_auth_v1.py +0 -0
  73. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_backplane.py +0 -0
  74. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_browser_build.py +0 -0
  75. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_budget.py +0 -0
  76. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_cli_deploy.py +0 -0
  77. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_codegen.py +0 -0
  78. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_compiler.py +0 -0
  79. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_db_production.py +0 -0
  80. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_deploy_upload_render.py +0 -0
  81. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_docs.py +0 -0
  82. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_dts.py +0 -0
  83. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_e2e.py +0 -0
  84. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_examples.py +0 -0
  85. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_forms_security_obs.py +0 -0
  86. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_jobs_cache_realtime.py +0 -0
  87. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_lsp.py +0 -0
  88. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_lsp_state.py +0 -0
  89. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_models_db.py +0 -0
  90. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_observability.py +0 -0
  91. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_parser.py +0 -0
  92. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_parser_v1.py +0 -0
  93. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_plugins_platform_bench.py +0 -0
  94. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_production_gaps.py +0 -0
  95. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_pyjs_semantics.py +0 -0
  96. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_reactivity.py +0 -0
  97. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_realtime_transport.py +0 -0
  98. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_reference_apps.py +0 -0
  99. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_rpc_placement.py +0 -0
  100. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_rpc_production.py +0 -0
  101. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_runtime_signals.py +0 -0
  102. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_server_db.py +0 -0
  103. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_static_hashing.py +0 -0
  104. {pyweb_stack-0.1.0 → pyweb_stack-0.2.0}/tests/test_sync_live.py +0 -0
  105. {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.1.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 # scaffold
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 # scaffold
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.1.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
- NEW_APP = """from pyweb import App
386
-
387
- app = App(title="{title}")
388
-
389
-
390
- @app.page("/")
391
- def Home():
392
- count = 0
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 cmd_new(args):
407
- if os.path.exists(os.path.join(args.name, "app.pyweb")):
408
- raise SystemExit(f"{args.name}/app.pyweb already exists")
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")