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.
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/CHANGELOG.md +240 -0
- {pyweb_stack-0.6.1/pyweb_stack.egg-info → pyweb_stack-0.6.3}/PKG-INFO +19 -4
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/README.md +14 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/THREAT_MODEL.md +38 -10
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/01-introduction.md +4 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/02-quickstart.md +12 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/08-pages-routing-assets.md +40 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/11-testing.md +30 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/12-deployment.md +7 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/14-cli.md +20 -11
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/16-limitations-roadmap.md +30 -7
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/17-ai-assistants.md +20 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/18-npm-packages.md +4 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/23-forms.md +29 -2
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/27-upgrading.md +26 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/29-performance.md +36 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/32-desktop.md +4 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/34-ui-and-motion.md +21 -1
- pyweb_stack-0.6.3/docs/35-dev-loop.md +176 -0
- pyweb_stack-0.6.3/docs/36-routes.md +130 -0
- pyweb_stack-0.6.3/docs/37-i18n.md +90 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyproject.toml +4 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/__init__.py +8 -9
- pyweb_stack-0.6.3/pyweb/_templates.py +18 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/a11y.py +52 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ai/guide.md +16 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/app.py +70 -7
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/app_loader.py +48 -4
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/asgi.py +3 -0
- pyweb_stack-0.6.3/pyweb/assist.py +394 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/auth.py +10 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/__init__.py +12 -6
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/admin.py +23 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/oauth.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/pages.py +17 -11
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/bench.py +35 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/browser.py +29 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/build.py +11 -6
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cache.py +43 -16
- pyweb_stack-0.6.3/pyweb/checks.py +111 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cli/__init__.py +230 -274
- pyweb_stack-0.6.3/pyweb/compiler/diskcache.py +91 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/lower.py +61 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/parser.py +56 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/pipeline.py +34 -5
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/pyjs.py +7 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/config.py +20 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/context.py +51 -10
- pyweb_stack-0.6.3/pyweb/csp.py +60 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/__init__.py +112 -16
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/migrate.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/rls.py +22 -13
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/gate.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/shell.py +2 -2
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/devlink.py +39 -5
- pyweb_stack-0.6.3/pyweb/devopen.py +92 -0
- pyweb_stack-0.6.3/pyweb/devserver.py +389 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/doctor.py +1 -1
- pyweb_stack-0.6.3/pyweb/errorboundary.py +22 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/forms.py +45 -15
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/hosting.py +46 -6
- pyweb_stack-0.6.3/pyweb/htmlquery.py +143 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/http.py +7 -0
- pyweb_stack-0.6.3/pyweb/i18n.py +437 -0
- pyweb_stack-0.6.3/pyweb/images.py +113 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/redis.py +21 -13
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/worker.py +35 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/livedata.py +41 -11
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp.py +39 -1
- pyweb_stack-0.6.3/pyweb/lsp_refs.py +280 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp.py +123 -198
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp_http.py +33 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mcp_tools.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/__init__.py +11 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/query.py +16 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/http.py +12 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/server.py +69 -16
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/openapi.py +50 -0
- pyweb_stack-0.6.3/pyweb/pagecache.py +144 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/plugins.py +12 -2
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/push.py +25 -9
- pyweb_stack-0.6.3/pyweb/qr.py +273 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/realtime.py +26 -21
- pyweb_stack-0.6.3/pyweb/redisutil.py +65 -0
- pyweb_stack-0.6.3/pyweb/routes.py +408 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rpc.py +26 -21
- pyweb_stack-0.6.3/pyweb/runtime/browser/boundary.js +48 -0
- pyweb_stack-0.6.3/pyweb/runtime/browser/dev/assist.js +156 -0
- pyweb_stack-0.6.3/pyweb/runtime/browser/dev/inspect.js +138 -0
- pyweb_stack-0.6.3/pyweb/runtime/browser/dev/reload.js +180 -0
- pyweb_stack-0.6.3/pyweb/runtime/browser/dev/timeline.js +144 -0
- pyweb_stack-0.6.3/pyweb/runtime/browser/dev/welcome.js +46 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/forms.js +133 -5
- pyweb_stack-0.6.3/pyweb/runtime/browser/i18n.js +42 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/pwa.js +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/runtime.js +9 -7
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/index.js +1 -1
- pyweb_stack-0.6.3/pyweb/runtime/browser/ui/stepper.js +99 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/server/__init__.py +141 -31
- pyweb_stack-0.6.3/pyweb/scaffold.py +212 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/security.py +39 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/serve.py +6 -63
- pyweb_stack-0.6.3/pyweb/share.py +173 -0
- pyweb_stack-0.6.3/pyweb/sourcemap.py +79 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ssr.py +30 -5
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ssr_codegen.py +2 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/storage.py +13 -8
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/devtools.py +8 -9
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/instruments.py +2 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/testing.py +79 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/data.pyweb +35 -4
- pyweb_stack-0.6.3/pyweb/ui/kit/image.pyweb +32 -0
- pyweb_stack-0.6.3/pyweb/ui/kit/stepper.pyweb +39 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/static/ui.css +20 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/upgrade.py +12 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/webhooks.py +8 -5
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3/pyweb_stack.egg-info}/PKG-INFO +19 -4
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/SOURCES.txt +44 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/requires.txt +5 -0
- pyweb_stack-0.6.3/tests/test_assist.py +243 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_authkit.py +89 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_backplane.py +8 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cli_deploy.py +3 -2
- pyweb_stack-0.6.3/tests/test_compile_cache.py +165 -0
- pyweb_stack-0.6.3/tests/test_compiler_errors.py +13 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_db_production.py +13 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dev_reload.py +5 -3
- pyweb_stack-0.6.3/tests/test_devopen.py +87 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dx_v5.py +1 -1
- pyweb_stack-0.6.3/tests/test_env_documented.py +45 -0
- pyweb_stack-0.6.3/tests/test_first_run.py +119 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_forms_v5.py +50 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_http_fetch.py +2 -1
- pyweb_stack-0.6.3/tests/test_i18n.py +126 -0
- pyweb_stack-0.6.3/tests/test_images.py +83 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_v5.py +49 -3
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_livedata.py +14 -0
- pyweb_stack-0.6.3/tests/test_lsp_refs.py +112 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp_v6.py +11 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_net.py +78 -0
- pyweb_stack-0.6.3/tests/test_pagecache.py +130 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_push.py +16 -1
- pyweb_stack-0.6.3/tests/test_qr.py +103 -0
- pyweb_stack-0.6.3/tests/test_routes.py +258 -0
- pyweb_stack-0.6.3/tests/test_security_063.py +95 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_server_db.py +2 -2
- pyweb_stack-0.6.3/tests/test_share.py +180 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_static_serving.py +1 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_streaming.py +18 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_systems.py +58 -1
- pyweb_stack-0.6.3/tests/test_testing_helpers.py +132 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_trusted_types.py +9 -1
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_webhooks.py +1 -0
- pyweb_stack-0.6.1/pyweb/_templates.py +0 -3
- pyweb_stack-0.6.1/pyweb/runtime/browser/dev/reload.js +0 -102
- pyweb_stack-0.6.1/tests/test_compile_cache.py +0 -88
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/LICENSE +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/MANIFEST.in +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/03-tutorial.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/04-pyweb-files.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/05-reactivity.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/06-server-functions.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/07-browser-python.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/09-data.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/10-auth.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/13-security.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/15-toolkit.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/19-layouts-navigation.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/20-ai-apps.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/21-live-data.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/22-recipe-web3.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/24-jobs.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/25-scaling.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/26-observability.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/28-saas-tutorial.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/30-teams.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/31-pwa-push.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/docs/33-webhooks-files.md +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/_speedups.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/html.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/models.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/orgpages.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/orgs.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/passwords.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/privacy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/teams.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/throttle.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/tokens.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/authkit/webauthn.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/cli/__main__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/ast.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/codegen/ir.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/errors.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/compiler/rpc.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/csp_report.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/css.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/dialect.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/schema.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/db/seeds.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/decorators.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/legacy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/plan.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deploy/targets.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/deprecation.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/desktop/package.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/dts.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/fieldcrypt.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/filegc.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/builtin.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/core.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/db.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/legacy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/memory.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/jobs/schedule.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/keys.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/livetable.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_data.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_teams.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/lsp_ui.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/mail.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/markdown.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/fields.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/models/relations.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/motion.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/live.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/net/ws.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/observability.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/packages.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/platform.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/profile.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/pwa.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/py.typed +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rooms.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/rules.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/auth.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/desktop.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/live.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/markdown.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-api.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-count.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-drag.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-layout.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion-math.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/motion.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/sw.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/accordion.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/calendar.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/carousel.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/checkbox.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/collapsible.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/combobox.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/command.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/context-menu.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/core.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/dialog.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/hover-card.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/listbox.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menu.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/menubar.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/navigation-menu.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/otp.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/popover.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/radio-group.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/resizable.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/select.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/sidebar.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/slider.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tabs.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toaster.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle-group.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/toggle.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/browser/ui/tooltip.js +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/runtime/server/argcheck.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/sbom.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/secret_scan.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/sync.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/logs.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/metrics.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/telemetry/tracing.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/ai-chat.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/app.css +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/auth.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/blog.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/chat.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/counter.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/desktop/app.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/desktop/test_app.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/app.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/seeds.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/saas/test_app.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/templates/todo.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/tenancy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/__init__.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/cli.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/extending.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/button.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/card.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/carousel.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/chart.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/command.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/controls.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/date.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/dialog.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/disclosure.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/display.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/layout.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/menu.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/overlay.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/kit/style.pyweb +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/serve.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/source_edit.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/ui/theme.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb/uploads.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/dependency_links.txt +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/entry_points.txt +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/pyweb_stack.egg-info/top_level.txt +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/setup.cfg +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/setup.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_account_privacy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_api_tokens.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_contract.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_security.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_sessions.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_auth_v1.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_authkit_units.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_browser_build.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_budget.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cli_test.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_codegen.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_compiler.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_cron.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_customise.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_data_layer.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_deploy_upload_render.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_deploy_v5.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_desktop.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_docs.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_doctor.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_dts.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_e2e.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_encrypted_fields.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_error_messages.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_examples.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_filegc.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_forms_security_obs.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_fuzz_requests.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_app.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_jobs_cache_realtime.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_keys_sessions.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_live_patches.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_lsp.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_lsp_teams.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_markdown.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_mcp_teams.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_migrations_property.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_migrations_v5.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_models_db.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_motion.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_multifile.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_multipage.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_observability.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_openapi.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_org_pages.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_packages.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_parser.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_parser_v1.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_plugins_platform_bench.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_production_gaps.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_profile.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_pwa.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_pyjs_semantics.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rate_per_function.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_reactivity.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_realtime_transport.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_reference_apps.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_request_hardening.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rpc_placement.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_rpc_production.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_runtime_signals.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_sbom.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_secret_scan.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_speedups.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_ssr_codegen.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_static_hashing.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_sync_live.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_telemetry.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_tenancy.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_testing_orgs.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_threat_model.py +0 -0
- {pyweb_stack-0.6.1 → pyweb_stack-0.6.3}/tests/test_ui_kit.py +0 -0
- {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.
|
|
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 #
|
|
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
|
|
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 #
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 |
|
|
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 |
|
|
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
|
|
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
|