pyweb-stack 0.5.0__tar.gz → 0.6.1__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 (367) hide show
  1. pyweb_stack-0.6.1/CHANGELOG.md +1135 -0
  2. pyweb_stack-0.6.1/MANIFEST.in +3 -0
  3. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/PKG-INFO +117 -32
  4. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/README.md +105 -29
  5. pyweb_stack-0.6.1/THREAT_MODEL.md +201 -0
  6. pyweb_stack-0.6.1/docs/01-introduction.md +166 -0
  7. pyweb_stack-0.6.1/docs/02-quickstart.md +117 -0
  8. pyweb_stack-0.6.1/docs/03-tutorial.md +300 -0
  9. pyweb_stack-0.6.1/docs/04-pyweb-files.md +345 -0
  10. pyweb_stack-0.6.1/docs/05-reactivity.md +149 -0
  11. pyweb_stack-0.6.1/docs/06-server-functions.md +382 -0
  12. pyweb_stack-0.6.1/docs/07-browser-python.md +98 -0
  13. pyweb_stack-0.6.1/docs/08-pages-routing-assets.md +202 -0
  14. pyweb_stack-0.6.1/docs/09-data.md +432 -0
  15. pyweb_stack-0.6.1/docs/10-auth.md +324 -0
  16. pyweb_stack-0.6.1/docs/11-testing.md +193 -0
  17. pyweb_stack-0.6.1/docs/12-deployment.md +267 -0
  18. pyweb_stack-0.6.1/docs/13-security.md +282 -0
  19. pyweb_stack-0.6.1/docs/14-cli.md +183 -0
  20. pyweb_stack-0.6.1/docs/15-toolkit.md +110 -0
  21. pyweb_stack-0.6.1/docs/16-limitations-roadmap.md +74 -0
  22. pyweb_stack-0.6.1/docs/17-ai-assistants.md +224 -0
  23. pyweb_stack-0.6.1/docs/18-npm-packages.md +210 -0
  24. pyweb_stack-0.6.1/docs/19-layouts-navigation.md +148 -0
  25. pyweb_stack-0.6.1/docs/20-ai-apps.md +146 -0
  26. pyweb_stack-0.6.1/docs/21-live-data.md +130 -0
  27. pyweb_stack-0.6.1/docs/22-recipe-web3.md +146 -0
  28. pyweb_stack-0.6.1/docs/23-forms.md +192 -0
  29. pyweb_stack-0.6.1/docs/24-jobs.md +186 -0
  30. pyweb_stack-0.6.1/docs/25-scaling.md +89 -0
  31. pyweb_stack-0.6.1/docs/26-observability.md +207 -0
  32. pyweb_stack-0.6.1/docs/27-upgrading.md +206 -0
  33. pyweb_stack-0.6.1/docs/28-saas-tutorial.md +234 -0
  34. pyweb_stack-0.6.1/docs/29-performance.md +163 -0
  35. pyweb_stack-0.6.1/docs/30-teams.md +199 -0
  36. pyweb_stack-0.6.1/docs/31-pwa-push.md +168 -0
  37. pyweb_stack-0.6.1/docs/32-desktop.md +190 -0
  38. pyweb_stack-0.6.1/docs/33-webhooks-files.md +103 -0
  39. pyweb_stack-0.6.1/docs/34-ui-and-motion.md +537 -0
  40. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyproject.toml +12 -6
  41. pyweb_stack-0.6.1/pyweb/__init__.py +149 -0
  42. pyweb_stack-0.6.1/pyweb/_speedups.py +89 -0
  43. pyweb_stack-0.6.1/pyweb/_templates.py +3 -0
  44. pyweb_stack-0.6.1/pyweb/a11y.py +121 -0
  45. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/ai/guide.md +134 -11
  46. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/app.py +37 -6
  47. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/app_loader.py +137 -10
  48. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/asgi.py +6 -4
  49. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/auth.py +6 -0
  50. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/__init__.py +130 -4
  51. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/admin.py +25 -10
  52. pyweb_stack-0.6.1/pyweb/authkit/html.py +141 -0
  53. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/models.py +72 -1
  54. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/oauth.py +3 -0
  55. pyweb_stack-0.6.1/pyweb/authkit/orgpages.py +234 -0
  56. pyweb_stack-0.6.1/pyweb/authkit/orgs.py +98 -0
  57. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/pages.py +138 -11
  58. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/passwords.py +3 -0
  59. pyweb_stack-0.6.1/pyweb/authkit/privacy.py +101 -0
  60. pyweb_stack-0.6.1/pyweb/authkit/teams.py +286 -0
  61. pyweb_stack-0.6.1/pyweb/authkit/tokens.py +81 -0
  62. pyweb_stack-0.6.1/pyweb/bench.py +373 -0
  63. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/build.py +13 -0
  64. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/cli/__init__.py +274 -39
  65. pyweb_stack-0.6.1/pyweb/compiler/errors.py +223 -0
  66. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/lower.py +158 -23
  67. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/parser.py +40 -4
  68. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/pipeline.py +97 -10
  69. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/pyjs.py +84 -9
  70. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/config.py +31 -2
  71. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/context.py +29 -0
  72. pyweb_stack-0.6.1/pyweb/csp_report.py +116 -0
  73. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/db/__init__.py +39 -0
  74. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/db/migrate.py +2 -0
  75. pyweb_stack-0.6.1/pyweb/db/rls.py +120 -0
  76. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/db/schema.py +6 -0
  77. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/decorators.py +29 -3
  78. pyweb_stack-0.6.1/pyweb/desktop/__init__.py +243 -0
  79. pyweb_stack-0.6.1/pyweb/desktop/gate.py +164 -0
  80. pyweb_stack-0.6.1/pyweb/desktop/package.py +255 -0
  81. pyweb_stack-0.6.1/pyweb/desktop/shell.py +195 -0
  82. pyweb_stack-0.6.1/pyweb/devlink.py +236 -0
  83. pyweb_stack-0.6.1/pyweb/doctor.py +303 -0
  84. pyweb_stack-0.6.1/pyweb/fieldcrypt.py +186 -0
  85. pyweb_stack-0.6.1/pyweb/filegc.py +116 -0
  86. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/forms.py +26 -5
  87. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/hosting.py +83 -8
  88. pyweb_stack-0.6.1/pyweb/http.py +265 -0
  89. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/core.py +2 -0
  90. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/memory.py +2 -0
  91. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/worker.py +5 -1
  92. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/livedata.py +4 -3
  93. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/lsp.py +13 -0
  94. pyweb_stack-0.6.1/pyweb/lsp_teams.py +139 -0
  95. pyweb_stack-0.6.1/pyweb/lsp_ui.py +175 -0
  96. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/mail.py +3 -0
  97. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/mcp.py +707 -180
  98. pyweb_stack-0.6.1/pyweb/mcp_http.py +219 -0
  99. pyweb_stack-0.6.1/pyweb/mcp_tools.py +708 -0
  100. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/models/__init__.py +104 -11
  101. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/models/fields.py +50 -1
  102. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/models/query.py +43 -0
  103. pyweb_stack-0.6.1/pyweb/motion.py +282 -0
  104. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/net/live.py +2 -1
  105. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/net/server.py +6 -3
  106. pyweb_stack-0.6.1/pyweb/openapi.py +228 -0
  107. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/packages.py +3 -0
  108. pyweb_stack-0.6.1/pyweb/platform.py +100 -0
  109. pyweb_stack-0.6.1/pyweb/profile.py +123 -0
  110. pyweb_stack-0.6.1/pyweb/push.py +468 -0
  111. pyweb_stack-0.6.1/pyweb/pwa.py +423 -0
  112. pyweb_stack-0.6.1/pyweb/runtime/browser/desktop.js +41 -0
  113. pyweb_stack-0.6.1/pyweb/runtime/browser/dev/reload.js +102 -0
  114. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/browser/markdown.js +8 -1
  115. pyweb_stack-0.6.1/pyweb/runtime/browser/motion-api.js +68 -0
  116. pyweb_stack-0.6.1/pyweb/runtime/browser/motion-count.js +48 -0
  117. pyweb_stack-0.6.1/pyweb/runtime/browser/motion-drag.js +105 -0
  118. pyweb_stack-0.6.1/pyweb/runtime/browser/motion-layout.js +71 -0
  119. pyweb_stack-0.6.1/pyweb/runtime/browser/motion-math.js +132 -0
  120. pyweb_stack-0.6.1/pyweb/runtime/browser/motion.js +498 -0
  121. pyweb_stack-0.6.1/pyweb/runtime/browser/pwa.js +120 -0
  122. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/browser/runtime.js +120 -21
  123. pyweb_stack-0.6.1/pyweb/runtime/browser/sw.js +149 -0
  124. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/accordion.js +89 -0
  125. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/calendar.js +162 -0
  126. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/carousel.js +105 -0
  127. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/checkbox.js +9 -0
  128. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/collapsible.js +12 -0
  129. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/combobox.js +91 -0
  130. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/command.js +72 -0
  131. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/context-menu.js +36 -0
  132. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/core.js +286 -0
  133. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/dialog.js +89 -0
  134. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/hover-card.js +39 -0
  135. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/index.js +86 -0
  136. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/listbox.js +74 -0
  137. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/menu.js +168 -0
  138. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/menubar.js +58 -0
  139. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/navigation-menu.js +94 -0
  140. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/otp.js +29 -0
  141. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/popover.js +75 -0
  142. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/radio-group.js +20 -0
  143. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/resizable.js +101 -0
  144. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/select.js +104 -0
  145. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/sidebar.js +71 -0
  146. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/slider.js +19 -0
  147. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/tabs.js +74 -0
  148. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/toaster.js +158 -0
  149. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/toggle-group.js +45 -0
  150. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/toggle.js +15 -0
  151. pyweb_stack-0.6.1/pyweb/runtime/browser/ui/tooltip.js +61 -0
  152. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/server/__init__.py +127 -17
  153. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/server/argcheck.py +30 -0
  154. pyweb_stack-0.6.1/pyweb/sbom.py +147 -0
  155. pyweb_stack-0.6.1/pyweb/secret_scan.py +102 -0
  156. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/security.py +3 -1
  157. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/serve.py +18 -1
  158. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/ssr.py +173 -6
  159. pyweb_stack-0.6.1/pyweb/ssr_codegen.py +263 -0
  160. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/storage.py +13 -1
  161. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/devtools.py +168 -1
  162. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/instruments.py +2 -0
  163. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/logs.py +31 -9
  164. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/metrics.py +5 -0
  165. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/tracing.py +16 -6
  166. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/templates/ai-chat.pyweb +9 -7
  167. pyweb_stack-0.6.1/pyweb/templates/app.css +105 -0
  168. pyweb_stack-0.6.1/pyweb/templates/auth.pyweb +135 -0
  169. pyweb_stack-0.6.1/pyweb/templates/blog.pyweb +87 -0
  170. pyweb_stack-0.6.1/pyweb/templates/chat.pyweb +100 -0
  171. pyweb_stack-0.6.1/pyweb/templates/counter.pyweb +37 -0
  172. pyweb_stack-0.6.1/pyweb/templates/desktop/app.pyweb +125 -0
  173. pyweb_stack-0.6.1/pyweb/templates/desktop/test_app.py +58 -0
  174. pyweb_stack-0.6.1/pyweb/templates/saas/app.pyweb +269 -0
  175. pyweb_stack-0.6.1/pyweb/templates/saas/seeds.py +19 -0
  176. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/templates/saas/test_app.py +28 -4
  177. pyweb_stack-0.6.1/pyweb/templates/todo.pyweb +84 -0
  178. pyweb_stack-0.6.1/pyweb/tenancy.py +123 -0
  179. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/testing.py +59 -6
  180. pyweb_stack-0.6.1/pyweb/ui/__init__.py +96 -0
  181. pyweb_stack-0.6.1/pyweb/ui/cli.py +86 -0
  182. pyweb_stack-0.6.1/pyweb/ui/extending.py +147 -0
  183. pyweb_stack-0.6.1/pyweb/ui/kit/button.pyweb +25 -0
  184. pyweb_stack-0.6.1/pyweb/ui/kit/card.pyweb +25 -0
  185. pyweb_stack-0.6.1/pyweb/ui/kit/carousel.pyweb +41 -0
  186. pyweb_stack-0.6.1/pyweb/ui/kit/chart.pyweb +292 -0
  187. pyweb_stack-0.6.1/pyweb/ui/kit/command.pyweb +113 -0
  188. pyweb_stack-0.6.1/pyweb/ui/kit/controls.pyweb +131 -0
  189. pyweb_stack-0.6.1/pyweb/ui/kit/data.pyweb +202 -0
  190. pyweb_stack-0.6.1/pyweb/ui/kit/date.pyweb +35 -0
  191. pyweb_stack-0.6.1/pyweb/ui/kit/dialog.pyweb +137 -0
  192. pyweb_stack-0.6.1/pyweb/ui/kit/disclosure.pyweb +85 -0
  193. pyweb_stack-0.6.1/pyweb/ui/kit/display.pyweb +59 -0
  194. pyweb_stack-0.6.1/pyweb/ui/kit/layout.pyweb +264 -0
  195. pyweb_stack-0.6.1/pyweb/ui/kit/menu.pyweb +130 -0
  196. pyweb_stack-0.6.1/pyweb/ui/kit/overlay.pyweb +65 -0
  197. pyweb_stack-0.6.1/pyweb/ui/kit/style.pyweb +118 -0
  198. pyweb_stack-0.6.1/pyweb/ui/serve.py +96 -0
  199. pyweb_stack-0.6.1/pyweb/ui/source_edit.py +111 -0
  200. pyweb_stack-0.6.1/pyweb/ui/static/ui.css +617 -0
  201. pyweb_stack-0.6.1/pyweb/ui/theme.py +293 -0
  202. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/upgrade.py +70 -0
  203. pyweb_stack-0.6.1/pyweb/webhooks.py +334 -0
  204. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb_stack.egg-info/PKG-INFO +117 -32
  205. pyweb_stack-0.6.1/pyweb_stack.egg-info/SOURCES.txt +351 -0
  206. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb_stack.egg-info/requires.txt +10 -0
  207. pyweb_stack-0.6.1/setup.py +24 -0
  208. pyweb_stack-0.6.1/tests/test_account_privacy.py +79 -0
  209. pyweb_stack-0.6.1/tests/test_api_tokens.py +107 -0
  210. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_auth.py +2 -1
  211. pyweb_stack-0.6.1/tests/test_auth_sessions.py +99 -0
  212. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_authkit.py +28 -1
  213. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_budget.py +1 -1
  214. pyweb_stack-0.6.1/tests/test_cli_test.py +44 -0
  215. pyweb_stack-0.6.1/tests/test_compile_cache.py +88 -0
  216. pyweb_stack-0.6.1/tests/test_customise.py +286 -0
  217. pyweb_stack-0.6.1/tests/test_desktop.py +467 -0
  218. pyweb_stack-0.6.1/tests/test_dev_reload.py +53 -0
  219. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_docs.py +4 -2
  220. pyweb_stack-0.6.1/tests/test_doctor.py +128 -0
  221. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_dx_v5.py +3 -1
  222. pyweb_stack-0.6.1/tests/test_encrypted_fields.py +151 -0
  223. pyweb_stack-0.6.1/tests/test_error_messages.py +164 -0
  224. pyweb_stack-0.6.1/tests/test_filegc.py +65 -0
  225. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_forms_v5.py +2 -1
  226. pyweb_stack-0.6.1/tests/test_fuzz_requests.py +143 -0
  227. pyweb_stack-0.6.1/tests/test_http_fetch.py +136 -0
  228. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_jobs_app.py +5 -1
  229. pyweb_stack-0.6.1/tests/test_lsp_teams.py +67 -0
  230. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_mcp.py +24 -6
  231. pyweb_stack-0.6.1/tests/test_mcp_teams.py +54 -0
  232. pyweb_stack-0.6.1/tests/test_mcp_v6.py +401 -0
  233. pyweb_stack-0.6.1/tests/test_motion.py +268 -0
  234. pyweb_stack-0.6.1/tests/test_openapi.py +107 -0
  235. pyweb_stack-0.6.1/tests/test_org_pages.py +150 -0
  236. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_plugins_platform_bench.py +21 -0
  237. pyweb_stack-0.6.1/tests/test_profile.py +63 -0
  238. pyweb_stack-0.6.1/tests/test_push.py +284 -0
  239. pyweb_stack-0.6.1/tests/test_pwa.py +199 -0
  240. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_pyjs_semantics.py +20 -2
  241. pyweb_stack-0.6.1/tests/test_rate_per_function.py +80 -0
  242. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_reference_apps.py +2 -1
  243. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_request_hardening.py +26 -0
  244. pyweb_stack-0.6.1/tests/test_sbom.py +62 -0
  245. pyweb_stack-0.6.1/tests/test_secret_scan.py +52 -0
  246. pyweb_stack-0.6.1/tests/test_speedups.py +57 -0
  247. pyweb_stack-0.6.1/tests/test_ssr_codegen.py +123 -0
  248. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_systems.py +80 -0
  249. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_telemetry.py +40 -0
  250. pyweb_stack-0.6.1/tests/test_tenancy.py +321 -0
  251. pyweb_stack-0.6.1/tests/test_testing_orgs.py +116 -0
  252. pyweb_stack-0.6.1/tests/test_threat_model.py +20 -0
  253. pyweb_stack-0.6.1/tests/test_trusted_types.py +95 -0
  254. pyweb_stack-0.6.1/tests/test_ui_kit.py +469 -0
  255. pyweb_stack-0.6.1/tests/test_webhooks.py +161 -0
  256. pyweb_stack-0.5.0/pyweb/__init__.py +0 -102
  257. pyweb_stack-0.5.0/pyweb/authkit/html.py +0 -107
  258. pyweb_stack-0.5.0/pyweb/bench.py +0 -125
  259. pyweb_stack-0.5.0/pyweb/compiler/errors.py +0 -29
  260. pyweb_stack-0.5.0/pyweb/platform.py +0 -69
  261. pyweb_stack-0.5.0/pyweb/templates/app.css +0 -50
  262. pyweb_stack-0.5.0/pyweb/templates/auth.pyweb +0 -111
  263. pyweb_stack-0.5.0/pyweb/templates/blog.pyweb +0 -75
  264. pyweb_stack-0.5.0/pyweb/templates/chat.pyweb +0 -81
  265. pyweb_stack-0.5.0/pyweb/templates/counter.pyweb +0 -26
  266. pyweb_stack-0.5.0/pyweb/templates/saas/app.pyweb +0 -168
  267. pyweb_stack-0.5.0/pyweb/templates/saas/seeds.py +0 -14
  268. pyweb_stack-0.5.0/pyweb/templates/todo.pyweb +0 -72
  269. pyweb_stack-0.5.0/pyweb_stack.egg-info/SOURCES.txt +0 -184
  270. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/LICENSE +0 -0
  271. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/throttle.py +0 -0
  272. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/authkit/webauthn.py +0 -0
  273. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/browser.py +0 -0
  274. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/cache.py +0 -0
  275. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/cli/__main__.py +0 -0
  276. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/__init__.py +0 -0
  277. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/ast.py +0 -0
  278. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/codegen/__init__.py +0 -0
  279. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/codegen/ir.py +0 -0
  280. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/compiler/rpc.py +0 -0
  281. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/css.py +0 -0
  282. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/db/dialect.py +0 -0
  283. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/db/seeds.py +0 -0
  284. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/deploy/__init__.py +0 -0
  285. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/deploy/legacy.py +0 -0
  286. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/deploy/plan.py +0 -0
  287. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/deploy/targets.py +0 -0
  288. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/deprecation.py +0 -0
  289. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/dts.py +0 -0
  290. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/__init__.py +0 -0
  291. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/builtin.py +0 -0
  292. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/db.py +0 -0
  293. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/legacy.py +0 -0
  294. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/redis.py +0 -0
  295. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/jobs/schedule.py +0 -0
  296. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/keys.py +0 -0
  297. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/livetable.py +0 -0
  298. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/lsp_data.py +0 -0
  299. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/markdown.py +0 -0
  300. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/models/relations.py +0 -0
  301. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/net/__init__.py +0 -0
  302. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/net/http.py +0 -0
  303. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/net/ws.py +0 -0
  304. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/observability.py +0 -0
  305. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/plugins.py +0 -0
  306. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/py.typed +0 -0
  307. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/realtime.py +0 -0
  308. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/rooms.py +0 -0
  309. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/rpc.py +0 -0
  310. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/rules.py +0 -0
  311. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/browser/auth.js +0 -0
  312. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/browser/forms.js +0 -0
  313. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/runtime/browser/live.js +0 -0
  314. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/sync.py +0 -0
  315. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/telemetry/__init__.py +0 -0
  316. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb/uploads.py +0 -0
  317. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  318. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb_stack.egg-info/entry_points.txt +0 -0
  319. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/pyweb_stack.egg-info/top_level.txt +0 -0
  320. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/setup.cfg +0 -0
  321. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_auth_contract.py +0 -0
  322. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_auth_security.py +0 -0
  323. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_auth_v1.py +0 -0
  324. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_authkit_units.py +0 -0
  325. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_backplane.py +0 -0
  326. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_browser_build.py +0 -0
  327. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_cli_deploy.py +0 -0
  328. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_codegen.py +0 -0
  329. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_compiler.py +0 -0
  330. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_cron.py +0 -0
  331. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_data_layer.py +0 -0
  332. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_db_production.py +0 -0
  333. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_deploy_upload_render.py +0 -0
  334. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_deploy_v5.py +0 -0
  335. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_dts.py +0 -0
  336. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_e2e.py +0 -0
  337. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_examples.py +0 -0
  338. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_forms_security_obs.py +0 -0
  339. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_jobs_cache_realtime.py +0 -0
  340. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_jobs_v5.py +0 -0
  341. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_keys_sessions.py +0 -0
  342. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_live_patches.py +0 -0
  343. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_livedata.py +0 -0
  344. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_lsp.py +0 -0
  345. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_markdown.py +0 -0
  346. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_migrations_property.py +0 -0
  347. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_migrations_v5.py +0 -0
  348. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_models_db.py +0 -0
  349. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_multifile.py +0 -0
  350. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_multipage.py +0 -0
  351. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_net.py +0 -0
  352. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_observability.py +0 -0
  353. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_packages.py +0 -0
  354. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_parser.py +0 -0
  355. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_parser_v1.py +0 -0
  356. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_production_gaps.py +0 -0
  357. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_reactivity.py +0 -0
  358. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_realtime_transport.py +0 -0
  359. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_rpc_placement.py +0 -0
  360. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_rpc_production.py +0 -0
  361. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_runtime_signals.py +0 -0
  362. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_server_db.py +0 -0
  363. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_static_hashing.py +0 -0
  364. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_static_serving.py +0 -0
  365. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_streaming.py +0 -0
  366. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_sync_live.py +0 -0
  367. {pyweb_stack-0.5.0 → pyweb_stack-0.6.1}/tests/test_website.py +0 -0
@@ -0,0 +1,1135 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project uses [semantic versioning](https://semver.org).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.6.1]
10
+
11
+ Better error messages, `pyweb doctor`, a `pyweb dev` that keeps your place
12
+ when it reloads, much faster reloads and tests, and fixes found by
13
+ installing 0.6.0 from PyPI and putting every template through every
14
+ command. Nothing to change in your apps.
15
+
16
+ ### Added
17
+ - **Better error messages.** Compile errors show the line with a caret under
18
+ the problem, suggest the name you meant (a prop, component, variable,
19
+ server function argument, kit variant or motion preset: "did you mean
20
+ 'name'?"), and link the docs page that explains them. The CLI, the
21
+ `pyweb dev` overlay and the MCP tools (`frame`, `docs` fields) show the
22
+ same. When page code raises in `pyweb dev`, the error page starts with the
23
+ frames in your own files and their source lines.
24
+ - **`pyweb dev` keeps your place when it reloads.** After a save, the page
25
+ comes back with its variables (a counter's count, a bound input's text),
26
+ unbound form fields, scroll position and focus as they were. A variable
27
+ whose type changed, or that was renamed, starts over; add `?fresh` to the
28
+ URL to start everything over. Production builds only grow by the names
29
+ of page variables.
30
+ - **`pyweb doctor`** checks the project and the machine in one go: Python,
31
+ whether the app compiles, optional packages the app needs, the database
32
+ and pending migrations, Redis, secrets written in the source, changes
33
+ `pyweb upgrade` wants, a stale dev server link, tests, the port and the
34
+ editor extension, each with a fix (`--production` adds the production
35
+ settings, `--json` for CI). Assistants get it as the `pyweb_doctor` tool.
36
+ - **Faster reloads and tests.** The UI kit is compiled once per process and
37
+ each file's Python code is reused until it changes: on the saas example a
38
+ `pyweb dev` reload after a save went from about 255 to 60 ms, a
39
+ `TestClient` from about 255 to 55 ms, and compiling from about 240 to
40
+ 60 ms. The dev server notices a save within 0.1 s (was 0.4 s) and the
41
+ page asks for the new version every 0.25 s (was 0.6 s).
42
+ `python -m pyweb.bench --system` reports `reload_ms` and `test_client_ms`,
43
+ and CI's benchmark gate watches them.
44
+ - `pyweb check` warns about kit props and motion values that won't work
45
+ (`<Button variant="outlin">`), as the editor already did.
46
+
47
+ ### Fixed
48
+ - `pyweb check` printed the file and line twice for compile errors.
49
+ - Components drawn as a kit button (`DialogClose`) were flagged by the
50
+ editor for `variant="default"`, which they support.
51
+ - `pyweb test` with no path ran `pytest tests/`, which new apps don't have
52
+ (their test is `test_app.py` next to `app.pyweb`), so it failed in every
53
+ new project. It now runs the current folder, and accepts the app's
54
+ `.pyweb` file as well as a test file or folder.
55
+ - The `pyweb_test` MCP tool given the app's `.pyweb` file (as every other
56
+ tool takes) found no tests; it now tests that file's folder.
57
+ - `pyweb upgrade` says to apply pending migrations before creating the
58
+ `auth_sessions` one (on a fresh checkout `pyweb db diff` refused to run),
59
+ and notes that 0.6's `pyweb check` reports accessibility problems, which
60
+ `--strict` treats as failures.
61
+
62
+ ## [0.6.0]
63
+
64
+ Teams, installable and desktop apps, a UI kit and motion, and security by
65
+ design: organizations with roles, invitations and tenant-scoped Models
66
+ (enforced by the database on Postgres), API tokens and OpenAPI, webhooks,
67
+ PWA and Web Push, native desktop windows and installers, a styled and
68
+ accessible component kit with springs and layout animations, Trusted
69
+ Types, encrypted fields and a threat model, about 2.5× faster serving,
70
+ Python 3.15 and free-threaded builds, and a much more capable MCP server.
71
+ Most 0.5 apps run unchanged; see [Upgrading to 0.6](docs/27-upgrading.md)
72
+ and run `pyweb upgrade --check`.
73
+
74
+ ### Added
75
+ - **Python 3.14, 3.15 and free-threaded 3.15t** are supported and tested in
76
+ CI. On a free-threaded build one `pyweb serve` process uses every core
77
+ (about 3× the requests per second of the same build with the GIL).
78
+ - **Python 3.15 features.** Browser code can use unpacking in
79
+ comprehensions (`[*row for row in rows]`, `{**d for d in ds}`). `lazy
80
+ import` is server-only Python, and older Pythons get a clear error that
81
+ names the version it needs.
82
+ - **`pyweb profile app.pyweb`** serves the app and records where the time
83
+ goes while you use it: a Tachyon flame graph on Python 3.15, a text
84
+ report before.
85
+ - **`pyweb-stack[speed]`** adds orjson for live queries, WebSocket messages,
86
+ page state and server function results (a 10,000-row live query encodes
87
+ 10× faster).
88
+ - **`python -m pyweb.bench --system`** measures requests per second over
89
+ sockets, a signed-in page, start-up and live queries; `--compare DIR`
90
+ fails on a 15% regression against another checkout. CI runs it on
91
+ every push.
92
+ - **`PYWEB_SSR_VERIFY=1`** renders every page with both renderers and
93
+ fails if they differ.
94
+ - New docs page: [Performance](docs/29-performance.md), including why
95
+ PyWeb has no Rust core.
96
+ - **Trusted Types.** Pages tell the browser to refuse plain strings in
97
+ HTML and script sinks; PyWeb's runtime uses private policies
98
+ (Markdown, client navigation, the service worker). Pages that use npm packages are exempt.
99
+ - **Encrypted fields.** `Field(encrypted=True)` stores AES-256-GCM
100
+ ciphertext under `PYWEB_DATA_KEY` (kept apart from the session secret);
101
+ `searchable=True` adds a blind index for exact lookups. Rotate with
102
+ `PYWEB_DATA_KEY_PREVIOUS` and `pyweb db reencrypt`.
103
+ - **Secret scanning.** `pyweb check` finds keys written into the app and
104
+ the files next to it: known formats fail the check, random-looking values
105
+ in secret-named variables are warnings.
106
+ - **`pyweb.http.fetch`** for URLs other people choose: refuses internal
107
+ addresses (including through redirects and DNS rebinding), with time and
108
+ size limits. `PYWEB_FETCH_ALLOW` lets it reach internal services you name.
109
+ - **`@server(rate="5/minute", per="user"|"ip"|"all")`** limits one server
110
+ function (429 with `Retry-After`; shared through Redis when configured).
111
+ - **Webhooks.** `@app.webhook("/hooks/stripe", secret=..., scheme="stripe")`
112
+ checks GitHub, Stripe and Standard Webhooks signatures, drops repeats
113
+ and replays, and runs the handler as a retried job. `webhooks.send()` /
114
+ `publish()` sign deliveries (Standard Webhooks), retry them over about a
115
+ day through the SSRF-safe fetch, and log each attempt; `WebhookEndpoint`
116
+ rows hold each organization's endpoints.
117
+ - **Installable apps.** `App(pwa=True)` (or `pwa=PWA(name=, icon=,
118
+ theme_color=, offline_page=, cache_pages=...)`) serves a web app
119
+ manifest, icons made from one PNG (or drawn for you) and a service
120
+ worker: static files work offline, pages without a connection show an
121
+ offline page, and with `cache_pages=True` pages visitors opened while
122
+ signed out stay readable offline. The worker is registered through a
123
+ third Trusted Types policy, `pyweb-sw`, which accepts only `/sw.js`.
124
+ - **A UI kit** (`from pyweb.ui import Button, Card, Badge, Alert, ...`):
125
+ themed, accessible, server-rendered components. `App(theme=Theme(accent=,
126
+ radius=, font=, density=, dark=))` or a preset computes 12-step OKLCH
127
+ colour scales (accent, gray, status colours; light and dark) with
128
+ contrast checked to 4.5:1. The kit's CSS sits in a cascade layer so
129
+ app CSS always wins, and every part carries `data-part`/`data-state`
130
+ attributes. `pyweb ui add|diff|list` copies a component's source into
131
+ the app. Overlays and pickers follow the WAI-ARIA Authoring Practices:
132
+ Dialog, AlertDialog, Sheet, Popover, Tooltip, HoverCard, DropdownMenu,
133
+ ContextMenu, Menubar (with checkbox/radio items and submenus),
134
+ SelectMenu, Combobox, a Command palette (⌘K) and sonner-style toasts
135
+ (`pyweb.ui.toast()` from server code). Also Tabs, Accordion and
136
+ Collapsible (native `<details>`), Checkbox, Switch, RadioGroup, Toggle,
137
+ ToggleGroup, Slider, InputOTP, a Sidebar layout, NavigationMenu,
138
+ Breadcrumb, Pagination, Resizable panels, ScrollArea, Calendar and
139
+ DatePicker, Carousel, DataTable (search, sort, pages, row selection) and
140
+ SVG charts (bar, line, area, pie, sparkline) drawn on the server. Each
141
+ behaviour module loads only on pages that use it; `from pyweb.ui import *`
142
+ imports them all.
143
+ - **Motion** (in the spirit of motion.dev): `initial=`, `animate=`,
144
+ `exit=` and `transition=` on any element, springs that keep their
145
+ momentum when interrupted, presets (`animate="fade-up"`), variants with
146
+ staggered children, `while_hover`/`while_tap`/`while_focus`/
147
+ `while_in_view`, exit animations for `if` and `for`, `layout` (rows glide
148
+ to their new places, keeping focus and input values), `layout_id`
149
+ (shared elements), `drag` with constraints and momentum, counting
150
+ numbers (`animate="count"`), View Transitions on client navigation, and
151
+ `from pyweb.motion import animate, timeline, stagger, scroll_progress,
152
+ use_transform` for browser code. `App(motion=Motion(feel=, speed=,
153
+ reduced=))` changes every animation at once, the UI kit's included, and
154
+ reduced-motion settings are followed. The server renders the first
155
+ pose; pages without motion don't load the engine (7.5 KB gzipped).
156
+ - **Customising the kit, every level.**
157
+ - The dev toolbar gets a **Theme** tab: a live preview on the running
158
+ app, copy as code, and "Save to app.pyweb", which rewrites only the
159
+ `App(theme=...)` argument.
160
+ - It also gets a **Motion** tab: pick an animated element, tune its
161
+ spring or duration, and replay it.
162
+ - `ui.extend("Button", variant={"brand": {...}})` adds your own
163
+ variants to kit components.
164
+ - `variants()`, `style()` and `cx()` (cva and tailwind-merge style)
165
+ are for your own components, on the server and in the browser.
166
+ - The editor completes variant, size and tone values, presets,
167
+ `Theme(...)`/`Motion(...)` settings and transition keys, and warns
168
+ about unknown ones or out-of-range springs.
169
+ - **A "UI kit and motion" example** (`examples/ui`): dialogs, menus,
170
+ pickers, toasts, a data table and charts, plus staggered, gesture,
171
+ drag, layout and shared-element animations. It runs on the website, as
172
+ a live demo and in the playground.
173
+ - `MenuItem(href=...)` makes a menu item a link.
174
+ - **The UI kit is tested in Chromium, Firefox and WebKit** (keyboard
175
+ behaviour of every component, motion and the examples), with
176
+ screenshot baselines per component in light and dark, and axe-core
177
+ finding no serious violations on the examples, the auth pages and
178
+ `/admin`.
179
+ - **Accessibility checks** in `pyweb check` and the editor: images
180
+ without alt, clicks keyboards can't reach, unlabelled fields, unnamed
181
+ buttons and links, positive tabindex, an empty page language. The
182
+ examples and templates were fixed where they fell short.
183
+ - **Testing apps with organizations:** `client.login(email, org="Acme",
184
+ org_role="admin")` signs in as a member of that organization (created
185
+ if needed) and makes it the current one; `with as_org(org):` runs code
186
+ for an organization; `Factory(...).for_org(org)` makes tenant rows in it.
187
+ - **`@app.page(..., org_role=..., feature=...)`** (and on layouts), like
188
+ `@server`: the page needs that role in the current organization, or a
189
+ plan with that feature (403 otherwise).
190
+ - **Editor:** completions for `@server(...)` and `@app.page(...)` options,
191
+ `org_role=`, `per=`, `rate=`, plan features, `tenant=True` and webhook
192
+ schemes; warnings for a malformed `rate=`, an unknown role, and a
193
+ `tenant=True` Model read with `.unscoped()` outside `with system():`.
194
+ - **A much better MCP server** (29 tools, up from 18):
195
+ - new tools: `pyweb_docs` (search every docs page, the guide and the
196
+ threat model), `pyweb_ui` (the kit's components, their parameters and
197
+ source, copying one into your app, making and saving a theme),
198
+ `pyweb_review` (screenshots in light and dark at desktop and phone
199
+ widths with axe-core's audit), `pyweb_explain` (what your editor shows
200
+ for a line: where each name runs, problems, completions),
201
+ `pyweb_profile`, `pyweb_webhook_send` (a correctly signed test
202
+ delivery, then the handler's result), `pyweb_teams`, `pyweb_upgrade`,
203
+ `pyweb_deploy_plan`, `pyweb_openapi` and `pyweb_security_check`;
204
+ - `pyweb_dev` and `pyweb_logs` see your running `pyweb dev`: whether the
205
+ app compiles (with the traceback), its logs and the requests your
206
+ browser made. `pyweb dev` writes `.pyweb/dev.json` (its address and a
207
+ token readable only by you); `pyweb_screenshot` and `pyweb_review` use
208
+ it too;
209
+ - `pyweb_render`, `pyweb_call`, `pyweb_screenshot` and `pyweb_profile`
210
+ act as a signed-in user and organization member (`as_user`, `roles`,
211
+ `org`, `org_role`);
212
+ - MCP 2025-11-25: every tool has a title, read-only/destructive hints
213
+ and an output schema; arguments are checked before a tool runs; slow
214
+ tools report progress; logging; completion of resource and prompt
215
+ arguments; resources for every docs page (`pyweb://docs/<page>`), kit
216
+ component (`pyweb://kit/<Component>`) and the threat model;
217
+ - eight new prompts: `add_teams`, `make_installable`, `make_desktop_app`,
218
+ `security_review`, `upgrade_pyweb`, `design_pass`, `fix_failing_test`
219
+ and `debug_page`;
220
+ - `pyweb mcp --http` serves Streamable HTTP on 127.0.0.1 (sessions,
221
+ event streams for progress; `Origin` and `Host` checked; a token is
222
+ required to listen anywhere else).
223
+ - The documentation ships inside the package, for the MCP server.
224
+ - **`pyweb upgrade`** points at stylesheets written for the 0.5 sign-in and
225
+ admin pages' classes.
226
+ - **Playground:** apps with teams get a "Team member" visitor next to
227
+ "Signed in" (both in one demo team, so you can see what each role sees),
228
+ and installable apps get an Install tab with their manifest and icons.
229
+ - **Docs:** [Upgrading to 0.6](docs/27-upgrading.md), testing apps with
230
+ teams, and the SaaS tutorial rewritten around teams.
231
+ - **New `desktop` template** (`pyweb new notes --template desktop`): a
232
+ Markdown notes app in its own window that exports a note with the
233
+ system's Save dialog, with its tests and packaging instructions.
234
+ - **Plain functions from other `.pyweb` files work in browser code**,
235
+ along with the functions and constants they use.
236
+ - **`math` in browser code.** `import math` (and `from math import pi`)
237
+ works in browser code, translated to JavaScript's `Math`.
238
+ - **Components pass attributes through.** A `**attrs` parameter takes any
239
+ other attribute, and `{**attrs}` spreads a dict onto an element
240
+ (explicit attributes win, classes merge).
241
+ - **Desktop apps.** `pyweb desktop app.pyweb` opens the app in its own
242
+ window (pywebview: WebView2, WKWebView, WebKitGTK or Qt), with its
243
+ database, uploads and per-installation secrets in the OS's data folder.
244
+ Only the window gets in: a one-time launch cookie, an exact `Host` and
245
+ same-origin requests are required, and links to other sites open in the
246
+ browser. `pyweb.desktop` gives server code file dialogs, notifications,
247
+ "reveal in folder" and the data folder; `App(desktop=Window(...))` sets
248
+ the window. `pyweb build --desktop [--package]` writes a Briefcase
249
+ project with icons for every OS and builds the installer (MSI, DMG,
250
+ `.deb`/`.rpm`). `pyweb-stack[desktop]` installs what it needs.
251
+ - **Push notifications.** A `data-pw-push="subscribe"` button asks for
252
+ permission; `push.send(user, "Title", "Body", url="/orders/42")` encrypts
253
+ (RFC 8291), signs (VAPID) and delivers through background jobs, deleting
254
+ subscriptions the push service says are gone. Only the browsers' push
255
+ services are accepted as endpoints. `pyweb push key` makes a
256
+ `PYWEB_VAPID_KEY`; `pyweb-stack[push]` installs what it needs.
257
+ - **Uploaded files are cleaned up.** Files no row refers to are deleted
258
+ after a day (daily, or `pyweb files clean`).
259
+ - **API tokens and OpenAPI.** `/account/tokens` makes tokens for calling
260
+ server functions with `Authorization: Bearer` (scoped to functions,
261
+ optionally expiring, stored as hashes); `pyweb openapi app.pyweb` writes
262
+ an OpenAPI 3.1 document from the functions' types, rules and guards.
263
+ - **Your data.** `/account` offers a JSON download of everything stored
264
+ about the user (`auth.export_user()`) and deleting the account
265
+ (`auth.delete_user()`, after a recent sign-in and typing the email).
266
+ - **Signed-in devices.** `/account` lists each session (device, network,
267
+ last seen) and can sign one out; `auth.sessions(user)` and
268
+ `auth.end_session(user, id)` do the same from code. Apps with migrations
269
+ need one for the new `auth_sessions` table (`pyweb upgrade` says so);
270
+ until then sign-in works as before.
271
+ - **Supply chain.** `pyweb build` writes a CycloneDX `sbom.json`; releases
272
+ get PEP 740 attestations on PyPI and a CycloneDX file; CI pins every
273
+ GitHub Action to a commit.
274
+ - **Teams.** `app.use_auth(orgs=True)` adds organizations, members with
275
+ roles (owner, admin, member, viewer), emailed invitations and the pages
276
+ to manage them (`/orgs`). `class Project(Model, tenant=True)` keeps each
277
+ organization's rows to itself: queries, saves, bulk updates, live lists
278
+ and background jobs are all limited to the current organization.
279
+ `@server(org_role="admin", feature="exports")` and `rate=..., per="org"`;
280
+ `auth.plans({...})` with features and limits (seats are enforced).
281
+ On Postgres, migrations add row-level security, so raw SQL is limited to
282
+ the organization too. See [Teams](docs/30-teams.md).
283
+ - **`THREAT_MODEL.md`**: every threat, its mitigation and the test that
284
+ proves it (a test keeps the references current). CI fuzzes the RPC and
285
+ form parsers with Hypothesis and runs an OWASP ZAP baseline scan.
286
+ - **CSP violation reports** go to `/__pyweb/csp-report`: a `pyweb.csp` log
287
+ line and the `pyweb_csp_violations_total` metric.
288
+
289
+ ### Changed
290
+ - **About 2.5× more requests per second** over `pyweb serve`: pages render
291
+ on the thread that read the request (the time limit moved into the
292
+ connection handler), each page's markup compiles to one Python function,
293
+ and request ids no longer cost a system call each.
294
+ - **`import pyweb` takes 2 ms** instead of 87 ms, and `pyweb --version`
295
+ about a third of the time it did: modules load when first used.
296
+ - **The sign-in, account, organization and admin pages use the UI kit**
297
+ and the app's theme: `App(theme=...)` restyles them along with your
298
+ pages. `/__pyweb/auth/auth.css` now only lays them out.
299
+ - **The templates and examples use the UI kit and motion.** `todo` rows
300
+ animate in and out and filter with a toggle group; `counter` counts;
301
+ `saas` (Teamboard) has a sidebar layout with an account menu, a
302
+ new-project dialog, toasts, animated live tasks, empty states and the
303
+ zinc theme; the dashboard's totals count up and it adds the kit's bar
304
+ chart and a sparkline. The shared `static/app.css` only lays pages out.
305
+ - **The `saas` template (Teamboard) has teams.** Projects and tasks are
306
+ `tenant=True` Models shared by an organization; each new user gets a
307
+ team, and the sidebar links to members and invitations and to API
308
+ tokens. Creating a project publishes a `project.created` webhook, and
309
+ the app is installable (`pwa=True`). Its tests show two teams kept apart
310
+ and two teammates sharing projects.
311
+ - `pyweb check` no longer asks for a policy on `tenant=True` Models: the
312
+ organization scope already limits who reads them.
313
+ - The CI benchmark measures fixed copies of the todo and saas apps
314
+ (`bench/apps/`), so redesigning an example doesn't read as a speed
315
+ change.
316
+ - The website's live demos are what the server sends (with the kit's and
317
+ motion's files), and the playground runs apps that use the kit.
318
+ - Tracebacks from `.pyweb` files show the source lines.
319
+ - `pyweb check` reports the line of a Python syntax error.
320
+ - **`pyweb.platform` is real and supported:** `platform.desktop`,
321
+ `platform.pwa`, `platform.installed` and `platform.web` are read per
322
+ request. `platform.mobile` and `platform.edge` are always False (they
323
+ were placeholders); `pyweb upgrade` points at them.
324
+
325
+ ### Fixed
326
+ - A server function argument annotated `date`, `datetime`, `time`,
327
+ `Decimal` or `UUID` arrived as the text the browser sent, unchecked
328
+ (`"banana"` for a `date`). It's now parsed (ISO 8601 for dates and
329
+ times), and anything else is a `validation_error` naming the field.
330
+ - `checked={row["done"]}` with a database's `0` rendered `checked="0"`, so
331
+ the server showed every box ticked. Boolean attributes (`checked`,
332
+ `disabled`, `selected`, ...) now follow truthiness on the server as in
333
+ the browser.
334
+ - A page using only components copied with `pyweb ui add` didn't get the
335
+ kit's stylesheet and behaviours.
336
+ - An alert's title was smaller than its text.
337
+ - The plant shop example's link and dark-mode button colours were below
338
+ WCAG contrast.
339
+ - A `Card` (or `Alert`, `Table`, `Breadcrumb`) inside `Tabs` had its
340
+ content taken for a tab panel and hidden; and the card's header styles
341
+ reached a data table's header row inside it.
342
+ - The saas template's daily digest called a query method that doesn't
343
+ exist (`values_list`); it uses `pluck`, and a test runs it.
344
+ - A menubar arrow key pressed right after opening a menu (before focus
345
+ moved into it) left that menu open.
346
+ - `lambda n=n: go(n)` in an event handler kept the event instead of `n`.
347
+ - Hydrating a page no longer rewrites attributes the server already
348
+ rendered, so it can't undo what a UI kit script (or the user, typing
349
+ early) changed in the meantime.
350
+ - Markup right after an `if`/`for` block inside an element (a
351
+ `{children}` or text line at the block's indent) was put inside the
352
+ block; it now follows it.
353
+ - An optional callback prop that wasn't passed no longer throws when its
354
+ event fires.
355
+ - Markup right after a `def` whose signature spans several lines failed to
356
+ compile.
357
+ - Text children re-rendered in the browser showed as a list (`['Medium']`).
358
+ - `get_or_404()` inside a server function answered 500; it now answers 404.
359
+ - `Post.id` (and other columns added automatically) failed before the Model
360
+ had been used once.
361
+ - Log redaction took time quadratic in the length of a word without `=`
362
+ (0.3 ms for a 64-character id, on every logged query parameter while
363
+ developing); it is now linear. Found by the new benchmark gate.
364
+ - Form submits with more than 1,000 fields or 100 files are refused (400)
365
+ instead of being parsed.
366
+ - A Model that fails to set up no longer breaks every Model defined after it.
367
+ - Models on Python 3.14+ (annotations are evaluated lazily there, PEP 649).
368
+
369
+ ## [0.5.0]
370
+
371
+ Production apps in one Python file: Models with migrations, forms, an
372
+ auth kit with an admin, row-level live updates over WebSockets on PyWeb's
373
+ own server, durable background jobs and schedules, one-command
374
+ deployment for six targets, built-in observability, and editors and AI
375
+ assistants that understand your data. Most 0.4 apps run unchanged; see
376
+ [Upgrading to 0.5](docs/27-upgrading.md) and run `pyweb upgrade --check`.
377
+
378
+ ### Added
379
+ - **Models on `pyweb.db`.** `class Post(Model): title: str = Field(max=120)`
380
+ works on SQLite, Postgres and MySQL with the same code. Types include
381
+ `Decimal`, timezone-aware `datetime`, `date`, `time`, JSON (`dict`/`list`),
382
+ `bytes`, `UUID`, `Text`, `Email`, `URL` and `Slug`. `Field(...)` holds the
383
+ column's schema and its validation rules (`min`, `max`, `pattern`,
384
+ `choices`, `format`, `unique`, `index`, `private`, `auto_now_add`, ...);
385
+ `@validates("field")` adds custom checks. Saving writes only changed
386
+ columns; unique violations become field errors.
387
+ - **Relations.** `author: User` is a foreign key, `tags: list[Tag]` is
388
+ many-to-many, `OneToOne(...)`, with reverse accessors (`user.posts`).
389
+ `include("author", "tags", "author.profile")` loads related rows with one
390
+ query per relation; reading a relation that wasn't included is an error
391
+ while developing (so pages can't do N+1 queries) and a logged warning in
392
+ production. Relation filters (`Post.author.has(...)`, `User.posts.any(...)`)
393
+ use subqueries.
394
+ - **Query builder.** Immutable, lazy queries with `Post.views > 10`, `&`,
395
+ `|`, `~`, keyword lookups (`title__icontains=`, `id__in=`, ...),
396
+ `order`, slicing, `first/last/get/get_or_404`, `count/exists`,
397
+ `values/pluck`, `aggregate` and `group_by` with `Count/Sum/Avg/Min/Max`,
398
+ bulk `update/delete`, cursor pagination with `page()`, `get_or_create`,
399
+ `upsert`, `bulk_create`, and `.live()`.
400
+ - **Transactions per request.** Every `@server` call is one transaction,
401
+ started at its first write; `@atomic` / `@atomic(False)`. Nested
402
+ `db.transaction()` blocks are savepoints. `ValidationError` from an RPC
403
+ answers 422 with the errors per field.
404
+ - **Migrations.** `pyweb db diff` compares Models with the live database
405
+ and writes Python migrations; `upgrade`, `downgrade`, `status`, `adopt`,
406
+ `squash`, `seed`. Changes are split into *expand* steps (safe during a
407
+ rolling deploy) and *contract* steps (`upgrade --contract`), including
408
+ renames with `--rename table.old=new`. A lock shared by every server
409
+ (Postgres advisory lock, MySQL `GET_LOCK`, or a lock row) makes
410
+ `pyweb serve --migrate` safe with many servers. SQLite schema changes
411
+ rebuild tables with foreign keys paused. `pyweb dev` applies pending
412
+ migrations, and apps without migrations create tables on first use.
413
+ - **Forms.** `<Form action={save_post}>` with `<Input>`, `<Textarea>`,
414
+ `<Select>`, `<Checkbox>`, `<FileInput>`, `<Submit>` and `<FormError>`
415
+ builds labelled, accessible fields from the action's Model (or plain
416
+ typed parameters). Rules are checked in the browser while typing, with
417
+ the same messages as the server; the form submits without a page load and
418
+ shows server errors next to each input, and still works as a plain POST
419
+ without JavaScript (422 re-render with values, 303 on success). CSRF
420
+ token, cross-site refusal, a one-time key per submit (double clicks run
421
+ once), only the action's own fields are read, `values={row}` edits a row
422
+ through a signed id, and `forms.carry()`/`restore()` for multi-step forms.
423
+ The browser code (`forms.js`) only loads on pages with a form.
424
+ - **Uploads and storage.** `File(types=["image/*"], max_size="2MB")` fields
425
+ and `UploadedFile` parameters; types checked from the file's bytes;
426
+ `PYWEB_MAX_UPLOAD` (default 10MB); `PYWEB_STORAGE` is a local folder
427
+ (served with a sandboxing CSP) or S3-compatible storage with presigned
428
+ URLs and no SDK.
429
+ - **Auth kit.** `auth = app.use_auth()` adds `/signup`, `/login`,
430
+ `/logout`, `/reset`, `/account` and `/admin`, rendered on the server and
431
+ working without JavaScript. Sign in with a password, an emailed link, a
432
+ passkey (also offered in the email field's autofill) or GitHub / Google /
433
+ Microsoft (PKCE + `state`), with optional authenticator-app codes and
434
+ recovery codes. Define a page at the same route to replace one;
435
+ `auth.user()`, `auth.authenticate()`, `auth.login()`, `auth.set_roles()`,
436
+ `auth.revoke()` ... for custom flows. Tables `users`, `auth_identities`,
437
+ `auth_credentials`, `auth_tokens` and `auth_events` are ordinary Models.
438
+ - **Guards.** `login=True`, `roles=[...]` and `fresh=seconds` on
439
+ `@app.page`, `@app.layout` and `@server`.
440
+ - **Row policies.** `Note.policy(read=..., write=...)` limits the rows each
441
+ user can see and change, on every query and save in a request
442
+ (`with system():` skips them on purpose).
443
+ - **Admin.** `/admin` lists, searches, edits and deletes rows of every
444
+ Model for users with the `admin` role; private fields never appear; every
445
+ change is in the audit log.
446
+ - **Account security.** scrypt (or argon2id with `pyweb-stack[auth]`)
447
+ hashes upgraded at sign-in; breached-password check in production;
448
+ guessing slowed down per account and per IP (shared through Redis) with no
449
+ lockout; the same answer and timing for unknown emails; emailed links
450
+ hashed, single-use (also under concurrent clicks) and short-lived;
451
+ passkey challenges single-use on every server; password resets, password
452
+ and role changes sign out other sessions, durably in the `users` table
453
+ (and fail closed if the database can't be checked); emailed links are
454
+ built from `PYWEB_ORIGIN`, never from the `Host` header, in production.
455
+ - **`pyweb.mail`.** `PYWEB_MAIL_URL` (SMTP/SMTPS) and `PYWEB_MAIL_FROM`;
456
+ while developing, emails are printed and kept in `pyweb.mail.OUTBOX`.
457
+ - `pyweb check --production` reports `use_auth()` without `PYWEB_ORIGIN`.
458
+ - **PyWeb's own server.** `pyweb serve` and `pyweb dev` now run on
459
+ `pyweb.net`: HTTP/1.1, WebSockets (RFC 6455) and Server-Sent Events on
460
+ one asyncio loop per process, standard library only. Thousands of open
461
+ live connections cost a coroutine each instead of a thread (the old
462
+ server stopped at 256); page renders and RPCs run on a thread pool
463
+ (`PYWEB_THREADS`). `--workers N` / `WEB_CONCURRENCY` forks processes
464
+ sharing the port and replaces crashed ones. The parser refuses request
465
+ smuggling vectors (CL+TE, repeated lengths, folded headers, bad chunk
466
+ sizes, bare LF) and has timeouts for heads, bodies, idle keep-alive and
467
+ slow readers; SIGTERM drains gracefully.
468
+ - **One WebSocket per tab for live updates.** Every `subscribe()` and live
469
+ query on a page shares `/__pyweb/ws` (Server-Sent Events used one
470
+ connection each, and browsers allow six per site), with jittered
471
+ reconnects that resume where they stopped, and automatic fallback to
472
+ Server-Sent Events, then polling. The socket checks `Origin`
473
+ (`PYWEB_ALLOWED_ORIGINS` for others), re-checks the session every minute
474
+ and closes when it's revoked, limits subscriptions, message size and
475
+ rate, and doesn't buffer without limit for slow clients. Works in the
476
+ ASGI adapter too.
477
+ - **Live queries send patches.** Rows with an `id` (or `key=`) travel as
478
+ inserts, updates, deletes and moves: changing one row of a long list
479
+ sends that row only, and the browser redraws only it. A page that missed
480
+ a change gets the whole result once. `Model.query().live()` uses the
481
+ primary key.
482
+ - **Presence.** `presence(name)` and `join(room, info, on_members,
483
+ on_cast)`: who is on a page, and ephemeral casts (cursors, typing) to
484
+ everyone else in the room, across processes with Redis.
485
+ - Feeds made while someone is signed in only work for that session.
486
+ - `live.js`: live-update code loads only on pages that use it.
487
+ - **Durable background jobs.** `@app.job(retries=, timeout=, queue=,
488
+ unique_for=)` and `fn.enqueue(..., delay=, at=, key=)`. Jobs are stored
489
+ in the app's database (`pyweb_jobs`), Redis (`PYWEB_JOBS=redis`) or memory,
490
+ and queued inside the request's transaction (a rollback queues nothing).
491
+ Workers claim with leases (`FOR UPDATE SKIP LOCKED` on Postgres/MySQL), so
492
+ a crashed or killed worker's job runs again elsewhere; failures retry with
493
+ jittered exponential backoff, then go `dead` (retry from `/admin` or
494
+ `pyweb jobs retry`); timeouts; graceful SIGTERM hands running jobs back.
495
+ `pyweb worker`, `pyweb jobs list|retry|purge|run`, and a worker inside
496
+ `pyweb dev`/`pyweb serve` (`PYWEB_WORKER=0` to run them separately).
497
+ - **Schedules.** `@app.cron("0 3 * * *", tz=..., catchup=...)` with PyWeb's
498
+ own cron parser (daylight saving handled) and `@app.every(minutes=5)`.
499
+ Every server runs the scheduler and each slot is queued exactly once,
500
+ with no leader election.
501
+ - **Email outbox.** With durable jobs, `pyweb.mail.send` queues delivery as
502
+ a retried job that only leaves if the request committed.
503
+ - **Nightly cleanup** of finished jobs, expired sign-in links and old audit
504
+ entries.
505
+ - `db.after_commit(fn)`: run code once the current transaction commits
506
+ (dropped on rollback, including a rolled-back savepoint), and
507
+ `db.after_end(fn)` for code that must run either way.
508
+ - Jobs stored in the app's database are marked done in the job's own
509
+ transaction, so a job's database writes happen exactly once even if a
510
+ worker crashes or loses its lease.
511
+ - **`pyweb deploy` plans the production topology.** It reads the app
512
+ (database, jobs, live updates, sign-in, uploads, migrations), decides
513
+ what production needs for the target and number of machines (Postgres
514
+ instead of SQLite for several machines or disposable disks, Redis for
515
+ more than one process, a separate worker, migrations once per deploy,
516
+ object storage for uploads, the secrets to set), prints the plan with
517
+ the reason for each decision, and writes the files. Targets: `docker`,
518
+ `compose` (Caddy, load-balanced replicas, Postgres, Redis, worker,
519
+ migrate step), `k8s` (web and worker Deployments, migrate init
520
+ container, probes, PodDisruptionBudget, HorizontalPodAutoscaler,
521
+ Ingress), `fly`, `render` and `railway`. `--plan` prints only;
522
+ `--check` fails CI when the plan has warnings.
523
+ - **A production image**: multi-stage, non-root, cached pip installs,
524
+ health check, `SIGTERM` drain, `PYTHON_IMAGE`/`PYWEB_SPEC`/
525
+ `EXTRA_PACKAGES` build arguments.
526
+ - `DATABASE_URL` overrides the URL in `App(database=...)` (develop on
527
+ SQLite, deploy on Postgres, same code). `pyweb serve` uses `$PORT`.
528
+ - `pyweb check --production` also reports Models without migrations,
529
+ unapplied migrations, email without `PYWEB_MAIL_URL`, uploads without
530
+ object storage, and jobs with `PYWEB_WORKER=0`.
531
+ - CI deploys a real stack (2 replicas, worker, Postgres, Redis, Caddy)
532
+ with `pyweb deploy compose` and checks load balancing, jobs on the
533
+ worker and live updates across replicas.
534
+ - **Read replicas** with `DATABASE_REPLICA_URL` (read-your-writes inside a
535
+ request), slow query logging with query plans (`PYWEB_SLOW_QUERY_MS`),
536
+ `pyweb.db.QUERY_HOOKS`, seeds (`pyweb.db.seeds`) and
537
+ `pyweb.testing.Factory`.
538
+
539
+ - **Observability.** A dev toolbar on every page in `pyweb dev`: the SQL each
540
+ request ran (repeated statements grouped, with an N+1 warning), spans,
541
+ jobs queued, emails captured, errors, and a list of every request since;
542
+ it never ships in production builds. JSON logs in production (readable
543
+ text while developing) where every line carries the request id, trace
544
+ id, route and user, and secrets (`password`, `token`, `api_key`, ... plus
545
+ `PYWEB_LOG_REDACT`) are redacted, including inside messages and URLs.
546
+ Prometheus `/metrics` (`PYWEB_METRICS_TOKEN`; requests, server functions,
547
+ queries, jobs, live sockets, mail, sign-ins, errors), added up across
548
+ `--workers` processes; `pyweb worker --metrics-port`. W3C `traceparent`
549
+ is continued, returned on every response and carried into the jobs a
550
+ request queues, so one trace covers the click and the job's work on
551
+ another machine. OpenTelemetry spans for requests, queries, jobs and
552
+ `with span("..."):` blocks (`pip install pyweb-stack[otel]`,
553
+ `PYWEB_OTEL=1`). `@app.on_error` hooks receive every unhandled error with
554
+ its request context.
555
+ - `pyweb worker --alive`: a health check for worker containers (the worker
556
+ touches a heartbeat file after each round that reached its job store);
557
+ compose and Kubernetes use it.
558
+
559
+ - **`pyweb new --template saas`**: accounts, projects and tasks with row
560
+ policies, `<Form>`s, live pages, a background job, a weekday cron email,
561
+ `/admin`, seeds, tests and a first migration, ready for `pyweb deploy`.
562
+ Templates with a database get `migrations/0001_initial.py` and a README.
563
+ - `TestClient.login(email, roles=[...])` signs a test in (creating the
564
+ account with the auth kit) without going through the sign-in form;
565
+ `logout()` signs out.
566
+ - `seeds.py` sees the app's Models by name; `pyweb.models.model("Post")`
567
+ looks one up anywhere.
568
+ - A server function parameter can take its rules from a default:
569
+ `title: str = Field(min=1, max=120)` (same as `Annotated[str, Field(...)]`).
570
+ - `<Input name="project_id" type="hidden" />` is just the input (for ids the
571
+ page sets with `values=`); `label=""` hides a field's label but keeps it as
572
+ the input's `aria-label`.
573
+
574
+ - **Editors know your data.** The language server completes Model fields
575
+ and query methods after `Post.`, keyword fields in `where(`/`create(`,
576
+ relations in `include("`, columns in `order("`, `Field(` options and a
577
+ `<Form>`'s fields in `name="`. It flags form fields the action doesn't
578
+ have, loops that read a relation their query didn't `include()` (N+1,
579
+ pointing at the line), and Models changed without a migration, with a
580
+ code lens that writes it. VS Code: "Create Migration from Model Changes",
581
+ "Apply Migrations", and snippets for Models, relations, form pages, live
582
+ pages, jobs, schedules, row policies and `use_auth`. The extension uses
583
+ the current language client (vscode-languageclient 10), is bundled
584
+ (12 files instead of 364), needs VS Code 1.91 or later, and is tested in
585
+ a real VS Code against the real language server in CI.
586
+ - **`pyweb db check`**: fails (exit 1) when the Models changed without a
587
+ migration, by applying the migrations to a scratch database, so it needs
588
+ no real database. For CI.
589
+ - **MCP tools for data**: `pyweb_db_schema` (Models, tables, drift),
590
+ `pyweb_db_query` (read-only SQL, capped, secrets redacted),
591
+ `pyweb_migrations` (status, diff, upgrade), `pyweb_jobs` (list, retry,
592
+ run) and `pyweb_requests` (each request's SQL, N+1 warnings, jobs, errors).
593
+
594
+ - **Playground 2.** Several files per app (import components from a second
595
+ `.pyweb` file; shared as one link, downloaded as a zip), a Requests tab
596
+ (each request's SQL with N+1 warnings, spans, jobs, emails, errors), a
597
+ Database tab (tables and rows, secrets redacted), a visitor picker to see
598
+ pages signed out, signed in or as an admin, background jobs run right
599
+ after the request that queued them, and SQLite loaded when an app has a
600
+ database. New examples: the blog (on Models) and the saas template.
601
+ - `Worker.run_inline()` runs due jobs on the calling thread (no thread pool).
602
+
603
+ - **`pyweb upgrade --check/--fix`** lists what an app needs to change for
604
+ 0.5, with file and line, and applies the safe rewrites:
605
+ `Model.configure(...)` → `App(database=...)`, and `pyweb db
606
+ migrate/rollback` and `pyweb deploy --target X` in Procfiles, scripts,
607
+ Dockerfiles and CI files.
608
+ - `PYWEB_STRICT_DEPRECATIONS=1` turns deprecation warnings into errors.
609
+ Deprecated in 0.5: `Model.configure()`, the in-memory `Queue`,
610
+ `RedisQueue` and `@task` (use `@app.job`), and `pyweb deploy
611
+ --target/--compose`.
612
+ - `pyweb check` warns about SQL built from strings passed to `execute()`,
613
+ pages that read `auth.user().x` without `login=True`, and Models owned by
614
+ a user without a row policy; `--strict` fails on any warning.
615
+
616
+ ### Changed
617
+ - The blog template and example use a Model instead of raw SQL.
618
+ - `pyweb lsp` answers requests it doesn't handle with "method not found"
619
+ (it answered `null`), as LSP clients expect.
620
+ - `PYWEB_MAX_CONNECTIONS` defaults to 10,000 per process (it was 256 for
621
+ the threaded server, which `pyweb serve --app module:factory` still uses).
622
+ - `pyweb.testing.serve` runs PyWeb's server.
623
+ - SQLite connections enforce foreign keys and use write-ahead logging.
624
+ - `Model.configure()` is deprecated in favour of `App(database=...)` /
625
+ `DATABASE_URL`; 0.4-style Models keep working (rows still read like
626
+ dicts: `row["name"]`).
627
+ - In production, a Model with no database is an error instead of an
628
+ in-memory SQLite file.
629
+
630
+ ### Security
631
+ - Sessions and feed tokens are verified only with keys derived per purpose.
632
+ The raw secret, still accepted in 0.4.4 for tokens from older versions,
633
+ is no longer a key.
634
+ - The default Content-Security-Policy allows inline `<style>` blocks by
635
+ their hashes instead of `'unsafe-inline'` (so injected markup can't add
636
+ a stylesheet that leaks page content), and adds `form-action 'self'`.
637
+
638
+ ### Fixed
639
+ - Rules on server function parameters (`Annotated[str, Field(max=40)]`) were
640
+ ignored: a direct call could send any value, and forms didn't get the
641
+ matching browser checks. They're enforced on every call now, with errors
642
+ per field (422).
643
+ - `owner: User = Field(readonly=True)` made a plain column instead of a
644
+ foreign key; options given with `Field(...)` now apply to the relation.
645
+ - `pyweb db seed --database URL` wrote to the app's own database; apps
646
+ without migrations yet couldn't be seeded before `pyweb dev` created
647
+ their tables.
648
+ - `pyweb dev` didn't reload pages larger than 1 KB after a change: they were
649
+ gzipped before the reload script could be added.
650
+ - `Model.query()...live()` in a page made a static page: the compiler only
651
+ recognised `live(db, sql)`.
652
+ - A value computed from a live query (`left = len([t for t in tasks if not
653
+ t["done"]])`), or from any server-loaded list a handler changes, was
654
+ computed once on the server and never updated in the browser. It now
655
+ follows its source there.
656
+ - A form for a Model with a read-only field (`owner: User =
657
+ Field(readonly=True)`) was refused with "owner is required" before the
658
+ server function could set the field.
659
+ - Validation messages named the field twice ("body Body is required").
660
+ - Schedules in UTC (including PyWeb's nightly cleanup) no longer need a
661
+ time zone database, which Windows and Pyodide (the playground) don't
662
+ ship; other zones explain that `pip install tzdata` provides one.
663
+ - The server answered a refused connection (503) or a rejected request and
664
+ closed at once; unread request bytes then made the kernel reset the
665
+ connection, which could destroy the response. It now closes politely
666
+ (half-close, drain briefly).
667
+ - While developing, a Model's table made on first use inside a request that
668
+ then failed was rolled back but still counted as made ("no such table"
669
+ until a restart). Tables are now made when the app loads, and a lazily
670
+ made one only counts once its transaction commits.
671
+ - Audit log entries written during a request that rolls back are kept
672
+ (written when the transaction ends, committed or not), and a failing
673
+ audit insert can no longer abort the request's transaction.
674
+ - `Site(..., rate_limit=False)` (used by `pyweb.testing.serve`) passed
675
+ `False` to the server instead of turning rate limits off.
676
+
677
+ ## [0.4.4]
678
+
679
+ Security and running in production. No app changes needed, and nobody
680
+ is signed out by the upgrade.
681
+
682
+ ### Added
683
+ - **Signing keys per purpose, and rotation.** Sessions, live-update feeds
684
+ and live query specs are signed with separate keys derived from
685
+ `PYWEB_AUTH_SECRET`. `PYWEB_AUTH_SECRET_PREVIOUS` keeps tokens from an
686
+ old secret working while you change it. Tokens from 0.4.3 and earlier
687
+ still work.
688
+ - **Sessions:** each login starts a new session; sessions renew on visits
689
+ and end 30 days after sign-in; `auth.revoke_user(user_id)` signs a user
690
+ out on every device (in memory, or shared with `RedisSessionVersions`).
691
+ - **Typed server function arguments:** `list[...]`, `dict[...]`,
692
+ `Optional`, `Literal`, dataclasses, Models and `Email` are checked
693
+ against their type hints, and unknown arguments are refused (422).
694
+ - **`pyweb/config.py`:** every `PYWEB_*` setting in one place, validated
695
+ at startup (a bad value stops the server with a clear message).
696
+ - **`PYWEB_REDIS_URL`:** shares rate limits (`RedisRateLimiter`) and live
697
+ updates between server processes.
698
+ - **`/readyz`** checks that every database the app opened answers (503
699
+ when one doesn't); `/healthz` stays a liveness check.
700
+ - **Graceful shutdown:** on SIGTERM and ASGI shutdown, live connections
701
+ end and background jobs get up to 10 seconds to finish.
702
+ - **`pyweb deploy --target compose --domain example.com`** puts Caddy in
703
+ front of the app with automatic HTTPS, keeps the app reachable only
704
+ through it, and sets `PYWEB_TRUST_PROXY=1` and Secure cookies. The k8s
705
+ target trusts its Ingress and uses `/readyz` for readiness. The
706
+ deployment docs explain reverse proxies, with nginx and Caddy examples.
707
+ - **`pyweb check --production`** lists settings that would leave a gap
708
+ (secret, HTTPS cookies, proxy, Redis with several workers, ...).
709
+ - **npm supply chain:** `pyweb.lock` records a SHA-256 per vendored file
710
+ and `pyweb build` refuses files that changed.
711
+ - **CI:** dependency vulnerability audit (pip-audit), CodeQL code
712
+ scanning, Dependabot, and signed build provenance for release files.
713
+ - **A much better playground** on the website: syntax highlighting with
714
+ the error line marked (click the line number to jump to it), a Console
715
+ that shows `print()` output, every server call with its arguments,
716
+ status and time, and errors from the page; a draggable divider;
717
+ phone/tablet/desktop preview widths and a reload button; highlighted
718
+ JavaScript with its gzipped size; Reset, Download (`app.pyweb`, ready
719
+ for `pyweb dev`) and autosave, so edits survive a reload; and a
720
+ Code/Preview switch on phones.
721
+ - Docs: a threat model, key rotation, session lifetime, a production
722
+ checklist, and the new settings.
723
+
724
+ ### Security
725
+ - Requests the browser marks `Sec-Fetch-Site: cross-site` can't call
726
+ server functions.
727
+ - Deeply nested JSON bodies crashed the request handler with a
728
+ `RecursionError`; bodies deeper than 32 levels or with more than
729
+ 10,000 values are now refused.
730
+ - `pyweb serve` and `pyweb dev` drop clients that stall (30 s), answer
731
+ 503 past 256 simultaneous connections and 431 to headers over 16 KB.
732
+ - At most 20 open live connections per client address (429 beyond).
733
+ - Pages that take longer than 30 seconds to render answer 504 instead of
734
+ holding a thread.
735
+ - New response headers: `Permissions-Policy`, `Cross-Origin-Opener-Policy`,
736
+ `Cross-Origin-Resource-Policy`, and `Strict-Transport-Security` when
737
+ `PYWEB_COOKIE_SECURE` is on. Both servers now send the same set.
738
+ - `RedisCache` no longer unpickles data unless `allow_pickle=True`
739
+ (unpickling can run code written by anyone with Redis access).
740
+
741
+ ### Fixed
742
+ - `@auth_required` server functions rejected sessions older than one
743
+ hour, while pages accepted them for 7 days.
744
+
745
+ ## [0.4.3]
746
+
747
+ A review release: bug fixes, security hardening and speed-ups. No API
748
+ removals; one new setting (`PYWEB_TRUST_PROXY`).
749
+
750
+ ### Fixed
751
+ - Pages showed stale data when a module-level value (`ITEMS = []`) was
752
+ changed by an `@server` function: the compiler copied its starting
753
+ value into browser code, and the page re-rendered with it. Values
754
+ that code changes now come from the server on each request.
755
+ - `db.transaction()`: a statement error that your code caught rolled
756
+ back the transaction's earlier writes while later ones still
757
+ committed. Errors inside a transaction now leave it to
758
+ `transaction()` (all or nothing).
759
+ - `db.stream()` inside a transaction waited for a second connection
760
+ (a 10 second hang with SQLite in memory); it now uses the
761
+ transaction's.
762
+ - `pyweb serve` sent SVG, images, fonts, JSON and WebAssembly as
763
+ `application/octet-stream` (browsers don't show an SVG sent that way).
764
+ - Your own files in `static/` (such as `app.css`) were cached by
765
+ browsers for a year, so deploys didn't show up for returning
766
+ visitors. They're now revalidated with an ETag (a `304` when
767
+ unchanged); content-hashed files, `?v=` URLs and npm packages are
768
+ still cached for a year.
769
+ - Client-side navigation could show a page prefetched before a server
770
+ call changed its data.
771
+ - `async def` server functions ignored `rpc_timeout`.
772
+ - The ASGI adapter kept only the last of repeated request headers, so
773
+ HTTP/2 clients sending each cookie separately lost all but one.
774
+ - `pyweb dts`: type aliases were never written, members on one line
775
+ (`{ a: string; b?: number }`) produced invalid Python, and optional
776
+ fields before required ones made the stub fail to import.
777
+ - Very long upload filenames are shortened (keeping the extension)
778
+ instead of failing to save.
779
+
780
+ ### Security
781
+ - RPC rate limits used the `X-Forwarded-For` header, which any client
782
+ can set (so the limit was easy to get around), and put every visitor
783
+ without a session cookie in one shared bucket. They now use the
784
+ connection's address; behind a reverse proxy set `PYWEB_TRUST_PROXY=1`
785
+ (or the number of proxies) to use the address your proxy saw.
786
+ - `auth.require_session` and `auth.login_response` only redirect to
787
+ paths on your site (`//evil.example` becomes `/`); the new
788
+ `auth.safe_next()` does the same for your own `?next=` handling.
789
+ - Magic-link tokens are signed separately from session cookies, so one
790
+ can't be used as the other. Links issued before the upgrade stop
791
+ working (they last 15 minutes).
792
+
793
+ ### Performance
794
+ - HTML, JavaScript, CSS, JSON and SVG are gzipped for browsers that
795
+ accept it (the shared runtime goes from 46 KB to 15 KB); static files
796
+ are compressed once and kept.
797
+ - Memory no longer grows without bound in long-running servers: the
798
+ rate limiter forgets idle callers, the realtime bus forgets channels
799
+ quiet for an hour, the in-memory cache drops expired entries, and
800
+ the job queue forgets jobs an hour after they finish.
801
+ - The job queue runs jobs on a pool of 8 threads instead of one thread
802
+ per job, and `wait()` returns as soon as the job finishes.
803
+ - `pyweb dev` no longer re-scans virtualenvs in the project folder
804
+ every 0.4 s, and reloads when `pyweb.lock` changes (after
805
+ `pyweb add`).
806
+
807
+ ## [0.4.2]
808
+
809
+ ### Added
810
+ - VS Code extension: **PyWeb: Run App** (a run button on `.pyweb`
811
+ files, Ctrl+F5), **New App** from any template, **Check App**, **Add
812
+ npm Package**, **Install or Update PyWeb** and **Set Up AI Assistant
813
+ (MCP)**, which adds the PyWeb MCP server to `.vscode/mcp.json`.
814
+ - VS Code extension: closing tags are added as you type, Emmet works in
815
+ markup, new snippets (`layout`, `error`, `stream`, `livequery`, `npm`,
816
+ `mount`), a status item that shows the PyWeb version in use, and an
817
+ icon.
818
+
819
+ ### Fixed
820
+ - VS Code extension: the language server failed with
821
+ `spawn python ENOENT` when no Python interpreter was selected. The
822
+ extension now looks for a Python with PyWeb (the selected
823
+ interpreter, `pyweb`, `py`, `python3`, `python`), offers to install
824
+ PyWeb when it's missing, and restarts when you change interpreter.
825
+
826
+ ## [0.4.1]
827
+
828
+ ### Added
829
+ - MCP server: `pyweb_routes` maps every page (route, path and query
830
+ parameters with types and defaults, title and head tags, layouts,
831
+ live-data variables), the layouts, error pages and server functions.
832
+ - MCP server: `pyweb_packages` adds, removes or lists npm packages for
833
+ browser code, with each package's exported names and the `npm(...)`
834
+ lines to bind them.
835
+ - MCP server: `add_ai_feature` and `make_data_live` prompts.
836
+ - MCP server: `pyweb_call` returns every chunk a streaming function
837
+ yields; `pyweb_render` reports the title, head tags and the layouts that
838
+ rendered; `pyweb_check` reports layouts, live variables, npm packages
839
+ and streaming functions.
840
+ - The AI guide (`pyweb_guide`, `AGENTS.md`) has sections with working
841
+ examples for multi-page apps, streaming and AI, live data and npm
842
+ packages.
843
+ - A new README with screenshots, and docs on publishing the VS Code
844
+ extension.
845
+
846
+ ### Fixed
847
+ - Markup right after a `def` line that ends in a comment
848
+ (`def Page(q: str = ""): # ...`) failed to compile.
849
+
850
+ ## [0.4.0]
851
+
852
+ Apps that are bigger, livelier and smarter: npm packages without
853
+ Node.js, multi-page apps with layouts and client-side navigation,
854
+ streaming for AI features, and page data that follows the database.
855
+
856
+ ### Added
857
+ - **npm packages without Node.js.** `pyweb add chart.js/auto` downloads a
858
+ package from the npm registry, checks its sha512 checksum, follows its
859
+ imports from the browser entry point and copies only the files it
860
+ needs (and its dependencies) into `static/vendor/`, pinning versions in
861
+ `pyweb.lock`. `pyweb add` with no arguments reinstalls from the lock;
862
+ `pyweb remove` takes a package out. Browser code binds exports with
863
+ `Chart = npm("chart.js/auto")` or `npm("pkg", "Export")`. Calling a
864
+ class constructs it and keyword arguments become an options object.
865
+ Each page imports only the packages it uses, through an import map
866
+ that the Content Security Policy allows by hash. CommonJS-only and
867
+ Node.js-only packages are refused with an explanation, and an
868
+ uninstalled package is a compile error that names the command to run.
869
+ - **`ref={el}`** sets a page variable to an element, in time for
870
+ `on_mount`, for libraries that draw into the page.
871
+ - Changing an attribute of a package's object (`chart.value = 5`)
872
+ re-renders whatever shows it.
873
+ - Web components (custom elements from npm) work in markup.
874
+ - `pyweb lsp`: hover shows an npm binding's installed version and
875
+ TypeScript signature; completion offers installed packages in
876
+ `npm("` and a module's exports after `name.`.
877
+ - New docs page: npm packages.
878
+ - **Layouts.** `@app.layout` wraps every page (or, with
879
+ `@app.layout("/admin")`, pages under a prefix) in shared markup;
880
+ `{children}` marks where the page goes. Layouts nest, have their own
881
+ server code, state and handlers, and pages can opt out with
882
+ `layout=None` or pick one with `layout="Name"`.
883
+ - **Client-side navigation.** Links between pages fetch the next page's
884
+ HTML and swap it in below the layouts both pages share, so layout
885
+ state (an open menu, a player) survives. Links are prefetched on
886
+ hover or focus, back/forward restore the scroll position, and anything
887
+ unusual falls back to a full page load. `navigate("/path")` from
888
+ `pyweb.browser` does the same from browser code; `on_unmount` runs
889
+ when a page is left; `App(client_nav=False)` turns it off.
890
+ - **Current links.** Links to the current page get
891
+ `aria-current="page"` and links to its parent sections
892
+ `aria-current="true"`, on the server and after each navigation.
893
+ - **Head tags.** `@app.page(description=..., image=..., noindex=...)` and
894
+ `head(title=..., description=...)` in page code add description, Open
895
+ Graph and Twitter tags; `App(base_url=...)` adds canonical URLs and
896
+ absolute image links.
897
+ - **Error pages in `.pyweb`.** `@app.error(404)` / `@app.error(500)` pages
898
+ take `path`, `status`, `message` or `request_id` and use root layouts.
899
+ - **Typed query parameters.** Page parameters that aren't in the route
900
+ are read from the query string and converted by annotation (`int`,
901
+ `float`, `bool`, `list[...]`); bad or missing values answer 400.
902
+ - New docs page: Layouts & navigation.
903
+ - **Streaming server functions.** A `@server` function that `yield`s
904
+ sends each value as it's produced (NDJSON over the same RPC endpoint);
905
+ browser code reads it with `async for`. `stream.cancel()` or leaving the
906
+ loop aborts the request and closes the generator on the server, so an
907
+ upstream AI call stops too. Errors raised part-way arrive as
908
+ `RPCError`. Works with `async def` generators, under `pyweb serve`,
909
+ `pyweb dev` and ASGI, and `TestClient.rpc` returns the streamed values.
910
+ - **`<Markdown text={...} />`** (from `pyweb`) renders Markdown to safe
911
+ HTML on the server and updates it in the browser as the text changes,
912
+ with the same rules in both places. Raw HTML is shown as text and
913
+ links only accept http(s), mailto and relative URLs.
914
+ - **`ai-chat` template and example**: a streaming chat with Stop and
915
+ Markdown replies that talks to Anthropic, any OpenAI-compatible server
916
+ (OpenAI, Ollama, vLLM, ...) or a built-in demo model, chosen by
917
+ environment variables. `pyweb new NAME --template ai-chat`.
918
+ - New docs page: Building AI apps.
919
+ - **Live data.** `rows = live(db, "select ...", params)` in a page or
920
+ layout renders the rows and keeps them current: writes through
921
+ `pyweb.db` (and `pyweb.models`) announce their table after commit, each
922
+ distinct query re-runs once per change (bursts coalesced) and sends
923
+ rows to every page showing it only when they changed. Pages follow a
924
+ signed feed; `db.notify("table")` covers writes made elsewhere. With
925
+ `RedisBus` it works across processes, and any process can take over a
926
+ query from the page's signed description.
927
+ - New docs page: Live data.
928
+ - **`watch(lambda: value, handler)`** (from `pyweb.browser`) runs a
929
+ handler whenever a value changes, for work outside markup such as
930
+ redrawing a chart or saving a draft.
931
+ - `pyweb add` applies packages' `browser` field (browser versions of
932
+ files, modules turned off), so packages such as ethers install.
933
+ - New examples: **dashboard** (live queries and a Chart.js chart from
934
+ npm), **site** (layouts, navigation, query parameters, a 404 page) and
935
+ **ai-chat**. The playground has the site and AI chat examples.
936
+ - New docs page: Recipe: wallets & web3 (wallet sign-in with ethers and
937
+ eth-account).
938
+ - A layout's `{children}` can sit on a line of its own.
939
+
940
+ ### Fixed
941
+ - Subscriptions and cleanups set up in `on_mount` now end when the page
942
+ is left (they were never stopped).
943
+
944
+ ### Changed
945
+ - The experimental `pyweb.live` module (in-memory live tables) is now
946
+ `pyweb.livetable`; `pyweb.live` is the new live-query function.
947
+ - `pyweb.npm` (TypeScript declarations to dataclasses) is now
948
+ `pyweb.dts`, and its command is `pyweb dts`. `pyweb build` no longer
949
+ writes an esm.sh import map; packages come from `pyweb.lock`.
950
+ - The default Content Security Policy adds `worker-src 'self' blob:` so
951
+ packages can start Web Workers.
952
+ - The starter stylesheet styles Markdown and chat bubbles.
953
+ - The browser runtime is about 14 KB gzipped (was about 11 KB), for
954
+ client-side navigation and npm support.
955
+ - New website design: a colour system where blue means the browser and
956
+ amber the server, self-hosted Inter / Bricolage Grotesque / JetBrains
957
+ Mono, dark code windows, and a landing page that shows each line of an
958
+ app with where it runs (worked out by the compiler) next to the real
959
+ compiled output.
960
+ - The example apps and the `pyweb new` stylesheet use the new palette.
961
+
962
+ ## [0.3.0]
963
+
964
+ Faster pages that feel like an app, apps that span several files, and
965
+ tools for editors, the browser and AI assistants.
966
+
967
+ ### Added
968
+ - **Hydration.** Pages now adopt the server-rendered DOM instead of
969
+ rebuilding it: nodes are kept, focus and text typed before the script
970
+ loads survive, and typed values reach their variables. When the server
971
+ HTML doesn't match, the page falls back to a client render and logs
972
+ a warning. The page root carries `data-pw-mode="hydrated"` or
973
+ `"rendered"`.
974
+ - **Live updates.** `publish(name, data)` in server code pushes to every
975
+ page that called `subscribe(channel_feed, handler)`, over Server-Sent
976
+ Events with a polling fallback. Feeds come from `channel(name)` while
977
+ rendering: signed, expiring, and positioned so no message published
978
+ after the render is missed. `pyweb dev`, `pyweb serve` and the ASGI
979
+ adapter stream; `realtime.use_bus()` shares the bus (e.g. Redis)
980
+ between processes. The chat example uses it instead of polling.
981
+ - **Multi-file apps.** `from widgets import Card, save, COLOR` imports
982
+ components, `@server` functions and constants from `widgets.pyweb` (or
983
+ `ui/cards.pyweb` as `ui.cards`). Each file keeps its own names in the
984
+ generated JavaScript, imported files run as real modules on the
985
+ server, `pyweb build` copies them into `dist/`, and errors name the
986
+ file they're in. Pages stay in the app file; circular imports and
987
+ duplicate server-function names are compile errors.
988
+ - **Editor support.** `pyweb lsp` is a language server (stdio, standard
989
+ library only): compile errors and security warnings as you type,
990
+ hover showing where each name runs and why, completion for components,
991
+ props and tags, go to definition across files, and an outline. The new
992
+ VS Code extension (`editors/vscode`) adds highlighting and snippets and
993
+ starts it; releases attach a `.vsix`. Setup for Neovim and Helix is in
994
+ the command-line docs.
995
+ - **Playground** on the website: edit an app and run it in the browser,
996
+ with the real compiler, server rendering, `@server` functions,
997
+ sessions and live updates running on Pyodide. Shows the compiled
998
+ JavaScript and what runs where, reports errors with a fix, and shares
999
+ code as links.
1000
+ - **MCP tools** `pyweb_screenshot` (open a page in headless Chromium,
1001
+ run click/fill/press steps, get a screenshot, the page text, console
1002
+ errors and hydration status) and `pyweb_test` (run the app's pytest
1003
+ tests and report failures). New apps include a starter `test_app.py`.
1004
+ - `python -m pyweb.bench --max-ssr-ms N --max-compile-ms N` fails when
1005
+ a benchmark exceeds its budget; CI runs it.
1006
+
1007
+ ### Removed
1008
+ - The old `pyweb.lsp` helper functions (`complete`, `DocumentState`,
1009
+ `complete_*`, `diagnostics_for_compile_error`, `hover(compiled, name)`,
1010
+ `boundary_lens`); they described an earlier API and no editor used them.
1011
+
1012
+ ### Changed
1013
+ - `import pyweb` no longer needs the `sqlite3` module (it's imported when
1014
+ a SQLite database is opened), and password hashing falls back to a
1015
+ pure-Python PBKDF2 on Pythons without OpenSSL. Both make PyWeb run on
1016
+ Pyodide.
1017
+ - `/__pyweb/events` and `/__pyweb/poll` require a signed `?feed=`; the
1018
+ unauthenticated `?channel=` form is gone, so channels are no longer
1019
+ readable by anyone who guesses a name.
1020
+ - Server rendering caches compiled template expressions; rendering a
1021
+ page is about twice as fast.
1022
+ - Generated page modules pass element children as a function and wrap
1023
+ every child in a call (`$t`, `$dyn`), so nodes are created in document
1024
+ order.
1025
+
1026
+ ## [0.2.0]
1027
+
1028
+ Tools for building PyWeb apps with AI assistants.
1029
+
1030
+ ### Added
1031
+ - `pyweb mcp`: a Model Context Protocol server (stdio, standard library
1032
+ only) for Claude Code, Cursor, Claude Desktop, VS Code and other MCP
1033
+ clients. Tools: `pyweb_guide`, `pyweb_new_app`, `pyweb_check` (errors
1034
+ with line numbers and fix hints), `pyweb_inspect`, `pyweb_compiled`,
1035
+ `pyweb_render` and `pyweb_call` (session cookies persist between calls);
1036
+ resources for the guide and templates; a `build_pyweb_app` prompt.
1037
+ Tested against the official MCP Python SDK.
1038
+ - An AI-oriented guide to writing `.pyweb` apps, shipped in the package.
1039
+ - `pyweb new --template blank|counter|todo|blog|auth|chat`; new projects
1040
+ include `AGENTS.md` and `CLAUDE.md` for coding agents.
1041
+ - The docs site publishes `llms.txt` and `llms-full.txt`, and has a new
1042
+ "AI assistants & MCP" page.
1043
+
1044
+ ## [0.1.0]
1045
+
1046
+ The first public release, published on PyPI as `pyweb-stack`. The
1047
+ unpublished prototype was labelled "1.0" but only the simplest counter
1048
+ pattern worked in a browser; this release rebuilds the compiler, runtime
1049
+ and server around a design that works end to end, and is verified in a
1050
+ real browser.
1051
+
1052
+ ### Language and compiler
1053
+ - `.pyweb` parser with a character scanner: tags and expressions may span
1054
+ lines, braces and strings nest inside `{...}`, `elif` chains, HTML
1055
+ comments, and exact line numbers (the Python half keeps the source's
1056
+ line count; this also fixes crashes on Python 3.10/3.11).
1057
+ - Python → JavaScript translation of a documented subset with Python
1058
+ semantics (truthiness, `==` on containers, negative indexing, floor
1059
+ division/modulo, string/list/dict/set methods, Python exceptions).
1060
+ Unsupported code is a compile error with `file:line`. Verified
1061
+ differentially against CPython.
1062
+ - State inference: signals (mutated by handlers or bound), computeds,
1063
+ constants; initial values from literals, the browser, or the server.
1064
+ Mutations (`append`, item assignment, `del`, …) are copy-on-write.
1065
+ - Components with props, defaults and `children`; `on_mount` hook;
1066
+ page titles; `App(title=, stylesheets=, lang=)`.
1067
+ - `@server` calls from handlers compile to awaited typed RPC; async
1068
+ propagates through handler calls.
1069
+ - Only values browser code reads are serialised; values derived from
1070
+ server data are computed on the server. Secret-looking names read by
1071
+ browser code are rejected.
1072
+
1073
+ ### Runtime
1074
+ - New browser runtime: dependency-tracked signals, cached computeds,
1075
+ owned effects with disposal, batching; keyed lists and conditionals in
1076
+ marker-bounded regions; two-way binding for text, number, checkbox,
1077
+ radio and select; `javascript:` URL blocking; RPC client with typed
1078
+ errors.
1079
+
1080
+ ### Server
1081
+ - Pages render per request with real server values; `request`,
1082
+ `session`, `redirect`, `NotFound`; typed route parameters.
1083
+ - `@server` functions are registered automatically in `dev`, `serve`,
1084
+ tests and ASGI.
1085
+ - New ASGI adapter: `pyweb.asgi.create_app`.
1086
+ - RPC: JSON-only and same-origin checks, rate limiting, timeouts that
1087
+ actually return on time, async functions, JSON conversion for
1088
+ dataclasses/datetimes/Decimals.
1089
+ - Content-Security-Policy and security headers on HTML responses.
1090
+
1091
+ ### Tooling
1092
+ - `pyweb dev`: live reload and an in-browser compile-error overlay.
1093
+ - `pyweb build` produces a self-contained `dist/` (source, manifest with
1094
+ gzip sizes, hashed assets, working Dockerfile); `pyweb serve` runs it.
1095
+ - Token-aware JS minifier (the previous one altered string literals).
1096
+ - `pyweb.testing.TestClient` and `pyweb.testing.serve` for tests.
1097
+ - Clean `file:line: message` compile errors from the CLI.
1098
+
1099
+ ### Data, auth, jobs
1100
+ - `?` placeholders work on Postgres and MySQL; lazy connection pools.
1101
+ - WebAuthn: challenge, origin and sign-count checks; DER signatures; uses
1102
+ `cryptography` when installed.
1103
+ - Redis bus and queue work against real Redis.
1104
+
1105
+ ### Fixed
1106
+ - Migrations: the `pyweb_migrations` journal table can now be created on
1107
+ MySQL (`VARCHAR(255)` key instead of `TEXT`).
1108
+ - `pyweb serve` crashed on startup (logger misconfiguration).
1109
+ - Production builds referenced a hashed runtime the page modules never
1110
+ imported.
1111
+ - Installed packages were missing the browser runtime file.
1112
+ - `transaction()` did not roll back on Postgres/MySQL pools.
1113
+ - `sqlite:///file.db` pointed at the filesystem root.
1114
+ - `RedisBus.since()` duplicated and mis-ordered messages;
1115
+ `RedisQueue.drain(timeout=0)` blocked forever.
1116
+ - Jobs raising `TypeError` ran twice.
1117
+ - `Query.order_by()` ignored `-field` and did not validate a single field.
1118
+ - The deploy Dockerfile ran a non-existent `serve --dir` flag.
1119
+
1120
+ ### Removed
1121
+ - Modules the framework no longer uses: `pyweb.components` (pure-Python
1122
+ element API), `pyweb.routing` (unused router), `pyweb.reactive`
1123
+ (server-side `Signal`/`Computed`/`Effect`/`live`), and
1124
+ `pyweb.compiler.placement` (superseded by the compiler's boundary checks).
1125
+ - The `@browser` and `@shared` decorators, which had no effect.
1126
+ - `pyweb.compiler.rpc.client_stub`/`server_handler` and unused AST classes.
1127
+ - `docs/BUGLOG.md` (it described the prototype's internals) and the committed
1128
+ `website/dist/`; the Pages workflow builds the site.
1129
+ - The regex-based codegen, the virtual test client that only recorded
1130
+ clicks, documentation "snippets" that did not use PyWeb, island and
1131
+ streaming-SSR string helpers without runtime support, and the no-op
1132
+ `--security-scan` flag.
1133
+
1134
+ ## Pre-release prototype (unpublished)
1135
+ - Initial prototype.