pyweb-stack 0.6.1__tar.gz → 0.6.3__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 (400) hide show
  1. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/CHANGELOG.md +240 -0
  2. {pyweb_stack-0.6.1/pyweb_stack.egg-info → pyweb_stack-0.6.3}/PKG-INFO +19 -4
  3. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/README.md +14 -3
  4. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/THREAT_MODEL.md +38 -10
  5. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/01-introduction.md +4 -0
  6. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/02-quickstart.md +12 -1
  7. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/08-pages-routing-assets.md +40 -0
  8. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/11-testing.md +30 -0
  9. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/12-deployment.md +7 -0
  10. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/14-cli.md +20 -11
  11. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/16-limitations-roadmap.md +30 -7
  12. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/17-ai-assistants.md +20 -0
  13. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/18-npm-packages.md +4 -0
  14. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/23-forms.md +29 -2
  15. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/27-upgrading.md +26 -0
  16. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/29-performance.md +36 -0
  17. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/32-desktop.md +4 -0
  18. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/34-ui-and-motion.md +21 -1
  19. pyweb_stack-0.6.3/docs/35-dev-loop.md +176 -0
  20. pyweb_stack-0.6.3/docs/36-routes.md +130 -0
  21. pyweb_stack-0.6.3/docs/37-i18n.md +90 -0
  22. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyproject.toml +4 -3
  23. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/__init__.py +8 -9
  24. pyweb_stack-0.6.3/pyweb/_templates.py +18 -0
  25. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/a11y.py +52 -0
  26. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ai/guide.md +16 -0
  27. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/app.py +70 -7
  28. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/app_loader.py +48 -4
  29. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/asgi.py +3 -0
  30. pyweb_stack-0.6.3/pyweb/assist.py +394 -0
  31. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/auth.py +10 -3
  32. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/__init__.py +12 -6
  33. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/admin.py +23 -1
  34. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/oauth.py +1 -1
  35. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/pages.py +17 -11
  36. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/bench.py +35 -3
  37. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/browser.py +29 -0
  38. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/build.py +11 -6
  39. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cache.py +43 -16
  40. pyweb_stack-0.6.3/pyweb/checks.py +111 -0
  41. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cli/__init__.py +230 -274
  42. pyweb_stack-0.6.3/pyweb/compiler/diskcache.py +91 -0
  43. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/lower.py +61 -3
  44. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/parser.py +56 -0
  45. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/pipeline.py +34 -5
  46. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/pyjs.py +7 -0
  47. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/config.py +20 -1
  48. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/context.py +51 -10
  49. pyweb_stack-0.6.3/pyweb/csp.py +60 -0
  50. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/__init__.py +112 -16
  51. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/migrate.py +1 -1
  52. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/rls.py +22 -13
  53. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/gate.py +1 -1
  54. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/shell.py +2 -2
  55. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/devlink.py +39 -5
  56. pyweb_stack-0.6.3/pyweb/devopen.py +92 -0
  57. pyweb_stack-0.6.3/pyweb/devserver.py +389 -0
  58. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/doctor.py +1 -1
  59. pyweb_stack-0.6.3/pyweb/errorboundary.py +22 -0
  60. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/forms.py +45 -15
  61. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/hosting.py +46 -6
  62. pyweb_stack-0.6.3/pyweb/htmlquery.py +143 -0
  63. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/http.py +7 -0
  64. pyweb_stack-0.6.3/pyweb/i18n.py +437 -0
  65. pyweb_stack-0.6.3/pyweb/images.py +113 -0
  66. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/redis.py +21 -13
  67. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/worker.py +35 -3
  68. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/livedata.py +41 -11
  69. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp.py +39 -1
  70. pyweb_stack-0.6.3/pyweb/lsp_refs.py +280 -0
  71. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp.py +123 -198
  72. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp_http.py +33 -3
  73. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp_tools.py +1 -1
  74. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/__init__.py +11 -0
  75. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/query.py +16 -0
  76. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/http.py +12 -0
  77. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/server.py +69 -16
  78. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/openapi.py +50 -0
  79. pyweb_stack-0.6.3/pyweb/pagecache.py +144 -0
  80. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/plugins.py +12 -2
  81. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/push.py +25 -9
  82. pyweb_stack-0.6.3/pyweb/qr.py +273 -0
  83. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/realtime.py +26 -21
  84. pyweb_stack-0.6.3/pyweb/redisutil.py +65 -0
  85. pyweb_stack-0.6.3/pyweb/routes.py +408 -0
  86. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rpc.py +26 -21
  87. pyweb_stack-0.6.3/pyweb/runtime/browser/boundary.js +48 -0
  88. pyweb_stack-0.6.3/pyweb/runtime/browser/dev/assist.js +156 -0
  89. pyweb_stack-0.6.3/pyweb/runtime/browser/dev/inspect.js +138 -0
  90. pyweb_stack-0.6.3/pyweb/runtime/browser/dev/reload.js +180 -0
  91. pyweb_stack-0.6.3/pyweb/runtime/browser/dev/timeline.js +144 -0
  92. pyweb_stack-0.6.3/pyweb/runtime/browser/dev/welcome.js +46 -0
  93. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/forms.js +133 -5
  94. pyweb_stack-0.6.3/pyweb/runtime/browser/i18n.js +42 -0
  95. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/pwa.js +1 -1
  96. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/runtime.js +9 -7
  97. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/index.js +1 -1
  98. pyweb_stack-0.6.3/pyweb/runtime/browser/ui/stepper.js +99 -0
  99. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/server/__init__.py +141 -31
  100. pyweb_stack-0.6.3/pyweb/scaffold.py +212 -0
  101. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/security.py +39 -0
  102. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/serve.py +6 -63
  103. pyweb_stack-0.6.3/pyweb/share.py +173 -0
  104. pyweb_stack-0.6.3/pyweb/sourcemap.py +79 -0
  105. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ssr.py +30 -5
  106. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ssr_codegen.py +2 -0
  107. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/storage.py +13 -8
  108. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/devtools.py +8 -9
  109. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/instruments.py +2 -0
  110. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/testing.py +79 -0
  111. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/data.pyweb +35 -4
  112. pyweb_stack-0.6.3/pyweb/ui/kit/image.pyweb +32 -0
  113. pyweb_stack-0.6.3/pyweb/ui/kit/stepper.pyweb +39 -0
  114. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/static/ui.css +20 -0
  115. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/upgrade.py +12 -0
  116. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/webhooks.py +8 -5
  117. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3/pyweb_stack.egg-info}/PKG-INFO +19 -4
  118. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/SOURCES.txt +44 -0
  119. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/requires.txt +5 -0
  120. pyweb_stack-0.6.3/tests/test_assist.py +243 -0
  121. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_authkit.py +89 -1
  122. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_backplane.py +8 -0
  123. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cli_deploy.py +3 -2
  124. pyweb_stack-0.6.3/tests/test_compile_cache.py +165 -0
  125. pyweb_stack-0.6.3/tests/test_compiler_errors.py +13 -0
  126. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_db_production.py +13 -0
  127. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dev_reload.py +5 -3
  128. pyweb_stack-0.6.3/tests/test_devopen.py +87 -0
  129. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dx_v5.py +1 -1
  130. pyweb_stack-0.6.3/tests/test_env_documented.py +45 -0
  131. pyweb_stack-0.6.3/tests/test_first_run.py +119 -0
  132. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_forms_v5.py +50 -1
  133. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_http_fetch.py +2 -1
  134. pyweb_stack-0.6.3/tests/test_i18n.py +126 -0
  135. pyweb_stack-0.6.3/tests/test_images.py +83 -0
  136. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_v5.py +49 -3
  137. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_livedata.py +14 -0
  138. pyweb_stack-0.6.3/tests/test_lsp_refs.py +112 -0
  139. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp.py +1 -1
  140. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp_v6.py +11 -0
  141. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_net.py +78 -0
  142. pyweb_stack-0.6.3/tests/test_pagecache.py +130 -0
  143. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_push.py +16 -1
  144. pyweb_stack-0.6.3/tests/test_qr.py +103 -0
  145. pyweb_stack-0.6.3/tests/test_routes.py +258 -0
  146. pyweb_stack-0.6.3/tests/test_security_063.py +95 -0
  147. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_server_db.py +2 -2
  148. pyweb_stack-0.6.3/tests/test_share.py +180 -0
  149. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_static_serving.py +1 -1
  150. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_streaming.py +18 -1
  151. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_systems.py +58 -1
  152. pyweb_stack-0.6.3/tests/test_testing_helpers.py +132 -0
  153. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_trusted_types.py +9 -1
  154. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_webhooks.py +1 -0
  155. pyweb_stack-0.6.1/pyweb/_templates.py +0 -3
  156. pyweb_stack-0.6.1/pyweb/runtime/browser/dev/reload.js +0 -102
  157. pyweb_stack-0.6.1/tests/test_compile_cache.py +0 -88
  158. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/LICENSE +0 -0
  159. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/MANIFEST.in +0 -0
  160. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/03-tutorial.md +0 -0
  161. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/04-pyweb-files.md +0 -0
  162. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/05-reactivity.md +0 -0
  163. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/06-server-functions.md +0 -0
  164. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/07-browser-python.md +0 -0
  165. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/09-data.md +0 -0
  166. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/10-auth.md +0 -0
  167. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/13-security.md +0 -0
  168. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/15-toolkit.md +0 -0
  169. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/19-layouts-navigation.md +0 -0
  170. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/20-ai-apps.md +0 -0
  171. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/21-live-data.md +0 -0
  172. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/22-recipe-web3.md +0 -0
  173. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/24-jobs.md +0 -0
  174. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/25-scaling.md +0 -0
  175. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/26-observability.md +0 -0
  176. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/28-saas-tutorial.md +0 -0
  177. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/30-teams.md +0 -0
  178. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/31-pwa-push.md +0 -0
  179. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/33-webhooks-files.md +0 -0
  180. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/_speedups.py +0 -0
  181. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/html.py +0 -0
  182. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/models.py +0 -0
  183. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/orgpages.py +0 -0
  184. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/orgs.py +0 -0
  185. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/passwords.py +0 -0
  186. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/privacy.py +0 -0
  187. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/teams.py +0 -0
  188. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/throttle.py +0 -0
  189. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/tokens.py +0 -0
  190. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/webauthn.py +0 -0
  191. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cli/__main__.py +0 -0
  192. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/__init__.py +0 -0
  193. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/ast.py +0 -0
  194. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/__init__.py +0 -0
  195. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/ir.py +0 -0
  196. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/errors.py +0 -0
  197. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/rpc.py +0 -0
  198. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/csp_report.py +0 -0
  199. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/css.py +0 -0
  200. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/dialect.py +0 -0
  201. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/schema.py +0 -0
  202. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/seeds.py +0 -0
  203. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/decorators.py +0 -0
  204. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/__init__.py +0 -0
  205. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/legacy.py +0 -0
  206. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/plan.py +0 -0
  207. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/targets.py +0 -0
  208. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deprecation.py +0 -0
  209. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/__init__.py +0 -0
  210. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/package.py +0 -0
  211. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/dts.py +0 -0
  212. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/fieldcrypt.py +0 -0
  213. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/filegc.py +0 -0
  214. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/__init__.py +0 -0
  215. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/builtin.py +0 -0
  216. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/core.py +0 -0
  217. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/db.py +0 -0
  218. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/legacy.py +0 -0
  219. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/memory.py +0 -0
  220. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/schedule.py +0 -0
  221. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/keys.py +0 -0
  222. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/livetable.py +0 -0
  223. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_data.py +0 -0
  224. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_teams.py +0 -0
  225. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_ui.py +0 -0
  226. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mail.py +0 -0
  227. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/markdown.py +0 -0
  228. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/fields.py +0 -0
  229. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/relations.py +0 -0
  230. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/motion.py +0 -0
  231. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/__init__.py +0 -0
  232. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/live.py +0 -0
  233. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/ws.py +0 -0
  234. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/observability.py +0 -0
  235. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/packages.py +0 -0
  236. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/platform.py +0 -0
  237. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/profile.py +0 -0
  238. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/pwa.py +0 -0
  239. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/py.typed +0 -0
  240. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rooms.py +0 -0
  241. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rules.py +0 -0
  242. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/auth.js +0 -0
  243. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/desktop.js +0 -0
  244. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/live.js +0 -0
  245. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/markdown.js +0 -0
  246. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-api.js +0 -0
  247. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-count.js +0 -0
  248. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-drag.js +0 -0
  249. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-layout.js +0 -0
  250. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-math.js +0 -0
  251. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion.js +0 -0
  252. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/sw.js +0 -0
  253. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/accordion.js +0 -0
  254. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/calendar.js +0 -0
  255. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/carousel.js +0 -0
  256. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/checkbox.js +0 -0
  257. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/collapsible.js +0 -0
  258. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/combobox.js +0 -0
  259. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/command.js +0 -0
  260. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/context-menu.js +0 -0
  261. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/core.js +0 -0
  262. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/dialog.js +0 -0
  263. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/hover-card.js +0 -0
  264. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/listbox.js +0 -0
  265. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menu.js +0 -0
  266. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menubar.js +0 -0
  267. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/navigation-menu.js +0 -0
  268. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/otp.js +0 -0
  269. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/popover.js +0 -0
  270. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/radio-group.js +0 -0
  271. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/resizable.js +0 -0
  272. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/select.js +0 -0
  273. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/sidebar.js +0 -0
  274. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/slider.js +0 -0
  275. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tabs.js +0 -0
  276. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toaster.js +0 -0
  277. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle-group.js +0 -0
  278. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle.js +0 -0
  279. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tooltip.js +0 -0
  280. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/server/argcheck.py +0 -0
  281. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/sbom.py +0 -0
  282. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/secret_scan.py +0 -0
  283. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/sync.py +0 -0
  284. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/__init__.py +0 -0
  285. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/logs.py +0 -0
  286. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/metrics.py +0 -0
  287. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/tracing.py +0 -0
  288. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/ai-chat.pyweb +0 -0
  289. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/app.css +0 -0
  290. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/auth.pyweb +0 -0
  291. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/blog.pyweb +0 -0
  292. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/chat.pyweb +0 -0
  293. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/counter.pyweb +0 -0
  294. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/desktop/app.pyweb +0 -0
  295. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/desktop/test_app.py +0 -0
  296. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/app.pyweb +0 -0
  297. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/seeds.py +0 -0
  298. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/test_app.py +0 -0
  299. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/todo.pyweb +0 -0
  300. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/tenancy.py +0 -0
  301. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/__init__.py +0 -0
  302. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/cli.py +0 -0
  303. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/extending.py +0 -0
  304. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/button.pyweb +0 -0
  305. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/card.pyweb +0 -0
  306. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/carousel.pyweb +0 -0
  307. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/chart.pyweb +0 -0
  308. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/command.pyweb +0 -0
  309. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/controls.pyweb +0 -0
  310. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/date.pyweb +0 -0
  311. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/dialog.pyweb +0 -0
  312. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/disclosure.pyweb +0 -0
  313. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/display.pyweb +0 -0
  314. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/layout.pyweb +0 -0
  315. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/menu.pyweb +0 -0
  316. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/overlay.pyweb +0 -0
  317. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/style.pyweb +0 -0
  318. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/serve.py +0 -0
  319. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/source_edit.py +0 -0
  320. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/theme.py +0 -0
  321. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/uploads.py +0 -0
  322. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  323. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/entry_points.txt +0 -0
  324. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/top_level.txt +0 -0
  325. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/setup.cfg +0 -0
  326. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/setup.py +0 -0
  327. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_account_privacy.py +0 -0
  328. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_api_tokens.py +0 -0
  329. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth.py +0 -0
  330. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_contract.py +0 -0
  331. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_security.py +0 -0
  332. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_sessions.py +0 -0
  333. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_v1.py +0 -0
  334. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_authkit_units.py +0 -0
  335. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_browser_build.py +0 -0
  336. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_budget.py +0 -0
  337. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cli_test.py +0 -0
  338. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_codegen.py +0 -0
  339. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_compiler.py +0 -0
  340. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cron.py +0 -0
  341. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_customise.py +0 -0
  342. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_data_layer.py +0 -0
  343. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_deploy_upload_render.py +0 -0
  344. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_deploy_v5.py +0 -0
  345. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_desktop.py +0 -0
  346. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_docs.py +0 -0
  347. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_doctor.py +0 -0
  348. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dts.py +0 -0
  349. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_e2e.py +0 -0
  350. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_encrypted_fields.py +0 -0
  351. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_error_messages.py +0 -0
  352. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_examples.py +0 -0
  353. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_filegc.py +0 -0
  354. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_forms_security_obs.py +0 -0
  355. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_fuzz_requests.py +0 -0
  356. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_app.py +0 -0
  357. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_cache_realtime.py +0 -0
  358. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_keys_sessions.py +0 -0
  359. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_live_patches.py +0 -0
  360. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_lsp.py +0 -0
  361. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_lsp_teams.py +0 -0
  362. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_markdown.py +0 -0
  363. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp_teams.py +0 -0
  364. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_migrations_property.py +0 -0
  365. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_migrations_v5.py +0 -0
  366. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_models_db.py +0 -0
  367. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_motion.py +0 -0
  368. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_multifile.py +0 -0
  369. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_multipage.py +0 -0
  370. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_observability.py +0 -0
  371. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_openapi.py +0 -0
  372. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_org_pages.py +0 -0
  373. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_packages.py +0 -0
  374. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_parser.py +0 -0
  375. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_parser_v1.py +0 -0
  376. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_plugins_platform_bench.py +0 -0
  377. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_production_gaps.py +0 -0
  378. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_profile.py +0 -0
  379. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_pwa.py +0 -0
  380. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_pyjs_semantics.py +0 -0
  381. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rate_per_function.py +0 -0
  382. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_reactivity.py +0 -0
  383. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_realtime_transport.py +0 -0
  384. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_reference_apps.py +0 -0
  385. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_request_hardening.py +0 -0
  386. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rpc_placement.py +0 -0
  387. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rpc_production.py +0 -0
  388. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_runtime_signals.py +0 -0
  389. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_sbom.py +0 -0
  390. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_secret_scan.py +0 -0
  391. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_speedups.py +0 -0
  392. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_ssr_codegen.py +0 -0
  393. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_static_hashing.py +0 -0
  394. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_sync_live.py +0 -0
  395. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_telemetry.py +0 -0
  396. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_tenancy.py +0 -0
  397. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_testing_orgs.py +0 -0
  398. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_threat_model.py +0 -0
  399. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_ui_kit.py +0 -0
  400. {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_website.py +0 -0
@@ -6,6 +6,246 @@ project uses [semantic versioning](https://semver.org).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.3]
10
+
11
+ An audit release. A security and reliability pass over the whole
12
+ framework (guards and RLS fail closed, forms only accept their own fields,
13
+ dropped database connections recover, Redis hiccups no longer switch
14
+ features off), and the pieces apps missed most: HTTP routes and
15
+ middleware, page caching, translations, error boundaries, better testing
16
+ and debugging, CSV export, drag-and-drop uploads, a form stepper,
17
+ responsive images, and rename in the editor. Read **Changed** before
18
+ upgrading: `pyweb serve` now needs `PYWEB_AUTH_SECRET`, and Postgres
19
+ tenant tables get a new policy.
20
+
21
+ ### Security
22
+ - **Guards fail closed without a secret.** A server function guarded by
23
+ `@auth.required` or permissions used to run unchecked when the app had no
24
+ `PYWEB_AUTH_SECRET`; it is now refused. Rate limits run before any other
25
+ check.
26
+ - **Forms only accept the fields they rendered.** Each `<Form>` carries a
27
+ signed list of its field names; extra fields in a submit (`is_admin=1`)
28
+ are ignored, and a submit without the list is refused ("reload the page").
29
+ - **Row policies cover bulk writes and the stored row.** `update()` and
30
+ `delete()` on a policied Model check every affected row, and `save()`
31
+ checks the row as stored as well as the new values.
32
+ - **Postgres row-level security fails closed.** A connection that never set
33
+ the organization sees no rows (it used to see every organization), and
34
+ the organization is set before every statement, so views and functions
35
+ are covered too.
36
+ - **Spread attributes** (`{**attrs}`) can't add `on*` handlers in any
37
+ case, `srcdoc`, or unchecked URLs (`formaction`, `data`, `poster`, ...).
38
+ - **Emailed links** never use a foreign `Host` header, in development too.
39
+ - **Accounts:** 2FA codes work once; setting a first password and signing
40
+ out other devices need a recent sign-in (10 minutes); OAuth
41
+ `email_verified` must really be true; form redirects go through
42
+ `safe_next`.
43
+ - **Webhooks:** GitHub deliveries are de-duplicated on the body (GitHub
44
+ doesn't sign its delivery id).
45
+ - **Push:** someone who learns a browser's push endpoint can't take it
46
+ over or unsubscribe it.
47
+ - **Outbound fetch:** NAT64 (`64:ff9b::/96`) and IPv4-compatible IPv6
48
+ addresses are judged by the IPv4 address inside them.
49
+ - **S3:** `upload_url(name, content_type=...)` signs the content type, so an
50
+ upload can't be stored as `text/html`.
51
+ - **Request headers** are looked up case-insensitively everywhere (CSRF
52
+ token, `Last-Event-ID`, `traceparent`).
53
+ - **WebSockets:** a ping flood closes the socket.
54
+ - **Translations:** the `Plural-Forms` rule of a `.po` file may only be a
55
+ plural rule (no powers, shifts or huge numbers), so a hostile catalog
56
+ can't hang the server.
57
+ - **Page cache:** copies are kept per host and per language, and cached
58
+ pages say `Vary: Cookie` (plus `Accept-Language` with translations) so a
59
+ CDN keeps them apart too.
60
+ - **`<Image>`:** Pillow opens only the format a file's name says, and each
61
+ file has one cached copy per width however its path is spelled.
62
+ - **Source maps** in `pyweb dev` hold the whole `.pyweb` file, server code
63
+ included: only the developer's own browser gets them (not share-link
64
+ guests or the LAN). `pyweb build` maps never contain source.
65
+ - **CSV:** DataTable downloads also neutralize cells starting with a tab or
66
+ carriage return.
67
+ - The VS Code extension uses `vscode-languageclient` 11; its build tools no
68
+ longer pull in vulnerable `@azure/msal-node` and `qs` versions.
69
+ - **Dev tools:** `pyweb dev --share` treats a tunnel that rewrites `Host` to
70
+ localhost as a guest; detailed error pages go only to the developer;
71
+ `pyweb mcp --http` always needs a token (kept in `.pyweb/mcp-token`);
72
+ the compile cache is read only from a folder and files only you can
73
+ write.
74
+
75
+ ### Fixed (reliability)
76
+ - **Dropped database connections recover.** A connection the server closed
77
+ (restart, failover, idle timeout) is replaced instead of failing every
78
+ request that later draws it from the pool; a read on one is retried once
79
+ on a fresh connection; a transaction pings a connection that sat idle.
80
+ - **Redis live updates and rate limits** no longer turn themselves off for
81
+ good after one error: they work without Redis for 5 seconds and try
82
+ again. Two servers publishing to one channel at once can't make Redis
83
+ refuse a message anymore (number, store and announce are one atomic
84
+ script), and the shared rate limit can't be overshot by concurrent calls.
85
+ Redis clients get 2-second timeouts.
86
+ - **Live queries** no longer miss a change when a page render picked up the
87
+ new rows before the watcher re-ran.
88
+ - **`RedisCache`:** `clear()` deletes only the cache's own keys (it used to
89
+ empty the whole Redis database); `@cache(backend=RedisCache(...))` and
90
+ `swr()` work with it; tag sets expire with their keys.
91
+ - **Jobs:** PyWeb's own jobs (webhooks, push) run in a worker that never
92
+ queued one; a job name a worker doesn't know is retried later (a deploy in
93
+ progress) instead of dying at once; a timed-out job keeps its slot until
94
+ its thread really returns.
95
+ - **Overload and shutdown:** requests waiting for a thread beyond
96
+ `PYWEB_MAX_QUEUE` (256) get `503` with `Retry-After`; work still queued
97
+ when its time limit passes isn't started; `/healthz` is answered even with
98
+ every thread busy, and `/readyz` says 503 while draining; jobs and
99
+ requests wind down together within `PYWEB_SHUTDOWN_TIMEOUT` (25 s).
100
+ - Streamed server functions run every step on one thread; a WebSocket
101
+ handshake that fails gives its slot back; the Redis job store's cleanup
102
+ reaches every job; failures in the live-data watcher are logged.
103
+
104
+ ### Added
105
+ - **HTTP routes and middleware.** `@app.route("/api/items/{id:int}",
106
+ methods=[...])` (and `@app.get` / `@app.post`) answer anything that isn't a
107
+ page: JSON, text, CSV downloads (`pyweb.routes.csv`), redirects, with typed
108
+ path parameters, `login=`/`roles=`/`rate=` guards, API tokens
109
+ (`tokens=True`), CORS (`cors=[...]`) and a same-site check for writes.
110
+ `@app.middleware` runs around every request. Routes appear in
111
+ `pyweb openapi` and the MCP `pyweb_routes` tool. See
112
+ [Routes & middleware](docs/36-routes.md).
113
+ - **Page caching.** Every page gets an `ETag` (and `304 Not Modified`);
114
+ pages that are the same for everyone may be kept by the browser
115
+ (`no-cache` instead of `no-store`). `@app.page(cache=300, vary=[...])` keeps
116
+ the rendered HTML for visitors who aren't signed in, dropped when a table
117
+ the page read changes, shared through Redis when configured. See
118
+ [Performance](docs/29-performance.md#caching-pages).
119
+ - **Translations.** `_("...")` and `ngettext("{n} item", "{n} items", n)` in
120
+ Python and in markup, gettext `.po` catalogs in `locales/` (no extra
121
+ package), the language from a `lang` cookie or `Accept-Language`
122
+ (`App(locales=[...])`, `set_locale()`), `<html lang>` and `dir="rtl"`, the
123
+ page's strings sent to the browser (an optional `i18n.js`),
124
+ `number`/`currency`/`date` formatting, and `pyweb i18n extract | init |
125
+ check`. See [Translations](docs/37-i18n.md).
126
+ - **`<ErrorBoundary fallback="...">`.** A part of the page that fails, on the
127
+ server or in the browser, shows its fallback instead of breaking the page.
128
+ Its browser code is a small module loaded only by pages that use it.
129
+ - **Testing:** `response.find("li.done")` / `find_all(...)`,
130
+ `client.render("Component", **props)`, `client.a11y("/")` (accessibility
131
+ rules on the rendered page) and `assert_snapshot(value, name)`.
132
+ - **Debugging:** `pyweb dev --debug` (attach VS Code or PyCharm through
133
+ debugpy); source maps from each page module to its `.pyweb` lines in
134
+ `pyweb dev`, and `pyweb build --sourcemaps`.
135
+ - `/robots.txt` and `/sitemap.xml` (with `App(base_url=...)`), from the app's
136
+ public pages plus `@app.sitemap` generators; `App(robots=...)`.
137
+ - Plugins work: `App(plugins=[...])` registers a plugin's routes, pages,
138
+ server functions and Models.
139
+ - **CSV export:** `DataTable export="csv"` downloads the rows that match
140
+ the search; the admin has "Export CSV" per Model (fresh sign-in, logged).
141
+ - **Uploads:** drop files on a `<FileInput>`, see what's chosen (image
142
+ previews, remove one), an upload progress bar, and several files in one
143
+ field (`photos: list[UploadedFile]`).
144
+ - **`Stepper` / `Step`** (UI kit): a form in steps that checks each step's
145
+ fields before the next.
146
+ - **`Image`** (UI kit): `srcset` of resized copies made by the server
147
+ (`/__pyweb/img/<width>/...`, with Pillow: `pyweb-stack[images]`), lazy
148
+ loading, no layout shift.
149
+ - **Editor:** find references, rename (across the folder's `.pyweb` files
150
+ for components and server functions) and "did you mean" quick fixes.
151
+ - `TestClient.put/patch/delete`; `pyweb.request.body` and `request.json()`.
152
+ - `PYWEB_DB_POOL` (default 10) and `App(pool_size=...)`: database
153
+ connections per process.
154
+
155
+ ### Fixed
156
+ - `from pyweb.browser import document` (and `Blob`, `URL`, ...) works in a
157
+ file that also runs on the server; browser code can construct `Date`,
158
+ `URL`, `URLSearchParams`, `Blob`, `FormData`, `Event`, `CustomEvent` and
159
+ `Promise` (`new` is added for you).
160
+ - A server function with a `list[...]` parameter and no default gets `[]`
161
+ when the form sends none, instead of failing.
162
+
163
+ ### Changed (internals)
164
+ - An option in `@app.page(...)`/`@app.layout(...)` that isn't written out as
165
+ a value (`login=NEEDS_LOGIN`) is a compile error; it used to be ignored,
166
+ which could leave a page public.
167
+ - One definition of "came from another site" (`pyweb.security.cross_site`)
168
+ for server functions, forms, the auth kit and push; one cookie parser
169
+ (`pyweb.net.http.parse_cookies`).
170
+ - The dev server moved to `pyweb.devserver`, the production checks to
171
+ `pyweb.checks`, project scaffolding to `pyweb.scaffold` and the page CSP to
172
+ `pyweb.csp` (the old import paths still work). The server runtime no longer
173
+ imports the AI helper: `pyweb dev` hooks into page errors instead.
174
+ - `from pyweb import *` no longer brings in tooling modules (`sync`, `lsp`,
175
+ `deploy`, `build`, ...); `pyweb.lsp` and the others still import.
176
+ - Every `PYWEB_*` setting is documented (a test keeps it that way).
177
+
178
+ ### Changed
179
+ - `pyweb serve` and `pyweb.asgi.create_app()` refuse to start without
180
+ `PYWEB_AUTH_SECRET` (set `PYWEB_ENV=development` to run without one).
181
+ The development key is random per machine (kept in
182
+ `~/.cache/pyweb/dev-secret`) instead of derived from the project path.
183
+ - Tenant tables on Postgres get a new row-level security policy
184
+ (`pyweb_tenant_v2`). Apps that create tables get it on start; with
185
+ migrations, run `pyweb db diff` and `pyweb db upgrade`. `pyweb upgrade`
186
+ explains both changes.
187
+ - Forms rendered by 0.6.2 and submitted after the upgrade are refused once
188
+ with "reload the page".
189
+
190
+ ## [0.6.2]
191
+
192
+ The dev loop. Saves update the open page in place with every page and
193
+ component keeping its state, any element opens its own line in your
194
+ editor, error pages fix themselves with AI (your assistant's model through
195
+ MCP, or your own key), a timeline steps back through every change, and
196
+ `pyweb dev --share` gives someone a link to try what you're building. All
197
+ of it lives in `pyweb dev`: production output is unchanged and there is
198
+ nothing to change in your apps. See [The dev loop](docs/35-dev-loop.md).
199
+
200
+ ### Added
201
+ - **Hot reload without a page load.** A save swaps the page in place
202
+ through client navigation: no reload, no flash. Page variables,
203
+ component variables (each instance keeps its own), form fields, scroll,
204
+ focus and the selection stay. A stylesheet-only save swaps just that
205
+ stylesheet. When the page changed too much to swap, it reloads with the
206
+ same state, also after a compile error is fixed.
207
+ - **Click to source.** Hold Alt to see where any element comes from;
208
+ Alt-click opens that line in your editor (`PYWEB_EDITOR`, else VS Code,
209
+ Cursor, Windsurf, Zed or Sublime Text, else a `vscode://` link) and
210
+ shows the **inspector**: the components around it with their live
211
+ variables, which you can edit.
212
+ - **Fix with AI and Edit with AI.** Compile error pages and dev 500 pages
213
+ have Fix with AI; Alt+Shift-click any element to ask for a change. The
214
+ answer is a compiled diff of one app file: Apply writes it (refused if
215
+ the file changed since) and Undo puts it back (`.pyweb/history/`). It is
216
+ answered by the assistant connected through `pyweb mcp` when its app
217
+ supports MCP sampling, else `ANTHROPIC_API_KEY` or
218
+ `OPENAI_API_KEY`/`OPENAI_BASE_URL`, else the page offers the prompt to
219
+ copy. Credentials in what's sent are replaced by placeholders and put
220
+ back in the answer.
221
+ - **Time-travel timeline.** The toolbar's Timeline tab records every change
222
+ to the page's variables with its cause (a click, typing, a server call);
223
+ a slider steps back through them with the page paused, and Resume
224
+ returns to now.
225
+ - **`pyweb dev --share`**: a temporary public link (cloudflared, else
226
+ localhost.run) with a QR code. The link's token sets a cookie that every
227
+ request through the tunnel needs; guests get the app and hot reload, not
228
+ your tools.
229
+ - **A better first five minutes.** `pyweb new` with no name asks for the
230
+ name, a starter and a colour preset (`--theme` without asking); `pyweb
231
+ dev` opens the browser, prints the network URL with a QR code for a phone
232
+ with `--host 0.0.0.0`, and shows a one-time Welcome panel with what to
233
+ try and how to connect your assistant.
234
+ - **A faster first start.** Parse results and compiled code are cached on
235
+ disk (`~/.cache/pyweb`; `PYWEB_CACHE=0` turns it off): loading the saas
236
+ example in a fresh process went from about 340 to 210 ms.
237
+ `pyweb.bench --system` reports `cold_dev_ms`.
238
+ - MCP: the `pyweb_assist_requests` tool and the `fix_errors` prompt;
239
+ `pyweb mcp` answers the page's AI requests through sampling.
240
+
241
+ ### Changed
242
+ - `pyweb dev`'s toolbar and its `/__pyweb/dev/` endpoints answer only the
243
+ person running it: a request from this machine to a loopback address.
244
+ Visitors from the network (`--host 0.0.0.0`) or a share link get hot
245
+ reload only.
246
+ - The runtime is 25 bytes bigger (gzip) for the dev hooks; nothing runs
247
+ in production.
248
+
9
249
  ## [0.6.1]
10
250
 
11
251
  Better error messages, `pyweb doctor`, a `pyweb dev` that keeps your place
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweb-stack
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Full-stack web apps in one Python file: server-rendered pages, reactive browser UI, typed RPC.
5
5
  Author-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>
6
6
  Maintainer-email: MaanavKrishna <67054795+MaanavKrishna@users.noreply.github.com>
@@ -53,6 +53,8 @@ Provides-Extra: push
53
53
  Requires-Dist: cryptography>=42; extra == "push"
54
54
  Provides-Extra: desktop
55
55
  Requires-Dist: pywebview>=6; extra == "desktop"
56
+ Provides-Extra: images
57
+ Requires-Dist: Pillow>=10; extra == "images"
56
58
  Provides-Extra: all
57
59
  Requires-Dist: psycopg[binary]>=3.1; extra == "all"
58
60
  Requires-Dist: PyMySQL>=1.1; extra == "all"
@@ -63,6 +65,7 @@ Requires-Dist: argon2-cffi>=23.1; extra == "all"
63
65
  Requires-Dist: opentelemetry-sdk>=1.24; extra == "all"
64
66
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24; extra == "all"
65
67
  Requires-Dist: orjson>=3.10; extra == "all"
68
+ Requires-Dist: Pillow>=10; extra == "all"
66
69
  Provides-Extra: test
67
70
  Requires-Dist: pytest>=8; extra == "test"
68
71
  Requires-Dist: playwright>=1.45; extra == "test"
@@ -70,6 +73,7 @@ Requires-Dist: mcp>=1.2; extra == "test"
70
73
  Requires-Dist: hypothesis>=6.100; extra == "test"
71
74
  Requires-Dist: pyyaml>=6; extra == "test"
72
75
  Requires-Dist: opentelemetry-sdk>=1.24; extra == "test"
76
+ Requires-Dist: Pillow>=10; extra == "test"
73
77
  Dynamic: license-file
74
78
 
75
79
  <div align="center">
@@ -400,6 +404,17 @@ develop a toolbar on every page shows each request's queries and flags
400
404
  N+1 loops. [Scaling & deploying →](docs/25-scaling.md) ·
401
405
  [Logs, metrics & tracing →](docs/26-observability.md)
402
406
 
407
+ ### A dev loop that keeps up
408
+
409
+ Save, and the open page changes in place: no reload, and every page and
410
+ component keeps its state. Hold Alt and click any element to open its
411
+ line in your editor; Alt+Shift-click to change it with AI, and error
412
+ pages have **Fix with AI** (your assistant's model through MCP, or your
413
+ own API key), with a diff to apply and Undo. The toolbar's timeline
414
+ steps back through every change and what caused it, and
415
+ `pyweb dev --share` gives someone a link to try what you're building.
416
+ [The dev loop →](docs/35-dev-loop.md)
417
+
403
418
  ### Small, fast and boring to run
404
419
 
405
420
  - **Every page is server-rendered**: complete HTML on first paint, then
@@ -442,7 +457,7 @@ it runs the real PyWeb, server functions included, in your browser.
442
457
  pip install pyweb-stack
443
458
  pyweb new myapp --template saas # saas | desktop | blank | counter | todo | blog | auth | chat | ai-chat
444
459
  cd myapp
445
- pyweb dev app.pyweb # edit app.pyweb; the page reloads on save
460
+ pyweb dev app.pyweb # saves update the open page in place, state kept
446
461
  ```
447
462
 
448
463
  Then:
@@ -478,7 +493,7 @@ The [architecture overview](ARCHITECTURE.md) goes through each stage.
478
493
 
479
494
  ## Build it with AI
480
495
 
481
- PyWeb ships an MCP server with 30 tools, so AI assistants can do what
496
+ PyWeb ships an MCP server with 31 tools, so AI assistants can do what
482
497
  you'd do: scaffold an app, search the docs, check each edit (errors come
483
498
  back as line numbers with fix hints), build with the UI kit and a theme,
484
499
  render pages and call server functions as any signed-in member, look at
@@ -566,7 +581,7 @@ Upgrade with `pip install -U pyweb-stack`.
566
581
 
567
582
  | Version | Highlights |
568
583
  |---|---|
569
- | 0.6 | Teams with roles, invitations and tenant-scoped Models (Postgres row-level security), API tokens and OpenAPI, webhooks, installable apps with Web Push, desktop windows and installers, a UI kit with themes and motion, Trusted Types, encrypted fields and a threat model, about 2.5× faster serving, Python 3.15 and free-threaded builds, a much richer MCP server; 0.6.1: error messages with code frames and suggestions, `pyweb doctor`, dev reloads that keep page state, 4× faster reloads and tests |
584
+ | 0.6 | Teams with roles, invitations and tenant-scoped Models (Postgres row-level security), API tokens and OpenAPI, webhooks, installable apps with Web Push, desktop windows and installers, a UI kit with themes and motion, Trusted Types, encrypted fields and a threat model, about 2.5× faster serving, Python 3.15 and free-threaded builds, a much richer MCP server; 0.6.1: error messages with code frames and suggestions, `pyweb doctor`, dev reloads that keep page state, 4× faster reloads and tests; 0.6.2: the dev loop (hot reload in place with component state, Alt-click to source, Fix/Edit with AI, a time-travel timeline, `pyweb dev --share`, a guided `pyweb new`); 0.6.3: security and reliability fixes, HTTP routes and middleware, page caching and ETags, translations, `<ErrorBoundary>`, test helpers, source maps, CSV export, drag-and-drop uploads, `Stepper`, `<Image>`, editor rename |
570
585
  | 0.5 | Models with migrations, forms, auth kit with passkeys, OAuth and an admin, row-level live updates over WebSockets on PyWeb's own server, durable jobs and schedules, `pyweb deploy` for docker, compose, k8s, fly, render and railway, logs, metrics, tracing and a dev toolbar, data-aware editor and MCP tools, the `saas` template, `pyweb upgrade` |
571
586
  | 0.4 | npm packages without Node, layouts and client-side navigation, streaming server functions and `<Markdown>` for AI apps, live queries, page head tags, `.pyweb` error pages; 0.4.1: richer MCP tools; 0.4.2: VS Code run, new-app and MCP commands; 0.4.3: fixes, security hardening, gzip and caching; 0.4.4: key rotation, session revocation, typed arguments, Redis-shared limits, `check --production` |
572
587
  | 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
@@ -326,6 +326,17 @@ develop a toolbar on every page shows each request's queries and flags
326
326
  N+1 loops. [Scaling & deploying →](docs/25-scaling.md) ·
327
327
  [Logs, metrics & tracing →](docs/26-observability.md)
328
328
 
329
+ ### A dev loop that keeps up
330
+
331
+ Save, and the open page changes in place: no reload, and every page and
332
+ component keeps its state. Hold Alt and click any element to open its
333
+ line in your editor; Alt+Shift-click to change it with AI, and error
334
+ pages have **Fix with AI** (your assistant's model through MCP, or your
335
+ own API key), with a diff to apply and Undo. The toolbar's timeline
336
+ steps back through every change and what caused it, and
337
+ `pyweb dev --share` gives someone a link to try what you're building.
338
+ [The dev loop →](docs/35-dev-loop.md)
339
+
329
340
  ### Small, fast and boring to run
330
341
 
331
342
  - **Every page is server-rendered**: complete HTML on first paint, then
@@ -368,7 +379,7 @@ it runs the real PyWeb, server functions included, in your browser.
368
379
  pip install pyweb-stack
369
380
  pyweb new myapp --template saas # saas | desktop | blank | counter | todo | blog | auth | chat | ai-chat
370
381
  cd myapp
371
- pyweb dev app.pyweb # edit app.pyweb; the page reloads on save
382
+ pyweb dev app.pyweb # saves update the open page in place, state kept
372
383
  ```
373
384
 
374
385
  Then:
@@ -404,7 +415,7 @@ The [architecture overview](ARCHITECTURE.md) goes through each stage.
404
415
 
405
416
  ## Build it with AI
406
417
 
407
- PyWeb ships an MCP server with 30 tools, so AI assistants can do what
418
+ PyWeb ships an MCP server with 31 tools, so AI assistants can do what
408
419
  you'd do: scaffold an app, search the docs, check each edit (errors come
409
420
  back as line numbers with fix hints), build with the UI kit and a theme,
410
421
  render pages and call server functions as any signed-in member, look at
@@ -492,7 +503,7 @@ Upgrade with `pip install -U pyweb-stack`.
492
503
 
493
504
  | Version | Highlights |
494
505
  |---|---|
495
- | 0.6 | Teams with roles, invitations and tenant-scoped Models (Postgres row-level security), API tokens and OpenAPI, webhooks, installable apps with Web Push, desktop windows and installers, a UI kit with themes and motion, Trusted Types, encrypted fields and a threat model, about 2.5× faster serving, Python 3.15 and free-threaded builds, a much richer MCP server; 0.6.1: error messages with code frames and suggestions, `pyweb doctor`, dev reloads that keep page state, 4× faster reloads and tests |
506
+ | 0.6 | Teams with roles, invitations and tenant-scoped Models (Postgres row-level security), API tokens and OpenAPI, webhooks, installable apps with Web Push, desktop windows and installers, a UI kit with themes and motion, Trusted Types, encrypted fields and a threat model, about 2.5× faster serving, Python 3.15 and free-threaded builds, a much richer MCP server; 0.6.1: error messages with code frames and suggestions, `pyweb doctor`, dev reloads that keep page state, 4× faster reloads and tests; 0.6.2: the dev loop (hot reload in place with component state, Alt-click to source, Fix/Edit with AI, a time-travel timeline, `pyweb dev --share`, a guided `pyweb new`); 0.6.3: security and reliability fixes, HTTP routes and middleware, page caching and ETags, translations, `<ErrorBoundary>`, test helpers, source maps, CSV export, drag-and-drop uploads, `Stepper`, `<Image>`, editor rename |
496
507
  | 0.5 | Models with migrations, forms, auth kit with passkeys, OAuth and an admin, row-level live updates over WebSockets on PyWeb's own server, durable jobs and schedules, `pyweb deploy` for docker, compose, k8s, fly, render and railway, logs, metrics, tracing and a dev toolbar, data-aware editor and MCP tools, the `saas` template, `pyweb upgrade` |
497
508
  | 0.4 | npm packages without Node, layouts and client-side navigation, streaming server functions and `<Markdown>` for AI apps, live queries, page head tags, `.pyweb` error pages; 0.4.1: richer MCP tools; 0.4.2: VS Code run, new-app and MCP commands; 0.4.3: fixes, security hardening, gzip and caching; 0.4.4: key rotation, session revocation, typed arguments, Redis-shared limits, `check --production` |
498
509
  | 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
@@ -42,17 +42,23 @@ load balancer in front.
42
42
  |---|---|---|---|
43
43
  | S | Another site calls a server function with the visitor's cookies (CSRF) | RPCs are `POST` with a JSON content type only (a cross-site form can't send one), `Origin` and `Sec-Fetch-Site` are checked, cookies are `SameSite=Lax`; optional token check (`PYWEB_CSRF_SECRET`) | `test_request_hardening.py::test_cross_site_requests_are_refused`, `test_rpc_production.py::test_csrf_gate_blocks_cookie_calls_without_token` |
44
44
  | S | Another site posts a `<Form>` | A per-visitor CSRF token in every form (double-submit cookie) plus the `Origin` check | `test_forms_v5.py::test_csrf_and_cross_site_posts_are_refused` |
45
- | T | Arguments of the wrong shape reach a server function (an object where a string was expected, extra fields) | Arguments are checked against the type hints; unknown names are refused; Models only take their own non-readonly fields | `test_forms_v5.py::test_mass_assignment_is_ignored`, `test_fuzz_requests.py::test_rpc_arguments` |
45
+ | T | Arguments of the wrong shape reach a server function (an object where a string was expected, extra fields) | Arguments are checked against the type hints; unknown names are refused; Models only take their own non-readonly fields; a `<Form>` only accepts the fields it rendered (a signed list travels with it) | `test_forms_v5.py::test_mass_assignment_is_ignored`, `test_forms_v5.py::test_fields_the_form_did_not_render_are_ignored`, `test_forms_v5.py::test_a_submit_without_the_signed_field_list_is_refused`, `test_fuzz_requests.py::test_rpc_arguments` |
46
+ | E | A guarded server function runs unguarded because the app has no secret | Guards fail closed: without `PYWEB_AUTH_SECRET` a guarded call is refused, and `pyweb serve` / `create_app` won't start without one (unless `PYWEB_ENV=development`); the development key is random per machine, not derived from the project | `test_security_063.py::test_guarded_functions_fail_closed_in_production_without_a_secret`, `test_security_063.py::test_serve_refuses_to_start_without_a_secret`, `test_security_063.py::test_the_dev_key_is_random_and_private` |
46
47
  | T | Request smuggling past a proxy | Ambiguous lengths (`Content-Length` with `Transfer-Encoding`, repeated lengths, folded headers) are refused | `test_net.py::test_refuses_ambiguous_or_malformed_requests` |
47
48
  | I | Server-only code, imports or secrets end up in the page's JavaScript | The compiler places each name in the browser or on the server, and referencing a server-only name from browser code is a compile error; credential-like page variables are refused | `test_compiler.py::test_server_only_names_cannot_reach_the_browser`, `test_compiler.py::test_secret_never_goes_to_browser` |
48
- | I | Stack traces or internal errors reach the visitor | Error responses carry a message and a request id, never a traceback | `test_auth_security.py::test_error_responses_never_leak_tracebacks` |
49
+ | I | Stack traces or internal errors reach the visitor | Error responses carry a message and a request id, never a traceback; in `pyweb dev` the detailed error pages go only to the developer's own browser | `test_auth_security.py::test_error_responses_never_leak_tracebacks`, `test_share.py::test_a_compile_error_page_for_guests_has_no_tools` |
49
50
  | I | Encrypted columns reach the browser through `to_dict()` | Encrypted and private fields are left out of `to_dict()` | `test_encrypted_fields.py::test_encrypted_values_stay_out_of_to_dict`, `test_data_layer.py::test_private_fields_stay_out_of_dicts` |
50
51
  | D | Huge, deeply nested or very wide bodies | 1 MiB body limit (413), 32 levels / 10,000 values for JSON, 1,000 fields / 100 files for forms, 16 KB of headers (431) | `test_request_hardening.py::test_deeply_nested_json_is_refused_not_a_crash`, `test_request_hardening.py::test_huge_headers_get_431`, `test_fuzz_requests.py::test_too_many_fields_are_refused` |
51
52
  | D | Slow clients hold connections (slowloris) | Request heads must arrive within 10 s, idle sockets close after 30 s, 10,000 connections per process | `test_request_hardening.py::test_slow_clients_are_disconnected` |
52
53
  | D | One client floods server functions | 120 calls a minute per address by default, per-function `@server(rate=...)`; `X-Forwarded-For` is ignored unless `PYWEB_TRUST_PROXY` says a proxy sets it | `test_rpc_production.py::test_rate_limit_is_per_client_address_and_ignores_spoofed_headers`, `test_rate_per_function.py` |
53
54
  | D | Malformed input crashes a handler | The RPC and form parsers are fuzzed: every answer is 2xx or 4xx | `test_fuzz_requests.py` (Hypothesis) |
55
+ | S | Another site calls an HTTP route (`@app.route`) with the visitor's cookies | Unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`) authenticated by cookie must come from the site itself (`Origin` / `Sec-Fetch-Site`); a Bearer API token is exempt; CORS is off unless a route lists origins | `test_routes.py::test_methods_guards_and_csrf`, `test_routes.py::test_cors_and_preflight` |
56
+ | I | A cached page (`@app.page(cache=...)`) shows one visitor's data to another | Requests with a session cookie or `Authorization` header never get or make a shared copy; a render that read the session, set a cookie, showed a toast or opened a live feed isn't stored; copies are kept per host and language, with `Vary: Cookie` for CDNs | `test_pagecache.py::test_vary_and_personal_pages`, `test_pagecache.py::test_translated_pages_are_cached_per_language_and_host` |
57
+ | D | A translation file's `Plural-Forms` runs code or hangs the server | Only plural-rule syntax is accepted: `n`, small numbers, arithmetic without powers or shifts, comparisons and `?:` | `test_i18n.py::test_a_hostile_plural_rule_is_refused` |
58
+ | D | Resized images (`/__pyweb/img/<w>/...`) used to burn CPU or memory | Only six fixed widths, only files under `static/`, never larger than the original, images over 50 megapixels sent as they are, copies kept in a 64 MB memory cache | `test_images.py::test_only_known_widths_and_static_files` |
59
+ | I | A photo's metadata (camera, location) leaks through `<Image>` | Resized copies are written without EXIF; the original under `static/` is still served as uploaded | `test_images.py::test_resized_without_metadata` |
54
60
  | E | A visitor who isn't signed in reaches a protected page or function | `login=`, `roles=` and `fresh=` guards on pages, layouts and server functions | `test_authkit.py::test_instant_signup_and_guards`, `test_auth_security.py::test_required_page_guard_blocks_anonymous` |
55
- | E | A signed-in user reads or changes another user's rows | Row policies apply to every query and save made for a request | `test_authkit.py::test_row_policies_follow_the_signed_in_user` |
61
+ | E | A signed-in user reads or changes another user's rows | Row policies apply to every query, save, bulk `update()` and `delete()` made for a request; a save checks the row as stored as well as the new values (so a row can't be moved out of someone else's hands). Raw SQL (`db.execute`) is not covered | `test_authkit.py::test_row_policies_follow_the_signed_in_user`, `test_authkit.py::test_write_rules_cover_bulk_changes_and_the_row_as_stored` |
56
62
 
57
63
  ## 2. Server to browser: what the page runs
58
64
 
@@ -60,6 +66,7 @@ load balancer in front.
60
66
  |---|---|---|---|
61
67
  | T | Data shown on a page injects markup or script (XSS) | Text and attributes are escaped on the server and set as text in the browser; Markdown escapes everything it doesn't build | `test_auth_security.py::test_escape_attr_neutralizes_quote_breakout`, `e2e/test_trusted_types_browser.py::test_pages_enforce_trusted_types_and_the_runtime_works_under_them` |
62
68
  | T | `javascript:` and `data:text/html` URLs in links | Replaced by `#` on the server and in the browser | `e2e/test_runtime_dom.py::test_javascript_urls_are_blocked` |
69
+ | T | Spread attributes (`{**attrs}`) built from visitor data add an event handler or a script URL | Spread names must be valid attribute names; `on*` (any case) and `srcdoc` are dropped; URL attributes (`href`, `src`, `action`, `formaction`, `data`, `poster`, …) are checked whatever their case | `test_security_063.py::test_spread_attributes_cannot_inject_handlers_or_urls` |
63
70
  | T | Injected markup runs script anyway | Content-Security-Policy: scripts only from the site, no inline script; inline styles only by hash | `test_static_serving.py` (the header is sent), `test_trusted_types.py::test_default_policy_enforces_trusted_types_and_reports`, every browser test (a CSP violation is a console error, which fails the test) |
64
71
  | T | A bug passes visitor text to an HTML sink (`innerHTML`) | Trusted Types: the browser refuses strings in HTML and script sinks; the runtime's two sinks use private policies | `e2e/test_trusted_types_browser.py::test_strings_in_html_sinks_are_refused_and_reported`, `test_trusted_types.py::test_the_runtime_only_reaches_html_sinks_through_its_policies` |
65
72
  | S | The site is framed to trick clicks (clickjacking) | `frame-ancestors 'self'` and `X-Frame-Options` | `test_production_gaps.py::test_body_cap_and_head_and_security_headers`, ZAP rule 10020 in CI |
@@ -75,13 +82,16 @@ load balancer in front.
75
82
  | S | A stolen session keeps working | Sessions end 30 days after sign-in; a password change or reset signs out other devices; any one device can be signed out from `/account` | `test_authkit.py::test_password_reset_signs_out_other_devices`, `test_auth_sessions.py::test_account_lists_sessions_and_signs_one_out` |
76
83
  | S | Revocation silently stops working when the database is unwell | The check fails closed: if it can't run, the session is refused | `integration/test_authkit_backends.py::test_session_check_fails_closed_when_the_database_errors` |
77
84
  | S | Password guessing | Slowed down per account and per address (not a lockout, which would let anyone lock others out); breached passwords refused | `test_authkit.py::test_guessing_is_slowed_down_not_locked` |
78
- | S | Account enumeration through sign-in or sign-up | The same message and timing for unknown emails and wrong passwords | `test_authkit.py::test_signup_with_a_taken_email_looks_the_same` |
85
+ | S | Account enumeration through sign-in or sign-up | The same message and timing for unknown emails and wrong passwords at sign-in; with email verification on, sign-up with a taken email looks the same as a new one. With `instant` sign-up (no verification) a taken email is reported, so that mode does reveal which emails have accounts | `test_authkit.py::test_signup_with_a_taken_email_looks_the_same` |
79
86
  | S | A replayed or cross-site passkey sign-in | WebAuthn challenges are single-use and bound to the site's origin and RP id | `test_authkit.py::test_a_captured_passkey_sign_in_cant_be_replayed`, `test_authkit.py::test_passkey_from_another_site_is_refused` |
80
87
  | S | Taking over an account through an OAuth provider with the same email | A provider sign-in links to an existing account only after signing in to it | `test_authkit.py::test_oauth_sign_up_link_and_no_silent_takeover` |
81
- | S | Emailed links pointing at an attacker's host | Links use `PYWEB_ORIGIN`, never the `Host` header, in production; each link works once | `test_authkit.py::test_emailed_links_never_trust_the_host_header_in_production`, `test_authkit.py::test_magic_links_need_a_click_and_work_once` |
88
+ | S | Emailed links pointing at an attacker's host | Links use `PYWEB_ORIGIN`; production refuses to send them without it, and elsewhere only a localhost `Host` is used; each link works once | `test_authkit.py::test_emailed_links_never_trust_the_host_header_in_production`, `test_authkit.py::test_emailed_links_never_use_a_foreign_host_header_outside_production`, `test_authkit.py::test_magic_links_need_a_click_and_work_once` |
82
89
  | S/E | A leaked API token is used for everything, or for pages | Tokens are stored as hashes, scoped to named server functions, only work on server function calls, never count as a recent sign-in, can expire and be revoked | `test_api_tokens.py::test_scopes_limit_what_a_token_can_call`, `test_api_tokens.py::test_tokens_dont_open_pages_and_can_be_revoked`, `test_api_tokens.py::test_tokens_act_as_their_user_and_organization` |
83
90
  | I | A user's data can't be taken out or erased on request | `/account` exports everything that points at the user and deletes the account (organizations owned alone go with it; the audit log is pseudonymised) | `test_account_privacy.py::test_export_holds_the_account_and_rows_that_point_at_it`, `test_account_privacy.py::test_deleting_an_account` |
84
- | E | A stolen session changes the password or 2FA | Sensitive account changes need a sign-in within the last 10 minutes | `test_authkit.py::test_account_changes_need_a_recent_sign_in` |
91
+ | E | A stolen session changes the password or 2FA | Sensitive account changes (password, 2FA, passkeys, email, signing out other devices) need a sign-in within the last 10 minutes | `test_authkit.py::test_account_changes_need_a_recent_sign_in`, `test_authkit.py::test_setting_a_password_without_one_or_signing_out_everywhere_needs_a_recent_sign_in` |
92
+ | S | A shoulder-surfed or phished 2FA code used again | Each authenticator code works once: the last accepted time step is stored and older ones are refused | `test_authkit.py::test_two_step_sign_in` |
93
+ | S | An OAuth provider's unverified email links an account | `email_verified` must be the boolean `true` (or the string `"true"`) | `test_authkit.py::test_oauth_email_verified_must_really_be_true` |
94
+ | I | The admin's CSV export used to take a copy of a whole table | Admin only, a sign-in within the last 10 minutes, private and encrypted fields left out, 100,000 rows at most, each export written to the audit log | `test_authkit.py::test_admin_lists_edits_and_respects_private_fields` |
85
95
  | R | Disputed sign-ins and changes | The audit log (`auth_events`) records sign-ins, failures, resets, 2FA and role changes, with address and device | `test_authkit.py` |
86
96
  | T | Header injection through email fields | Mail refuses newlines in headers | `test_authkit_units.py::test_mail_refuses_header_injection` |
87
97
 
@@ -100,7 +110,7 @@ load balancer in front.
100
110
  |---|---|---|---|
101
111
  | T | SQL injection | Every value is a bound parameter; identifiers come from the Models; `pyweb check` flags SQL built from strings | `test_models_db.py::test_param_queries_not_interpolated`, `test_forms_security_obs.py::test_security_flags_sql_and_cmd` |
102
112
  | I | A leaked backup or read-only SQL injection exposes personal data | `Field(encrypted=True)`: AES-256-GCM per column under `PYWEB_DATA_KEY`, kept apart from the session secret | `test_encrypted_fields.py::test_values_are_encrypted_at_rest_and_read_back` |
103
- | T | Ciphertext copied into another column or row to make it show elsewhere | The table and column are the associated data; decryption fails | `test_encrypted_fields.py::test_a_ciphertext_moved_to_another_column_or_row_tamper_fails` |
113
+ | T | Ciphertext copied into another column to make it show elsewhere | The table and column are the associated data; decryption fails. Not covered: someone who can write to the database can copy a value between rows of the same column (the row isn't part of the associated data) | `test_encrypted_fields.py::test_a_ciphertext_moved_to_another_column_or_row_tamper_fails` |
104
114
  | I | Key loss on rotation | Old keys stay readable from `PYWEB_DATA_KEY_PREVIOUS`; `pyweb db reencrypt` rewrites rows | `test_encrypted_fields.py::test_rotation_reads_old_rows_and_reencrypt_rewrites_them` |
105
115
  | E | Background jobs or admin bypass row policies | Jobs and `system()` blocks run without a user on purpose (they're server code); the admin requires the `admin` role and a recent sign-in, and never shows private fields | `test_authkit.py::test_admin_lists_edits_and_respects_private_fields` |
106
116
 
@@ -109,7 +119,7 @@ load balancer in front.
109
119
  | | Threat | Mitigation | Proof |
110
120
  |---|---|---|---|
111
121
  | I/E | A member of one organization reads or changes another's rows | Tenant Models are limited to the current organization on every query, save, bulk update and delete; a row of another organization answers 404 even if loaded some other way | `test_tenancy.py::test_rows_stay_inside_their_organization`, `test_tenancy.py::test_no_sequence_of_operations_crosses_organizations` (Hypothesis) |
112
- | I/E | Raw SQL (or SQL injection) skips the ORM's organization filter | On Postgres, forced row-level security on every tenant table, checked by the database with a per-statement setting | `integration/test_rls_postgres.py::test_raw_sql_is_limited_by_the_database` |
122
+ | I/E | Raw SQL skips the ORM's organization filter | On Postgres, forced row-level security on every tenant table. The policy fails closed: a connection that never set the organization sees no rows, and PyWeb sets it again before every statement (so views and functions are covered too). It limits mistakes, not an attacker who can run arbitrary SQL as the app (who can change the setting within one statement) | `integration/test_rls_postgres.py::test_raw_sql_is_limited_by_the_database`, `integration/test_rls_postgres.py::test_the_policy_fails_closed` |
113
123
  | E | A background job reads every organization's data because no one is signed in | Jobs run for the organization that queued them | `test_tenancy.py::test_jobs_run_for_the_organization_that_queued_them` |
114
124
  | E | A viewer writes, an admin removes an owner, a member invites | Role checks on writes and on every management action; an organization keeps at least one owner | `test_org_pages.py::test_admins_cant_touch_owners_and_members_cant_manage`, `test_tenancy.py::test_switching_and_invitations` |
115
125
  | S | Someone else accepts a forwarded invitation, or reuses one | Invitations are for the invited email only, work once, expire after 7 days, and are stored as hashes | `test_org_pages.py::test_an_invitation_is_for_its_email_only`, `test_org_pages.py::test_the_whole_team_flow` |
@@ -129,7 +139,7 @@ load balancer in front.
129
139
  | | Threat | Mitigation | Proof |
130
140
  |---|---|---|---|
131
141
  | S | Forged webhook deliveries | Signatures checked in constant time for each scheme (GitHub, Stripe, Standard Webhooks); 401 otherwise | `test_webhooks.py::test_github_and_stripe_schemes`, `test_webhooks.py::test_standard_webhooks_reference_vector` |
132
- | T | A captured delivery replayed later, or a provider's retry running twice | Signed timestamps must be within 5 minutes; each event id runs once | `test_webhooks.py::test_receiving_runs_the_handler_once_as_a_job` |
142
+ | T | A captured delivery replayed later, or a provider's retry running twice | Stripe and Standard Webhooks: signed timestamps must be within 5 minutes and each event id runs once. GitHub signs no timestamp or id, so its deliveries are de-duplicated on the body itself: the same body runs once | `test_webhooks.py::test_receiving_runs_the_handler_once_as_a_job` |
133
143
  | S/I | A customer's webhook endpoint pointing at the internal network | Outbound deliveries go through `pyweb.http.fetch` | `test_webhooks.py::test_endpoints_cant_point_at_the_internal_network` |
134
144
  | I | Old uploads (replaced avatars, deleted attachments) kept forever | Files no row refers to are deleted after a day | `test_filegc.py::test_unreferenced_files_go_after_the_grace_period` |
135
145
 
@@ -140,6 +150,7 @@ load balancer in front.
140
150
  | T/E | Injected script registering its own service worker (which would sit between the app and every later visit) | The worker URL must come from the `pyweb-sw` Trusted Types policy, which accepts `/sw.js` only; the worker is served by PyWeb from its own code | `test_pwa.py::test_registration_uses_a_trusted_types_policy`, `e2e/test_pwa_browser.py::test_installable_and_offline` |
141
151
  | I | A signed-in visitor's page kept offline and shown to the next person on a shared computer | Only pages rendered for visitors who aren't signed in are marked as keepable; server functions, forms, live updates and credentialed requests are never cached | `test_pwa.py::test_pages_are_only_marked_for_offline_when_signed_out`, `test_pwa.py::test_service_worker` |
142
152
  | S | A notification that opens another site | Notification links must be paths of the app, checked when sending and again in the worker | `test_push.py::test_message_rules` |
153
+ | T | Someone who learns a browser's push endpoint takes it over or removes it | Re-registering an endpoint owned by another user needs that browser's `auth` key; unsubscribing needs the key or the owning user | `test_push.py::test_someone_who_knows_an_endpoint_cant_take_it_over`, `test_push.py::test_subscription_requests_are_checked` |
143
154
  | I/S | A visitor subscribing with an endpoint that makes the server send requests elsewhere | Endpoints must be https URLs of the browsers' push services (others only when named in `PYWEB_PUSH_HOSTS`); delivery through `pyweb.http.fetch` | `test_push.py::test_only_real_push_services`, `test_push.py::test_subscription_requests_are_checked` |
144
155
  | I | Notification content read by the push service | Payloads are encrypted for the receiving browser (RFC 8291) and requests signed with the app's VAPID key (RFC 8292) | `test_push.py::test_rfc8291_example`, `test_push.py::test_vapid_keys_and_jwt` |
145
156
  | T | Another site subscribing a visitor's browser | Subscription requests must be same-origin JSON | `test_push.py::test_subscription_requests_are_checked` |
@@ -172,11 +183,28 @@ websites in the user's browser.
172
183
  | | Threat | Mitigation | Proof |
173
184
  |---|---|---|---|
174
185
  | S/I | A website in the developer's browser calling `pyweb mcp --http` (DNS rebinding, cross-site requests) | Listens on 127.0.0.1; requests with a foreign `Origin` or `Host` are refused | `test_mcp_v6.py::test_http_sessions_streams_and_checks` |
175
- | S | Someone on the network calling the MCP server | Any address but loopback needs a bearer token | `test_mcp_v6.py::test_http_needs_a_token_beyond_this_machine` |
186
+ | S | Someone on the network, or another user or program on this machine, calling the MCP server | `pyweb mcp --http` always needs a bearer token: `--token`, `PYWEB_MCP_TOKEN`, or one kept in `.pyweb/mcp-token` (mode 0600) | `test_mcp_v6.py::test_http_needs_a_token_beyond_this_machine`, `test_mcp_v6.py::test_http_keeps_a_private_token_when_none_is_given` |
187
+ | T | Another local user planting compiled code in the disk cache | Cache entries are read only from a folder and files owned by you that no one else can write | `test_compile_cache.py::test_disk_cache_ignores_files_others_can_write` |
176
188
  | I | A web page or another local user reading the dev server's logs and errors | The agent endpoints need the token in `.pyweb/dev.json` (mode 0600, git-ignored) and answer only on loopback | `test_mcp_v6.py::test_dev_link_needs_its_token`, `test_mcp_v6.py::test_tools_see_a_running_pyweb_dev` |
177
189
  | T | An assistant's "read-only" SQL changing data | One statement, write keywords refused (even in a CTE), a read-only transaction that is rolled back; secrets redacted | `test_dx_v5.py::test_mcp_data_tools` |
178
190
  | T | Malformed tool arguments reaching app code | Arguments are checked against each tool's schema before it runs | `test_mcp_v6.py::test_arguments_are_checked_before_the_tool_runs` |
179
191
 
192
+ ## 7d. The dev loop: sharing, AI help, open in editor
193
+
194
+ `pyweb dev` only. Its tools change files, open programs and show request data, so they are for the
195
+ person running it, on that machine: a request must come from a loopback address and name a loopback
196
+ `Host` (a tunnel's or the LAN's requests don't), and writes must be same-origin.
197
+
198
+ | | Threat | Mitigation | Proof |
199
+ |---|---|---|---|
200
+ | S | Someone who learns the `--share` address (or guesses the tunnel name) using the app | Every request through the tunnel needs a cookie (`HttpOnly`, `Secure`, `SameSite=Lax`) set by the printed link's random token; forged cookies are refused. A tunnel that rewrites `Host` to localhost is still treated as a guest: only the exact `localhost:<port>` the developer opened counts as them | `test_share.py::test_the_link_sets_a_cookie_and_nothing_else_gets_in`, `test_share.py::test_pyweb_dev_share_end_to_end`, `test_share.py::test_a_tunnel_that_rewrites_host_is_still_a_guest` |
201
+ | E/I | A share-link guest (or someone on the LAN) using the developer's tools: the toolbar's SQL and requests, the agent endpoints, AI fixes, opening files, saving the theme | Guests get the app and hot reload only; every `/__pyweb/dev/` endpoint but change polling answers 403 to them, and their pages carry no toolbar or tool scripts | `test_share.py::test_guests_get_the_app_and_hot_reload_only`, `test_share.py::test_you_still_get_everything_and_the_lan_gets_hot_reload`, `test_share.py::test_a_compile_error_page_for_guests_has_no_tools` |
202
+ | T | A web page making "Fix with AI" or "Edit with AI" write to the app | Same-origin, loopback-only POSTs; the answer is compiled first, applied only on Apply, only to `.pyweb` files in the app, refused if the file changed since; the old version is kept for Undo | `test_assist.py::test_endpoints_refuse_other_sites_and_other_machines`, `test_assist.py::test_a_change_that_does_not_compile_is_refused`, `test_assist.py::test_apply_refuses_when_the_file_changed_since`, `test_assist.py::test_only_app_files_are_targets` |
203
+ | I | A share-link guest or someone on the LAN reads the app's server code through source maps | Dev source maps (they hold the whole `.pyweb` file) go only to the developer's own browser; `pyweb build` maps hold no source | `test_share.py::test_source_maps_hold_server_code_so_only_the_developer_gets_them` |
204
+ | I | Credentials in the app's source sent to a model | Known key formats and random-looking secret values are replaced by placeholders before sending and put back in the answer | `test_assist.py::test_secrets_are_replaced_before_sending_and_put_back`, `test_assist.py::test_the_connected_assistant_answers_through_mcp_sampling` |
205
+ | E | Alt-click "open in editor" used to open arbitrary files or run commands | Only `.pyweb` files inside the app folder or the UI kit; the editor gets an argument list, never a shell; same-origin and loopback only | `test_devopen.py::test_only_app_and_kit_files_resolve`, `e2e/test_dev_inspect_browser.py::test_open_refuses_cross_site_and_outside_files` |
206
+ | I | Dev-only markers or scripts in production pages | `data-pw-src` markers come from dev compiles only; the dev scripts are added by the dev server only | `test_devopen.py::test_other_builds_have_no_marks`, `test_trusted_types.py::test_dev_scripts_never_ship_in_built_pages` |
207
+
180
208
  ## 8. The release
181
209
 
182
210
  | | Threat | Mitigation | Proof |
@@ -43,6 +43,10 @@ Around that core, PyWeb has what a product needs, in the same file:
43
43
  ([Installable apps](31-pwa-push.md), [Desktop apps](32-desktop.md))
44
44
  - **Webhooks and files**: signed webhooks in and out, and unused uploads
45
45
  cleaned up. ([Webhooks & files](33-webhooks-files.md))
46
+ - **A dev loop that keeps up**: saves update the open page in place with
47
+ its state kept; Alt-click opens any element's source; Fix with AI and
48
+ Edit with AI; a time-travel timeline; `pyweb dev --share`.
49
+ ([The dev loop](35-dev-loop.md))
46
50
  - **Shipping**: `pyweb deploy` writes Docker, Compose, Kubernetes, Fly,
47
51
  Render or Railway files; logs, metrics and traces come built in.
48
52
  ([Scaling & deploying](25-scaling.md))
@@ -20,9 +20,14 @@ pip install "pyweb-stack[all]" # optional: Postgres, MySQL, Redis, argon2, O
20
20
  ```bash
21
21
  pyweb new hello
22
22
  cd hello
23
- pyweb dev app.pyweb # http://localhost:8000, reloads on save
23
+ pyweb dev app.pyweb # opens http://localhost:8000; saves update the page in place
24
24
  ```
25
25
 
26
+ (`pyweb new` with no name asks for one, a starter and colours.) Save a
27
+ change and the open page updates without losing its state; hold Alt and
28
+ click anything to open its line in your editor. [The dev loop](35-dev-loop.md)
29
+ has the rest: Fix with AI, time travel and sharing with `--share`.
30
+
26
31
  `pyweb new` writes `app.pyweb`, a starter test (`test_app.py`) and
27
32
  `AGENTS.md`/`CLAUDE.md` (instructions for AI coding agents). Add
28
33
  `--template saas` for a small product with teams, accounts, a database,
@@ -82,9 +87,14 @@ page Home("/") signals ['count'] rpc []
82
87
  ```bash
83
88
  pyweb check app.pyweb # compile, security and accessibility checks (CI-friendly)
84
89
  pyweb build app.pyweb --out dist --production # hashed, minified, deployable dist/
90
+ export PYWEB_AUTH_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
85
91
  pyweb serve dist # production server with /healthz
86
92
  ```
87
93
 
94
+ `pyweb serve` needs `PYWEB_AUTH_SECRET` (it signs sessions, CSRF tokens and
95
+ links) and won't start without it; `PYWEB_ENV=development pyweb serve dist`
96
+ tries a build locally without one.
97
+
88
98
  `dist/` contains your app source, static assets and a `Dockerfile`; see
89
99
  [Deployment](12-deployment.md).
90
100
 
@@ -107,6 +117,7 @@ ships it. Then, depending on what you're building:
107
117
  - work that runs later or on a schedule: [Background jobs](24-jobs.md);
108
118
  - teams that share data: [Teams & organizations](30-teams.md);
109
119
  - components, a theme and animation: [UI kit & motion](34-ui-and-motion.md);
120
+ - everything `pyweb dev` does for you: [The dev loop](35-dev-loop.md);
110
121
  - an app people install, or a desktop app: [Installable apps & push](31-pwa-push.md),
111
122
  [Desktop apps](32-desktop.md);
112
123
  - several pages with a shared header: [Layouts & navigation](19-layouts-navigation.md);
@@ -151,6 +151,46 @@ def Broken(request_id):
151
151
  traceback for 500s; `pyweb serve` shows your page.
152
152
  - If an error page itself fails, the built-in page is used.
153
153
 
154
+ ### One broken part, not a broken page
155
+
156
+ Wrap a part that may fail (a chart from a flaky service, a widget with complex logic) in
157
+ `<ErrorBoundary>`:
158
+
159
+ ```pyweb
160
+ from pyweb import App, ErrorBoundary
161
+
162
+ app = App()
163
+
164
+
165
+ def RevenueChart(data: list):
166
+ <ol class="chart">
167
+ for point in data:
168
+ <li style={{"--v": point["value"]}}>{point["label"]}</li>
169
+ </ol>
170
+
171
+
172
+ def load_stats():
173
+ return {"revenue": [{"label": "May", "value": 12}, {"label": "June", "value": 15}]}
174
+
175
+
176
+ @app.page("/dashboard")
177
+ def Dashboard():
178
+ stats = load_stats()
179
+ <h1>Dashboard</h1>
180
+ <ErrorBoundary fallback="The revenue chart couldn't load.">
181
+ <RevenueChart data={stats["revenue"]} />
182
+ </ErrorBoundary>
183
+ <p>Orders and the rest of the page keep working if the chart fails.</p>
184
+ ```
185
+
186
+ - On the server, if rendering what's inside raises, the page is sent with the fallback in its place
187
+ (and the error is reported like any page error).
188
+ - In the browser, if code inside throws (while the page starts, or in a click or input handler and
189
+ what it changes), that part is replaced by the fallback and a Reload button; the rest of the page
190
+ keeps working.
191
+ - `NotFound` and bad query parameters still answer for the whole page. The code that runs before the
192
+ markup (`stats = load_stats()` above) isn't inside the boundary: catch errors there yourself.
193
+
154
194
  ## Responses other than HTML
155
195
 
156
196
  Return `redirect(url)` from a page to send `303 See Other`. Raise