pyweb-stack 0.6.2__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 (397) hide show
  1. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/CHANGELOG.md +181 -0
  2. {pyweb_stack-0.6.2/pyweb_stack.egg-info → pyweb_stack-0.6.3}/PKG-INFO +6 -2
  3. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/README.md +1 -1
  4. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/THREAT_MODEL.md +24 -11
  5. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/02-quickstart.md +5 -0
  6. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/08-pages-routing-assets.md +40 -0
  7. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/11-testing.md +30 -0
  8. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/12-deployment.md +7 -0
  9. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/14-cli.md +7 -4
  10. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/16-limitations-roadmap.md +22 -3
  11. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/18-npm-packages.md +4 -0
  12. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/23-forms.md +29 -2
  13. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/27-upgrading.md +26 -0
  14. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/29-performance.md +36 -0
  15. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/32-desktop.md +4 -0
  16. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/34-ui-and-motion.md +21 -1
  17. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/35-dev-loop.md +25 -0
  18. pyweb_stack-0.6.3/docs/36-routes.md +130 -0
  19. pyweb_stack-0.6.3/docs/37-i18n.md +90 -0
  20. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyproject.toml +4 -3
  21. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/__init__.py +8 -9
  22. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/a11y.py +52 -0
  23. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ai/guide.md +16 -0
  24. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/app.py +70 -7
  25. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/app_loader.py +21 -2
  26. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/asgi.py +3 -0
  27. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/auth.py +10 -3
  28. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/__init__.py +12 -6
  29. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/admin.py +23 -1
  30. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/oauth.py +1 -1
  31. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/pages.py +17 -11
  32. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/bench.py +3 -3
  33. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/browser.py +29 -0
  34. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/build.py +11 -6
  35. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/cache.py +43 -16
  36. pyweb_stack-0.6.3/pyweb/checks.py +111 -0
  37. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/cli/__init__.py +124 -430
  38. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/diskcache.py +21 -2
  39. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/lower.py +59 -1
  40. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/pipeline.py +18 -1
  41. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/pyjs.py +7 -0
  42. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/config.py +20 -1
  43. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/context.py +51 -10
  44. pyweb_stack-0.6.3/pyweb/csp.py +60 -0
  45. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/__init__.py +112 -16
  46. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/migrate.py +1 -1
  47. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/rls.py +22 -13
  48. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/desktop/gate.py +1 -1
  49. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/desktop/shell.py +2 -2
  50. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/devlink.py +2 -2
  51. pyweb_stack-0.6.3/pyweb/devserver.py +389 -0
  52. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/doctor.py +1 -1
  53. pyweb_stack-0.6.3/pyweb/errorboundary.py +22 -0
  54. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/forms.py +45 -15
  55. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/hosting.py +42 -3
  56. pyweb_stack-0.6.3/pyweb/htmlquery.py +143 -0
  57. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/http.py +7 -0
  58. pyweb_stack-0.6.3/pyweb/i18n.py +437 -0
  59. pyweb_stack-0.6.3/pyweb/images.py +113 -0
  60. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/redis.py +21 -13
  61. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/worker.py +35 -3
  62. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/livedata.py +41 -11
  63. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/lsp.py +39 -1
  64. pyweb_stack-0.6.3/pyweb/lsp_refs.py +280 -0
  65. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/mcp.py +43 -198
  66. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/mcp_http.py +33 -3
  67. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/mcp_tools.py +1 -1
  68. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/models/__init__.py +11 -0
  69. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/models/query.py +16 -0
  70. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/net/http.py +12 -0
  71. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/net/server.py +69 -16
  72. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/openapi.py +50 -0
  73. pyweb_stack-0.6.3/pyweb/pagecache.py +144 -0
  74. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/plugins.py +12 -2
  75. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/push.py +25 -9
  76. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/realtime.py +26 -21
  77. pyweb_stack-0.6.3/pyweb/redisutil.py +65 -0
  78. pyweb_stack-0.6.3/pyweb/routes.py +408 -0
  79. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/rpc.py +26 -21
  80. pyweb_stack-0.6.3/pyweb/runtime/browser/boundary.js +48 -0
  81. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/forms.js +133 -5
  82. pyweb_stack-0.6.3/pyweb/runtime/browser/i18n.js +42 -0
  83. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/pwa.js +1 -1
  84. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/runtime.js +5 -4
  85. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/index.js +1 -1
  86. pyweb_stack-0.6.3/pyweb/runtime/browser/ui/stepper.js +99 -0
  87. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/server/__init__.py +141 -33
  88. pyweb_stack-0.6.3/pyweb/scaffold.py +212 -0
  89. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/security.py +39 -0
  90. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/serve.py +6 -63
  91. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/share.py +5 -4
  92. pyweb_stack-0.6.3/pyweb/sourcemap.py +79 -0
  93. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ssr.py +30 -5
  94. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ssr_codegen.py +2 -0
  95. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/storage.py +13 -8
  96. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/devtools.py +4 -8
  97. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/instruments.py +2 -0
  98. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/testing.py +79 -0
  99. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/data.pyweb +35 -4
  100. pyweb_stack-0.6.3/pyweb/ui/kit/image.pyweb +32 -0
  101. pyweb_stack-0.6.3/pyweb/ui/kit/stepper.pyweb +39 -0
  102. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/static/ui.css +20 -0
  103. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/upgrade.py +12 -0
  104. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/webhooks.py +8 -5
  105. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3/pyweb_stack.egg-info}/PKG-INFO +6 -2
  106. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/SOURCES.txt +29 -0
  107. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/requires.txt +5 -0
  108. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_authkit.py +89 -1
  109. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_backplane.py +8 -0
  110. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_cli_deploy.py +3 -2
  111. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_compile_cache.py +17 -1
  112. pyweb_stack-0.6.3/tests/test_compiler_errors.py +13 -0
  113. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_db_production.py +13 -0
  114. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_dx_v5.py +1 -1
  115. pyweb_stack-0.6.3/tests/test_env_documented.py +45 -0
  116. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_forms_v5.py +50 -1
  117. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_http_fetch.py +2 -1
  118. pyweb_stack-0.6.3/tests/test_i18n.py +126 -0
  119. pyweb_stack-0.6.3/tests/test_images.py +83 -0
  120. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_jobs_v5.py +49 -3
  121. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_livedata.py +14 -0
  122. pyweb_stack-0.6.3/tests/test_lsp_refs.py +112 -0
  123. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_mcp_v6.py +11 -0
  124. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_net.py +78 -0
  125. pyweb_stack-0.6.3/tests/test_pagecache.py +130 -0
  126. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_push.py +16 -1
  127. pyweb_stack-0.6.3/tests/test_routes.py +258 -0
  128. pyweb_stack-0.6.3/tests/test_security_063.py +95 -0
  129. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_server_db.py +2 -2
  130. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_share.py +27 -0
  131. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_static_serving.py +1 -1
  132. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_streaming.py +18 -1
  133. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_systems.py +58 -1
  134. pyweb_stack-0.6.3/tests/test_testing_helpers.py +132 -0
  135. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_trusted_types.py +1 -1
  136. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_webhooks.py +1 -0
  137. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/LICENSE +0 -0
  138. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/MANIFEST.in +0 -0
  139. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/01-introduction.md +0 -0
  140. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/03-tutorial.md +0 -0
  141. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/04-pyweb-files.md +0 -0
  142. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/05-reactivity.md +0 -0
  143. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/06-server-functions.md +0 -0
  144. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/07-browser-python.md +0 -0
  145. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/09-data.md +0 -0
  146. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/10-auth.md +0 -0
  147. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/13-security.md +0 -0
  148. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/15-toolkit.md +0 -0
  149. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/17-ai-assistants.md +0 -0
  150. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/19-layouts-navigation.md +0 -0
  151. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/20-ai-apps.md +0 -0
  152. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/21-live-data.md +0 -0
  153. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/22-recipe-web3.md +0 -0
  154. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/24-jobs.md +0 -0
  155. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/25-scaling.md +0 -0
  156. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/26-observability.md +0 -0
  157. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/28-saas-tutorial.md +0 -0
  158. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/30-teams.md +0 -0
  159. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/31-pwa-push.md +0 -0
  160. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/docs/33-webhooks-files.md +0 -0
  161. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/_speedups.py +0 -0
  162. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/_templates.py +0 -0
  163. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/assist.py +0 -0
  164. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/html.py +0 -0
  165. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/models.py +0 -0
  166. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/orgpages.py +0 -0
  167. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/orgs.py +0 -0
  168. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/passwords.py +0 -0
  169. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/privacy.py +0 -0
  170. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/teams.py +0 -0
  171. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/throttle.py +0 -0
  172. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/tokens.py +0 -0
  173. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/authkit/webauthn.py +0 -0
  174. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/cli/__main__.py +0 -0
  175. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/__init__.py +0 -0
  176. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/ast.py +0 -0
  177. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/__init__.py +0 -0
  178. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/ir.py +0 -0
  179. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/errors.py +0 -0
  180. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/parser.py +0 -0
  181. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/compiler/rpc.py +0 -0
  182. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/csp_report.py +0 -0
  183. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/css.py +0 -0
  184. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/dialect.py +0 -0
  185. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/schema.py +0 -0
  186. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/db/seeds.py +0 -0
  187. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/decorators.py +0 -0
  188. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/deploy/__init__.py +0 -0
  189. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/deploy/legacy.py +0 -0
  190. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/deploy/plan.py +0 -0
  191. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/deploy/targets.py +0 -0
  192. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/deprecation.py +0 -0
  193. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/desktop/__init__.py +0 -0
  194. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/desktop/package.py +0 -0
  195. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/devopen.py +0 -0
  196. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/dts.py +0 -0
  197. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/fieldcrypt.py +0 -0
  198. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/filegc.py +0 -0
  199. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/__init__.py +0 -0
  200. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/builtin.py +0 -0
  201. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/core.py +0 -0
  202. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/db.py +0 -0
  203. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/legacy.py +0 -0
  204. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/memory.py +0 -0
  205. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/jobs/schedule.py +0 -0
  206. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/keys.py +0 -0
  207. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/livetable.py +0 -0
  208. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/lsp_data.py +0 -0
  209. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/lsp_teams.py +0 -0
  210. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/lsp_ui.py +0 -0
  211. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/mail.py +0 -0
  212. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/markdown.py +0 -0
  213. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/models/fields.py +0 -0
  214. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/models/relations.py +0 -0
  215. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/motion.py +0 -0
  216. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/net/__init__.py +0 -0
  217. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/net/live.py +0 -0
  218. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/net/ws.py +0 -0
  219. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/observability.py +0 -0
  220. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/packages.py +0 -0
  221. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/platform.py +0 -0
  222. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/profile.py +0 -0
  223. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/pwa.py +0 -0
  224. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/py.typed +0 -0
  225. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/qr.py +0 -0
  226. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/rooms.py +0 -0
  227. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/rules.py +0 -0
  228. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/auth.js +0 -0
  229. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/desktop.js +0 -0
  230. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/dev/assist.js +0 -0
  231. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/dev/inspect.js +0 -0
  232. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/dev/reload.js +0 -0
  233. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/dev/timeline.js +0 -0
  234. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/dev/welcome.js +0 -0
  235. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/live.js +0 -0
  236. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/markdown.js +0 -0
  237. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-api.js +0 -0
  238. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-count.js +0 -0
  239. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-drag.js +0 -0
  240. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-layout.js +0 -0
  241. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-math.js +0 -0
  242. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion.js +0 -0
  243. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/sw.js +0 -0
  244. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/accordion.js +0 -0
  245. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/calendar.js +0 -0
  246. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/carousel.js +0 -0
  247. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/checkbox.js +0 -0
  248. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/collapsible.js +0 -0
  249. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/combobox.js +0 -0
  250. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/command.js +0 -0
  251. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/context-menu.js +0 -0
  252. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/core.js +0 -0
  253. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/dialog.js +0 -0
  254. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/hover-card.js +0 -0
  255. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/listbox.js +0 -0
  256. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menu.js +0 -0
  257. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menubar.js +0 -0
  258. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/navigation-menu.js +0 -0
  259. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/otp.js +0 -0
  260. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/popover.js +0 -0
  261. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/radio-group.js +0 -0
  262. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/resizable.js +0 -0
  263. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/select.js +0 -0
  264. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/sidebar.js +0 -0
  265. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/slider.js +0 -0
  266. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tabs.js +0 -0
  267. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toaster.js +0 -0
  268. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle-group.js +0 -0
  269. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle.js +0 -0
  270. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tooltip.js +0 -0
  271. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/runtime/server/argcheck.py +0 -0
  272. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/sbom.py +0 -0
  273. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/secret_scan.py +0 -0
  274. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/sync.py +0 -0
  275. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/__init__.py +0 -0
  276. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/logs.py +0 -0
  277. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/metrics.py +0 -0
  278. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/telemetry/tracing.py +0 -0
  279. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/ai-chat.pyweb +0 -0
  280. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/app.css +0 -0
  281. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/auth.pyweb +0 -0
  282. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/blog.pyweb +0 -0
  283. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/chat.pyweb +0 -0
  284. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/counter.pyweb +0 -0
  285. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/desktop/app.pyweb +0 -0
  286. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/desktop/test_app.py +0 -0
  287. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/saas/app.pyweb +0 -0
  288. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/saas/seeds.py +0 -0
  289. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/saas/test_app.py +0 -0
  290. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/templates/todo.pyweb +0 -0
  291. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/tenancy.py +0 -0
  292. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/__init__.py +0 -0
  293. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/cli.py +0 -0
  294. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/extending.py +0 -0
  295. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/button.pyweb +0 -0
  296. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/card.pyweb +0 -0
  297. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/carousel.pyweb +0 -0
  298. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/chart.pyweb +0 -0
  299. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/command.pyweb +0 -0
  300. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/controls.pyweb +0 -0
  301. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/date.pyweb +0 -0
  302. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/dialog.pyweb +0 -0
  303. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/disclosure.pyweb +0 -0
  304. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/display.pyweb +0 -0
  305. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/layout.pyweb +0 -0
  306. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/menu.pyweb +0 -0
  307. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/overlay.pyweb +0 -0
  308. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/kit/style.pyweb +0 -0
  309. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/serve.py +0 -0
  310. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/source_edit.py +0 -0
  311. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/ui/theme.py +0 -0
  312. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb/uploads.py +0 -0
  313. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/dependency_links.txt +0 -0
  314. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/entry_points.txt +0 -0
  315. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/top_level.txt +0 -0
  316. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/setup.cfg +0 -0
  317. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/setup.py +0 -0
  318. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_account_privacy.py +0 -0
  319. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_api_tokens.py +0 -0
  320. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_assist.py +0 -0
  321. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_auth.py +0 -0
  322. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_auth_contract.py +0 -0
  323. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_auth_security.py +0 -0
  324. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_auth_sessions.py +0 -0
  325. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_auth_v1.py +0 -0
  326. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_authkit_units.py +0 -0
  327. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_browser_build.py +0 -0
  328. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_budget.py +0 -0
  329. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_cli_test.py +0 -0
  330. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_codegen.py +0 -0
  331. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_compiler.py +0 -0
  332. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_cron.py +0 -0
  333. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_customise.py +0 -0
  334. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_data_layer.py +0 -0
  335. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_deploy_upload_render.py +0 -0
  336. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_deploy_v5.py +0 -0
  337. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_desktop.py +0 -0
  338. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_dev_reload.py +0 -0
  339. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_devopen.py +0 -0
  340. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_docs.py +0 -0
  341. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_doctor.py +0 -0
  342. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_dts.py +0 -0
  343. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_e2e.py +0 -0
  344. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_encrypted_fields.py +0 -0
  345. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_error_messages.py +0 -0
  346. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_examples.py +0 -0
  347. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_filegc.py +0 -0
  348. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_first_run.py +0 -0
  349. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_forms_security_obs.py +0 -0
  350. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_fuzz_requests.py +0 -0
  351. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_jobs_app.py +0 -0
  352. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_jobs_cache_realtime.py +0 -0
  353. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_keys_sessions.py +0 -0
  354. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_live_patches.py +0 -0
  355. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_lsp.py +0 -0
  356. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_lsp_teams.py +0 -0
  357. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_markdown.py +0 -0
  358. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_mcp.py +0 -0
  359. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_mcp_teams.py +0 -0
  360. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_migrations_property.py +0 -0
  361. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_migrations_v5.py +0 -0
  362. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_models_db.py +0 -0
  363. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_motion.py +0 -0
  364. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_multifile.py +0 -0
  365. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_multipage.py +0 -0
  366. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_observability.py +0 -0
  367. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_openapi.py +0 -0
  368. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_org_pages.py +0 -0
  369. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_packages.py +0 -0
  370. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_parser.py +0 -0
  371. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_parser_v1.py +0 -0
  372. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_plugins_platform_bench.py +0 -0
  373. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_production_gaps.py +0 -0
  374. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_profile.py +0 -0
  375. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_pwa.py +0 -0
  376. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_pyjs_semantics.py +0 -0
  377. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_qr.py +0 -0
  378. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_rate_per_function.py +0 -0
  379. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_reactivity.py +0 -0
  380. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_realtime_transport.py +0 -0
  381. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_reference_apps.py +0 -0
  382. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_request_hardening.py +0 -0
  383. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_rpc_placement.py +0 -0
  384. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_rpc_production.py +0 -0
  385. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_runtime_signals.py +0 -0
  386. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_sbom.py +0 -0
  387. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_secret_scan.py +0 -0
  388. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_speedups.py +0 -0
  389. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_ssr_codegen.py +0 -0
  390. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_static_hashing.py +0 -0
  391. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_sync_live.py +0 -0
  392. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_telemetry.py +0 -0
  393. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_tenancy.py +0 -0
  394. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_testing_orgs.py +0 -0
  395. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_threat_model.py +0 -0
  396. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_ui_kit.py +0 -0
  397. {pyweb_stack-0.6.2 → pyweb_stack-0.6.3}/tests/test_website.py +0 -0
@@ -6,6 +6,187 @@ 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
+
9
190
  ## [0.6.2]
10
191
 
11
192
  The dev loop. Saves update the open page in place with every page and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweb-stack
3
- Version: 0.6.2
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">
@@ -577,7 +581,7 @@ Upgrade with `pip install -U pyweb-stack`.
577
581
 
578
582
  | Version | Highlights |
579
583
  |---|---|
580
- | 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`) |
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 |
581
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` |
582
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` |
583
587
  | 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
@@ -503,7 +503,7 @@ Upgrade with `pip install -U pyweb-stack`.
503
503
 
504
504
  | Version | Highlights |
505
505
  |---|---|
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`) |
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 |
507
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` |
508
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` |
509
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,7 +183,8 @@ 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` |
@@ -185,9 +197,10 @@ person running it, on that machine: a request must come from a loopback address
185
197
 
186
198
  | | Threat | Mitigation | Proof |
187
199
  |---|---|---|---|
188
- | 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 | `test_share.py::test_the_link_sets_a_cookie_and_nothing_else_gets_in`, `test_share.py::test_pyweb_dev_share_end_to_end` |
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` |
189
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` |
190
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` |
191
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` |
192
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` |
193
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` |
@@ -87,9 +87,14 @@ page Home("/") signals ['count'] rpc []
87
87
  ```bash
88
88
  pyweb check app.pyweb # compile, security and accessibility checks (CI-friendly)
89
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))")
90
91
  pyweb serve dist # production server with /healthz
91
92
  ```
92
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
+
93
98
  `dist/` contains your app source, static assets and a `Dockerfile`; see
94
99
  [Deployment](12-deployment.md).
95
100
 
@@ -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
@@ -41,6 +41,36 @@ def test_notes():
41
41
  | `.cookies` | the cookie jar |
42
42
  | `.app.module` | the executed app module, for direct access to its objects |
43
43
 
44
+ ### Looking inside pages and components
45
+
46
+ ```python
47
+ from pyweb.testing import TestClient, assert_snapshot
48
+
49
+
50
+ def test_todos(client):
51
+ r = client.get("/")
52
+ assert [li.text for li in r.find_all("ul.todos li")] == ["milk", "bread"]
53
+ assert r.find("li.done").text == "milk"
54
+
55
+ badge = client.render("Badge", label="new", tone="warn") # one component, with props
56
+ assert badge.find("span").classes == ["badge", "warn"]
57
+
58
+ assert client.a11y("/") == [] # accessibility of the rendered page
59
+
60
+ assert_snapshot(r.find("main").text, "home-text") # __snapshots__/home-text.txt
61
+ ```
62
+
63
+ - `find` / `find_all` take CSS selectors: `tag`, `#id`, `.class`, `[attr]`, `[attr=value]`, combined
64
+ (`button.primary[disabled]`), descendants (`ul li`) and lists (`h1, h2`). Elements have `.text`
65
+ (what a person reads), `.attrs`, `.get(name)` and `.classes`.
66
+ - `client.render(name, **props)` renders a component (or a page) on its own, in a request, so
67
+ `session` works.
68
+ - `client.a11y(path)` runs the accessibility rules on what the page became (components included):
69
+ images without `alt`, fields without a label, buttons and links without a name, positive
70
+ `tabindex`, a missing page language. An empty list is a pass.
71
+ - `assert_snapshot(value, name)` compares with `__snapshots__/<name>.txt` next to the test (written
72
+ the first time). After a change you meant, run the tests with `PYWEB_UPDATE_SNAPSHOTS=1`.
73
+
44
74
  ## Apps with accounts, data and jobs
45
75
 
46
76
  For an app with a database, give each test its own: change into
@@ -227,6 +227,13 @@ across processes use `RedisCache`, and for
227
227
  | `PYWEB_REDIS_URL` | unset | share rate limits and live updates between server processes |
228
228
  | `PYWEB_MAX_CONNECTIONS` | `10000` | simultaneous connections per server process (`503` beyond) |
229
229
  | `PYWEB_THREADS` | `32` | threads per process rendering pages and running server functions |
230
+ | `PYWEB_DB_POOL` | `10` | database connections each process may open (keep `workers × pool` under the server's limit) |
231
+ | `PYWEB_MAX_QUEUE` | `256` | requests waiting for a thread before new ones get `503` with `Retry-After` |
232
+ | `PYWEB_SHUTDOWN_TIMEOUT` | `25` | seconds to finish requests and jobs after `SIGTERM` |
233
+ | `PYWEB_MIGRATE_ON_START` | unset | `1`: apply pending migrations when the server starts (same as `pyweb serve --migrate`) |
234
+ | `PYWEB_WORKER_HEARTBEAT` | temp dir | file `pyweb worker` touches every round (point a container health check at it) |
235
+ | `PYWEB_METRICS_EVERY` | `5` | seconds between each worker process writing its metrics for `/metrics` (with `--workers`) |
236
+ | `PYWEB_STRICT_RELATIONS` | on outside production | reading a relation you didn't `include()` is an error (`1`) or a logged warning (`0`) |
230
237
  | `PYWEB_ALLOWED_ORIGINS` | unset | other origins allowed to open the pages' WebSocket (comma-separated); by default only the site itself |
231
238
  | `WEB_CONCURRENCY` | `1` | worker processes for `pyweb serve` (same as `--workers`) |
232
239
  | `PYWEB_WORKER` | `1` | `0`: don't run background jobs inside the web server (run `pyweb worker` processes; see [Background jobs](24-jobs.md)) |
@@ -7,7 +7,7 @@ pyweb new [NAME] [--template T] [--theme PRESET]
7
7
  (apps with a database also get migrations/0001_initial.py and a README);
8
8
  PRESET = indigo | zinc | slate | rose | ocean | violet | emerald | amber
9
9
  (written into App(theme=...)); with no NAME, in a terminal, it asks
10
- pyweb dev FILE [--port 8000] [--host 127.0.0.1] [--no-reload] [--no-open] [--share]
10
+ pyweb dev FILE [--port 8000] [--host 127.0.0.1] [--no-reload] [--no-open] [--share] [--debug [--debug-port 5678] [--wait]]
11
11
  development server (see The dev loop): saves update the open page
12
12
  in place, keeping page and component state (?fresh starts over);
13
13
  Alt-click opens an element's line in your editor, Alt+Shift-click
@@ -28,10 +28,11 @@ pyweb doctor [FILE] [--production] [--json] [--port 8000]
28
28
  pyweb upgrade [FILE] [--fix] what to change for this PyWeb version (file and line); --fix applies
29
29
  the safe rewrites
30
30
  pyweb inspect FILE [--security] where every name runs and why; RPC table; findings
31
- pyweb build FILE [--out dist] [--production] [--budget PATH=SIZE ...]
31
+ pyweb build FILE [--out dist] [--production] [--sourcemaps] [--budget PATH=SIZE ...]
32
32
  self-contained dist/; --production hashes and minifies
33
33
  pyweb serve [DIR] [--host 0.0.0.0] [--port $PORT|8000] [--workers N] [--migrate]
34
- production server for a built dist/ (N processes)
34
+ production server for a built dist/ (N processes);
35
+ needs PYWEB_AUTH_SECRET (or PYWEB_ENV=development)
35
36
  pyweb ui list | add NAME... [--force] | diff NAME [--dir .]
36
37
  the UI kit's components; copy one into components/ to change
37
38
  it; show how your copy differs from the kit
@@ -76,8 +77,10 @@ pyweb add [PKG[@RANGE] ...] [--app DIR]
76
77
  and pin them in pyweb.lock; no arguments: reinstall from the lock
77
78
  pyweb remove PKG ... [--app DIR] remove npm packages (see npm packages)
78
79
  pyweb dts FILE.d.ts Python dataclasses from TypeScript declarations
80
+ pyweb i18n extract | init LANG | check [--strict] [--app app.pyweb]
81
+ translation catalogs in locales/ (see Translations)
79
82
  pyweb mcp MCP server over stdio for AI assistants (see AI assistants & MCP)
80
- pyweb mcp --http [--port 8765] [--token T] the same over Streamable HTTP (127.0.0.1 only without a token)
83
+ pyweb mcp --http [--port 8765] [--token T] the same over Streamable HTTP (always with a bearer token)
81
84
  pyweb lsp language server over stdio for editors (see below)
82
85
  pyweb test [PATH] run pytest on PATH (a test file, a folder or app.pyweb's folder;
83
86
  default: the current folder, where new apps keep test_app.py)
@@ -40,7 +40,11 @@ can decide up front whether they matter for your app.
40
40
  saves update the page in place with component state kept, Alt-click
41
41
  opens any element's source, Fix with AI and Edit with AI, a
42
42
  time-travel timeline, `pyweb dev --share`, a faster first start and a
43
- guided `pyweb new`.
43
+ guided `pyweb new`. 0.6.3 was a security and reliability release plus
44
+ what apps missed most: HTTP routes and middleware, page caching with
45
+ ETags, translations, `<ErrorBoundary>`, test helpers, source maps and
46
+ `pyweb dev --debug`, CSV export, drag-and-drop uploads, a `Stepper`,
47
+ responsive `<Image>`, and rename and find-references in the editor.
44
48
  - **0.5**: production apps. Models with relations and migrations, forms
45
49
  checked in the browser and on the server, an auth kit (passwords,
46
50
  passkeys, emailed links, OAuth, two-step sign-in, an admin), row-level
@@ -70,8 +74,23 @@ Planned, roughly in order. Nothing here is promised for a date.
70
74
 
71
75
  1. **1.0**: the API, RPC protocol and CLI frozen under semantic versioning,
72
76
  after 0.6 has been used in production for a while.
73
- 2. **File uploads from the playground and the admin.**
74
- 3. **Fix with AI in the editor**, from the same proposals the dev page
77
+ 2. **0.7: the next things apps ask for.**
78
+ - Billing: Stripe Checkout and the customer portal, plans tied to the
79
+ organization's features, webhooks handled for you.
80
+ - An in-app notifications center (a bell, unread counts, live), with
81
+ email and push from the same call.
82
+ - Full-text search on Models (SQLite FTS5, Postgres `tsvector`, MySQL
83
+ `FULLTEXT`) behind one `Model.search()`.
84
+ - A rich-text editor component whose output is sanitized on the server.
85
+ - Feature flags and A/B tests per user or organization.
86
+ - Streaming server rendering: `<Await>` sends the page shell first and
87
+ slow parts as they finish.
88
+ - Pages built ahead of time from data, rebuilt on a schedule or on a
89
+ write (incremental static regeneration).
90
+ - `pyweb typecheck`: pyright over the Python in `.pyweb` files.
91
+ - Admin customization per Model: list columns, filters, actions, forms.
92
+ 3. **File uploads from the playground.**
93
+ 4. **Fix with AI in the editor**, from the same proposals the dev page
75
94
  shows (diff, apply, undo), through the VS Code extension.
76
95
 
77
96
  Ideas, bug reports and pull requests are welcome; see
@@ -208,3 +208,7 @@ their URLs include the version.
208
208
  Trusted Types rule the others have (many libraries write `innerHTML`
209
209
  themselves); the rest of the policy still applies. See
210
210
  [Security](13-security.md#cross-site-scripting).
211
+
212
+
213
+ `PYWEB_NPM_REGISTRY` points `pyweb add` at another registry (a mirror or a private one); the default is
214
+ `https://registry.npmjs.org`.