tempest-fastapi-sdk 0.44.0__tar.gz → 0.46.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/CHANGELOG.md +50 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/PKG-INFO +3 -3
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/README.md +2 -2
- tempest_fastapi_sdk-0.46.0/docs/recipes/deploy-safety.en.md +111 -0
- tempest_fastapi_sdk-0.46.0/docs/recipes/deploy-safety.md +110 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.en.md +2 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.md +2 -0
- tempest_fastapi_sdk-0.46.0/docs/recipes/multi-tenant.en.md +120 -0
- tempest_fastapi_sdk-0.46.0/docs/recipes/multi-tenant.md +120 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.md +3 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/mkdocs.yml +4 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/pyproject.toml +1 -1
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/__init__.py +7 -1
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/__init__.py +2 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/__init__.py +2 -0
- tempest_fastapi_sdk-0.46.0/tempest_fastapi_sdk/api/middlewares/graceful.py +225 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/__init__.py +8 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/migrations.py +116 -0
- tempest_fastapi_sdk-0.46.0/tempest_fastapi_sdk/db/tenant.py +348 -0
- tempest_fastapi_sdk-0.46.0/tests/api/test_graceful.py +78 -0
- tempest_fastapi_sdk-0.46.0/tests/db/test_safe_upgrade.py +98 -0
- tempest_fastapi_sdk-0.46.0/tests/db/test_tenant.py +170 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/uv.lock +1 -1
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/ci.yml +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/docs.yml +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/.gitignore +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/.python-version +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/CLAUDE.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/Makefile +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/index.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/index.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.en.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.md +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/scripts/extract_recipe.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/auth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/config.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/forms.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/session.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/site.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/static/admin.css +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/base.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/dashboard.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/detail.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/form.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/list.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/login.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/logs.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/mfa.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/cookies.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/auth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/handlers.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/body_size.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/cors.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/csrf.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/idempotency.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/request_id.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/oauth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/health.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/logs.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/metrics.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/server.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/static.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/tracing.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/webhooks.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/page_renderer.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/schemas.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/service.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_error.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_success.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_error.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_form.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_success.html +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/decorator.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/README.md.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/env.example.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/gitignore.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/main.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/pyproject.toml.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/app.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/auth.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/resources.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/routers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/controllers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/exceptions.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/settings.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/user.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/repositories/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/schemas/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/server.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/services/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/utils/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/test_smoke.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/db.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/generate.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/lint.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/main.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/new.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/src_layers.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/user.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/context.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/enums.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/logging.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/env.py.template +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/connection.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/mixins.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/outbox.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/repository.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/slow_query.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_recovery_code_model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_token_model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/conflict.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/forbidden.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/jwt.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/not_found.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/too_many_requests.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/unauthorized.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/upload.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/validation.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/py.typed +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/link_headers.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/logs.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/pagination.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/response.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/dependencies.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/middleware.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/schemas.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/service.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/store.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/mixins.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/event_stream.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/minio_client.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/scheduler.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/database.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/client_ip.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/datetime.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/dict.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/download.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/email.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/http_client.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/jwt.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/log.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/metrics.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/password.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/regex.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/throttle.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/totp.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/upload.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/schemas.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/hub.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/schemas.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_auth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_forms.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_logs_nav.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_site.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_user_model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_body_size.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cookies.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cors.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_csrf.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_dependencies_auth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_handlers.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_health_router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_idempotency.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_jwt_dependency.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_logs_router.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_oauth.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_prometheus.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_request_id_middleware.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_role_dependency.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_server.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_static.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tracing.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks_rsa.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_service.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_decorator.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_db.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_generate.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_main.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_user.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/conftest.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/test_base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_context.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_enums.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_logging.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_bulk_ops.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_connection.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_migrations.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_model.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_outbox.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_repository.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_slow_query.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/test_exceptions.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_cursor_pagination.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_link_headers.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_pagination.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_response.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/services/test_base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/test_sessions.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_base.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/test_event_stream.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/test_minio_client.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_scheduler.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/test_database.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_client_ip.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_datetime.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_dict.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_download.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_email.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_http_client.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_jwt.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_lazy_extras.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_log.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_metrics.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_password.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_regex.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_throttle.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_upload.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_schemas.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.44.0 → tempest_fastapi_sdk-0.46.0}/tests/websockets/test_hub_and_router.py +0 -0
|
@@ -5,6 +5,56 @@ All notable changes to **tempest-fastapi-sdk** are listed below.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.46.0] — 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`AlembicHelper.safe_upgrade(revision="head", *, force=False)`** —
|
|
13
|
+
runs the upgrade only after scanning each pending migration's
|
|
14
|
+
`upgrade()` for data-destroying calls (`op.drop_table` /
|
|
15
|
+
`op.drop_column` / `op.drop_constraint` and `batch_op` variants). When
|
|
16
|
+
any are found it raises **`DestructiveMigrationError`** (carrying the
|
|
17
|
+
offending `(revision, operation)` pairs) and leaves the database
|
|
18
|
+
untouched; `force=True` logs and proceeds. Source-based scanning is
|
|
19
|
+
dialect-agnostic (no false positives on SQLite batch rebuilds) and
|
|
20
|
+
ignores drops in `downgrade()`. `pending_destructive_ops()` exposes the
|
|
21
|
+
scan without running anything (CI-friendly).
|
|
22
|
+
- **`GracefulShutdownMiddleware`** — tracks in-flight requests and, once
|
|
23
|
+
draining, replies `503` + `Retry-After` to new requests so a load
|
|
24
|
+
balancer deregisters the instance. `begin_drain()` / `wait_drained()`
|
|
25
|
+
(bounded by `drain_timeout`) are driven from the lifespan shutdown
|
|
26
|
+
(uvicorn owns `SIGTERM`); an opt-in `install_signal_handlers()` chains
|
|
27
|
+
the previous handler for servers that manage signals themselves. Wired
|
|
28
|
+
via `app.add_middleware(BaseHTTPMiddleware, dispatch=shutdown.dispatch)`.
|
|
29
|
+
|
|
30
|
+
### Docs
|
|
31
|
+
|
|
32
|
+
- New bilingual recipe **Deploy seguro / Safe deploys** covering
|
|
33
|
+
`safe_upgrade` and `GracefulShutdownMiddleware`, added to the nav, the
|
|
34
|
+
recipes index, and the API reference.
|
|
35
|
+
|
|
36
|
+
## [0.45.0] — 2026-06-11
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`TenantScopedRepository[ModelType]`** — a `BaseRepository` locked
|
|
41
|
+
to a single tenant for shared-schema multi-tenancy. Bind a
|
|
42
|
+
`tenant_id` (and optional `tenant_field`, default `"tenant_id"`) at
|
|
43
|
+
construction; it injects `WHERE tenant_id = ?` into **every** read
|
|
44
|
+
(`get`/`get_or_none`/`get_by_id`/`exists`/`first`/`list`/`count`/
|
|
45
|
+
`paginate`/`cursor_paginate`/`delete_many`) and stamps the tenant id
|
|
46
|
+
onto **every** write (`add`/`add_all`). `delete` / `delete_batch` add
|
|
47
|
+
the tenant predicate to the `DELETE` so a guessed id from another
|
|
48
|
+
tenant matches nothing — cross-tenant access (even probing existence
|
|
49
|
+
by id) is impossible through the repository. The constructor raises
|
|
50
|
+
`AttributeError` at boot if the model lacks the tenant column.
|
|
51
|
+
`tenant_column` property exposes the mapped column for custom queries.
|
|
52
|
+
|
|
53
|
+
### Docs
|
|
54
|
+
|
|
55
|
+
- New bilingual recipe **Multi-tenant**, added to the nav (under
|
|
56
|
+
Database), the recipes index, and the API reference.
|
|
57
|
+
|
|
8
58
|
## [0.44.0] — 2026-06-11
|
|
9
59
|
|
|
10
60
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempest-fastapi-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.0
|
|
4
4
|
Summary: Shared FastAPI building blocks: base schemas, ORM model, async repository, exceptions, pagination and settings — the conventions used across Tempest projects.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempest-fastapi-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/mauriciobenjamin700/tempest-fastapi-sdk
|
|
@@ -238,10 +238,10 @@ Since `0.7.1` every optional dependency is imported lazily at first instantiatio
|
|
|
238
238
|
| Module | Exports |
|
|
239
239
|
| --- | --- |
|
|
240
240
|
| `tempest_fastapi_sdk.schemas` | `BaseSchema`, `BaseResponseSchema`, `BasePaginationFilterSchema`, `BasePaginationSchema[T]`, `CursorPaginationFilterSchema`, `CursorPaginationSchema`, `LogEntrySchema`, `encode_cursor`, `decode_cursor`, `build_pagination_link_header` |
|
|
241
|
-
| `tempest_fastapi_sdk.db` | `BaseModel`, `BaseUserModel`, `BaseUserTokenModel`, `BaseUserRecoveryCodeModel`, `make_user_recovery_code_model`, `UserTokenPurpose`, `BaseRepository[ModelType]`, `AsyncDatabaseManager`, `AlembicHelper`, `NAMING_CONVENTION`, `AuditMixin`, `SoftDeleteMixin`, `MFAMixin`, `BASE_COLUMN_ORDER`, `reorder_base_columns_first`, `compose_hooks`, `SlowQueryLogger`, `BaseOutboxModel`, `OutboxRelay`, `OutboxStatus`, `BaseRepository.save_with_outbox` |
|
|
241
|
+
| `tempest_fastapi_sdk.db` | `BaseModel`, `BaseUserModel`, `BaseUserTokenModel`, `BaseUserRecoveryCodeModel`, `make_user_recovery_code_model`, `UserTokenPurpose`, `BaseRepository[ModelType]`, `TenantScopedRepository[ModelType]`, `AsyncDatabaseManager`, `AlembicHelper` (+ `safe_upgrade`), `DestructiveMigrationError`, `NAMING_CONVENTION`, `AuditMixin`, `SoftDeleteMixin`, `MFAMixin`, `BASE_COLUMN_ORDER`, `reorder_base_columns_first`, `compose_hooks`, `SlowQueryLogger`, `BaseOutboxModel`, `OutboxRelay`, `OutboxStatus`, `BaseRepository.save_with_outbox` |
|
|
242
242
|
| `tempest_fastapi_sdk.exceptions` | `AppException`, `NotFoundException`, `ConflictException`, `ValidationException`, `UnauthorizedException`, `ForbiddenException`, `InvalidTokenException`, `ExpiredTokenException`, `FileTooLargeException`, `InvalidFileTypeException`, `TooManyRequestsException` |
|
|
243
243
|
| `tempest_fastapi_sdk.settings` | `BaseAppSettings`, `ServerSettings`, `LogSettings`, `DatabaseSettings`, `RedisSettings`, `RabbitMQSettings`, `JWTSettings`, `CORSSettings`, `EmailSettings`, `UploadSettings`, `TokenSettings`, `WebPushSettings`, `TaskIQSettings`, `MinIOSettings`, `AuthSettings` |
|
|
244
|
-
| `tempest_fastapi_sdk.api` | `register_exception_handlers`, `app_exception_handler`, `apply_cors`, `make_health_router`, `make_logs_router`, `make_prometheus_router`, `make_prometheus_registry`, `PrometheusMiddleware`, `LogSource`, `make_tool_spec_router`, `make_token_dependency`, `make_bearer_token_dependency`, `make_jwt_user_dependency`, `make_role_dependency`, `make_permission_dependency`, `require_x_token`, `run_server`, `RequestIDMiddleware`, `RateLimitMiddleware`, `IdempotencyMiddleware`, `MemoryIdempotencyStore`, `RedisIdempotencyStore`, `BodySizeLimitMiddleware`, `CSRFMiddleware`, `make_csrf_token_dependency`, `WebhookSignatureVerifier`, `RSAWebhookSignatureVerifier`, OAuth2 (`GoogleOAuthClient`, `GitHubOAuthClient`, `OIDCProvider`), `HardenedStaticFiles`, `DEFAULT_STATIC_SECURITY_HEADERS`, `set_cookie`, `clear_cookie`, `SameSite`, `HealthCheck`, `setup_tracing` *(extra: `[otel]`)* |
|
|
244
|
+
| `tempest_fastapi_sdk.api` | `register_exception_handlers`, `app_exception_handler`, `apply_cors`, `make_health_router`, `make_logs_router`, `make_prometheus_router`, `make_prometheus_registry`, `PrometheusMiddleware`, `LogSource`, `make_tool_spec_router`, `make_token_dependency`, `make_bearer_token_dependency`, `make_jwt_user_dependency`, `make_role_dependency`, `make_permission_dependency`, `require_x_token`, `run_server`, `RequestIDMiddleware`, `RateLimitMiddleware`, `IdempotencyMiddleware`, `MemoryIdempotencyStore`, `RedisIdempotencyStore`, `BodySizeLimitMiddleware`, `CSRFMiddleware`, `make_csrf_token_dependency`, `GracefulShutdownMiddleware`, `WebhookSignatureVerifier`, `RSAWebhookSignatureVerifier`, OAuth2 (`GoogleOAuthClient`, `GitHubOAuthClient`, `OIDCProvider`), `HardenedStaticFiles`, `DEFAULT_STATIC_SECURITY_HEADERS`, `set_cookie`, `clear_cookie`, `SameSite`, `HealthCheck`, `setup_tracing` *(extra: `[otel]`)* |
|
|
245
245
|
| `tempest_fastapi_sdk.auth` *(extra: `[auth]`, opcional `[email]`, `[mfa]`)* | `UserAuthService`, `make_auth_router`, `SignupSchema` / `LoginSchema` / `PasswordResetRequestSchema` / `PasswordResetConfirmSchema` + responses, `ActivationToken`, `PasswordResetToken`, MFA schemas (`MFAEnrollResponseSchema` / `MFAConfirmSchema` / `MFAVerifySchema` / `MFADisableSchema`) — signup/activate/login/reset + TOTP 2FA out of the box |
|
|
246
246
|
| `tempest_fastapi_sdk.controllers` | `BaseController` |
|
|
247
247
|
| `tempest_fastapi_sdk.services` | `BaseService` |
|
|
@@ -136,10 +136,10 @@ Since `0.7.1` every optional dependency is imported lazily at first instantiatio
|
|
|
136
136
|
| Module | Exports |
|
|
137
137
|
| --- | --- |
|
|
138
138
|
| `tempest_fastapi_sdk.schemas` | `BaseSchema`, `BaseResponseSchema`, `BasePaginationFilterSchema`, `BasePaginationSchema[T]`, `CursorPaginationFilterSchema`, `CursorPaginationSchema`, `LogEntrySchema`, `encode_cursor`, `decode_cursor`, `build_pagination_link_header` |
|
|
139
|
-
| `tempest_fastapi_sdk.db` | `BaseModel`, `BaseUserModel`, `BaseUserTokenModel`, `BaseUserRecoveryCodeModel`, `make_user_recovery_code_model`, `UserTokenPurpose`, `BaseRepository[ModelType]`, `AsyncDatabaseManager`, `AlembicHelper`, `NAMING_CONVENTION`, `AuditMixin`, `SoftDeleteMixin`, `MFAMixin`, `BASE_COLUMN_ORDER`, `reorder_base_columns_first`, `compose_hooks`, `SlowQueryLogger`, `BaseOutboxModel`, `OutboxRelay`, `OutboxStatus`, `BaseRepository.save_with_outbox` |
|
|
139
|
+
| `tempest_fastapi_sdk.db` | `BaseModel`, `BaseUserModel`, `BaseUserTokenModel`, `BaseUserRecoveryCodeModel`, `make_user_recovery_code_model`, `UserTokenPurpose`, `BaseRepository[ModelType]`, `TenantScopedRepository[ModelType]`, `AsyncDatabaseManager`, `AlembicHelper` (+ `safe_upgrade`), `DestructiveMigrationError`, `NAMING_CONVENTION`, `AuditMixin`, `SoftDeleteMixin`, `MFAMixin`, `BASE_COLUMN_ORDER`, `reorder_base_columns_first`, `compose_hooks`, `SlowQueryLogger`, `BaseOutboxModel`, `OutboxRelay`, `OutboxStatus`, `BaseRepository.save_with_outbox` |
|
|
140
140
|
| `tempest_fastapi_sdk.exceptions` | `AppException`, `NotFoundException`, `ConflictException`, `ValidationException`, `UnauthorizedException`, `ForbiddenException`, `InvalidTokenException`, `ExpiredTokenException`, `FileTooLargeException`, `InvalidFileTypeException`, `TooManyRequestsException` |
|
|
141
141
|
| `tempest_fastapi_sdk.settings` | `BaseAppSettings`, `ServerSettings`, `LogSettings`, `DatabaseSettings`, `RedisSettings`, `RabbitMQSettings`, `JWTSettings`, `CORSSettings`, `EmailSettings`, `UploadSettings`, `TokenSettings`, `WebPushSettings`, `TaskIQSettings`, `MinIOSettings`, `AuthSettings` |
|
|
142
|
-
| `tempest_fastapi_sdk.api` | `register_exception_handlers`, `app_exception_handler`, `apply_cors`, `make_health_router`, `make_logs_router`, `make_prometheus_router`, `make_prometheus_registry`, `PrometheusMiddleware`, `LogSource`, `make_tool_spec_router`, `make_token_dependency`, `make_bearer_token_dependency`, `make_jwt_user_dependency`, `make_role_dependency`, `make_permission_dependency`, `require_x_token`, `run_server`, `RequestIDMiddleware`, `RateLimitMiddleware`, `IdempotencyMiddleware`, `MemoryIdempotencyStore`, `RedisIdempotencyStore`, `BodySizeLimitMiddleware`, `CSRFMiddleware`, `make_csrf_token_dependency`, `WebhookSignatureVerifier`, `RSAWebhookSignatureVerifier`, OAuth2 (`GoogleOAuthClient`, `GitHubOAuthClient`, `OIDCProvider`), `HardenedStaticFiles`, `DEFAULT_STATIC_SECURITY_HEADERS`, `set_cookie`, `clear_cookie`, `SameSite`, `HealthCheck`, `setup_tracing` *(extra: `[otel]`)* |
|
|
142
|
+
| `tempest_fastapi_sdk.api` | `register_exception_handlers`, `app_exception_handler`, `apply_cors`, `make_health_router`, `make_logs_router`, `make_prometheus_router`, `make_prometheus_registry`, `PrometheusMiddleware`, `LogSource`, `make_tool_spec_router`, `make_token_dependency`, `make_bearer_token_dependency`, `make_jwt_user_dependency`, `make_role_dependency`, `make_permission_dependency`, `require_x_token`, `run_server`, `RequestIDMiddleware`, `RateLimitMiddleware`, `IdempotencyMiddleware`, `MemoryIdempotencyStore`, `RedisIdempotencyStore`, `BodySizeLimitMiddleware`, `CSRFMiddleware`, `make_csrf_token_dependency`, `GracefulShutdownMiddleware`, `WebhookSignatureVerifier`, `RSAWebhookSignatureVerifier`, OAuth2 (`GoogleOAuthClient`, `GitHubOAuthClient`, `OIDCProvider`), `HardenedStaticFiles`, `DEFAULT_STATIC_SECURITY_HEADERS`, `set_cookie`, `clear_cookie`, `SameSite`, `HealthCheck`, `setup_tracing` *(extra: `[otel]`)* |
|
|
143
143
|
| `tempest_fastapi_sdk.auth` *(extra: `[auth]`, opcional `[email]`, `[mfa]`)* | `UserAuthService`, `make_auth_router`, `SignupSchema` / `LoginSchema` / `PasswordResetRequestSchema` / `PasswordResetConfirmSchema` + responses, `ActivationToken`, `PasswordResetToken`, MFA schemas (`MFAEnrollResponseSchema` / `MFAConfirmSchema` / `MFAVerifySchema` / `MFADisableSchema`) — signup/activate/login/reset + TOTP 2FA out of the box |
|
|
144
144
|
| `tempest_fastapi_sdk.controllers` | `BaseController` |
|
|
145
145
|
| `tempest_fastapi_sdk.services` | `BaseService` |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Safe deploys (migrations + graceful shutdown)
|
|
2
|
+
|
|
3
|
+
Two classic deploy risks: a migration that **deletes data** by accident,
|
|
4
|
+
and a rollout that **severs in-flight requests** when the old pod dies.
|
|
5
|
+
This recipe covers the two defenses the SDK ships.
|
|
6
|
+
|
|
7
|
+
## Safe migrations: `safe_upgrade`
|
|
8
|
+
|
|
9
|
+
`AlembicHelper.safe_upgrade()` runs the upgrade **only if** no pending
|
|
10
|
+
migration is destructive. It scans each pending revision's `def upgrade()`
|
|
11
|
+
for data-deleting calls — `op.drop_table`, `op.drop_column`,
|
|
12
|
+
`op.drop_constraint` (and `batch_op` variants) — and, if it finds one,
|
|
13
|
+
raises `DestructiveMigrationError` **without touching the database**.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from tempest_fastapi_sdk import AlembicHelper, DestructiveMigrationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def deploy_migrations() -> None:
|
|
20
|
+
"""Apply migrations on deploy, blocking accidental DROPs."""
|
|
21
|
+
helper: AlembicHelper = AlembicHelper(db_url="postgresql+asyncpg://...")
|
|
22
|
+
try:
|
|
23
|
+
helper.safe_upgrade("head")
|
|
24
|
+
except DestructiveMigrationError as exc:
|
|
25
|
+
# CI/CD fails here — someone must review and unblock with force.
|
|
26
|
+
for revision, op in exc.offences:
|
|
27
|
+
print(f"blocked: {revision} → {op}")
|
|
28
|
+
raise
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The scan looks at the migration **code**, not the generated SQL — so it
|
|
32
|
+
never false-positives on the table rebuild SQLite does in batch mode. A
|
|
33
|
+
`drop_*` in `downgrade()` (the normal, expected path) is ignored.
|
|
34
|
+
|
|
35
|
+
### Allowing an intentional DROP
|
|
36
|
+
|
|
37
|
+
When the DROP is intentional (you took a backup, you reviewed it), pass
|
|
38
|
+
`force=True` — the destructive operations are logged and the upgrade runs:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from tempest_fastapi_sdk import AlembicHelper
|
|
42
|
+
|
|
43
|
+
helper: AlembicHelper = AlembicHelper(db_url="postgresql+asyncpg://...")
|
|
44
|
+
helper.safe_upgrade("head", force=True) # I know what I'm doing
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
!!! tip "Inspect only"
|
|
48
|
+
`helper.pending_destructive_ops("head")` returns the list of
|
|
49
|
+
`(revision, operation)` without running anything — handy for a CI step
|
|
50
|
+
that only reports.
|
|
51
|
+
|
|
52
|
+
!!! danger "force=True deletes data"
|
|
53
|
+
`DROP COLUMN` / `DROP TABLE` are irreversible. Only use `force=True`
|
|
54
|
+
after a backup and human review.
|
|
55
|
+
|
|
56
|
+
## Graceful shutdown: drain in-flight requests
|
|
57
|
+
|
|
58
|
+
On a rollout the orchestrator sends `SIGTERM` and, after a grace period,
|
|
59
|
+
`SIGKILL`. If a request is still running when the worker dies, it's
|
|
60
|
+
severed — an intermittent 502. `GracefulShutdownMiddleware`:
|
|
61
|
+
|
|
62
|
+
1. Once **draining**, replies `503` + `Retry-After` to new requests, so
|
|
63
|
+
the load balancer stops routing to this pod.
|
|
64
|
+
2. **Counts** in-flight requests; `wait_drained()` waits for them to
|
|
65
|
+
finish (with a timeout) before the process exits.
|
|
66
|
+
|
|
67
|
+
You hold the instance and drive draining from the `lifespan` (uvicorn runs
|
|
68
|
+
the lifespan shutdown on `SIGTERM` — and it owns the signal handling):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from contextlib import asynccontextmanager
|
|
72
|
+
from collections.abc import AsyncIterator
|
|
73
|
+
|
|
74
|
+
from fastapi import FastAPI
|
|
75
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
76
|
+
|
|
77
|
+
from tempest_fastapi_sdk import GracefulShutdownMiddleware
|
|
78
|
+
|
|
79
|
+
shutdown: GracefulShutdownMiddleware = GracefulShutdownMiddleware(drain_timeout=25.0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@asynccontextmanager
|
|
83
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
84
|
+
"""Drain in-flight requests on shutdown."""
|
|
85
|
+
yield
|
|
86
|
+
shutdown.begin_drain()
|
|
87
|
+
await shutdown.wait_drained()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
app: FastAPI = FastAPI(lifespan=lifespan)
|
|
91
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=shutdown.dispatch)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Set the orchestrator's grace period a little **above** `drain_timeout`,
|
|
95
|
+
and uvicorn's `--timeout-graceful-shutdown` to match.
|
|
96
|
+
|
|
97
|
+
!!! warning "The signal belongs to your server"
|
|
98
|
+
uvicorn already installs `SIGTERM` handlers and triggers the lifespan
|
|
99
|
+
shutdown — drive draining from there. The opt-in
|
|
100
|
+
`install_signal_handlers()` is only for servers that do **not** manage
|
|
101
|
+
signals themselves; it chains the previous handler and is a no-op off
|
|
102
|
+
the main thread.
|
|
103
|
+
|
|
104
|
+
## Recap
|
|
105
|
+
|
|
106
|
+
- `AlembicHelper.safe_upgrade()` refuses destructive migrations
|
|
107
|
+
(`DestructiveMigrationError`); `force=True` allows them;
|
|
108
|
+
`pending_destructive_ops()` only inspects.
|
|
109
|
+
- `GracefulShutdownMiddleware` replies `503` while draining and
|
|
110
|
+
`wait_drained()` waits for in-flight requests — driven from the
|
|
111
|
+
`lifespan`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Deploy seguro (migrações + graceful shutdown)
|
|
2
|
+
|
|
3
|
+
Dois riscos clássicos de deploy: uma migration que **apaga dados** sem
|
|
4
|
+
querer, e um rollout que **corta requests no meio** quando o pod velho
|
|
5
|
+
morre. Esta receita cobre as duas defesas que o SDK traz.
|
|
6
|
+
|
|
7
|
+
## Migrações seguras: `safe_upgrade`
|
|
8
|
+
|
|
9
|
+
`AlembicHelper.safe_upgrade()` roda o upgrade **só se** nenhuma migration
|
|
10
|
+
pendente for destrutiva. Ele varre o `def upgrade()` de cada revision
|
|
11
|
+
pendente atrás de chamadas que apagam dados — `op.drop_table`,
|
|
12
|
+
`op.drop_column`, `op.drop_constraint` (e variantes `batch_op`) — e, se
|
|
13
|
+
achar alguma, levanta `DestructiveMigrationError` **sem tocar no banco**.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from tempest_fastapi_sdk import AlembicHelper, DestructiveMigrationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def deploy_migrations() -> None:
|
|
20
|
+
"""Aplica migrations no deploy, barrando DROPs acidentais."""
|
|
21
|
+
helper: AlembicHelper = AlembicHelper(db_url="postgresql+asyncpg://...")
|
|
22
|
+
try:
|
|
23
|
+
helper.safe_upgrade("head")
|
|
24
|
+
except DestructiveMigrationError as exc:
|
|
25
|
+
# CI/CD falha aqui — alguém precisa revisar e liberar com force.
|
|
26
|
+
for revision, op in exc.offences:
|
|
27
|
+
print(f"bloqueado: {revision} → {op}")
|
|
28
|
+
raise
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A varredura olha o **código** da migration, não o SQL gerado — então não
|
|
32
|
+
dá falso-positivo no rebuild de tabela que o SQLite faz em batch mode. Um
|
|
33
|
+
`drop_*` no `downgrade()` (o caminho normal e esperado) é ignorado.
|
|
34
|
+
|
|
35
|
+
### Liberando um DROP intencional
|
|
36
|
+
|
|
37
|
+
Quando o DROP é proposital (você já fez backup, já validou), passe
|
|
38
|
+
`force=True` — as operações destrutivas são logadas e o upgrade roda:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from tempest_fastapi_sdk import AlembicHelper
|
|
42
|
+
|
|
43
|
+
helper: AlembicHelper = AlembicHelper(db_url="postgresql+asyncpg://...")
|
|
44
|
+
helper.safe_upgrade("head", force=True) # eu sei o que estou fazendo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
!!! tip "Só inspecionar"
|
|
48
|
+
`helper.pending_destructive_ops("head")` devolve a lista de
|
|
49
|
+
`(revision, operação)` sem rodar nada — útil pra um passo de CI que só
|
|
50
|
+
reporta.
|
|
51
|
+
|
|
52
|
+
!!! danger "force=True apaga dados"
|
|
53
|
+
`DROP COLUMN` / `DROP TABLE` são irreversíveis. Só use `force=True`
|
|
54
|
+
depois de backup e revisão humana.
|
|
55
|
+
|
|
56
|
+
## Graceful shutdown: drenar requests em voo
|
|
57
|
+
|
|
58
|
+
No rollout, o orquestrador manda `SIGTERM` e, depois de um tempo,
|
|
59
|
+
`SIGKILL`. Se uma request ainda estiver rodando quando o worker morre, ela
|
|
60
|
+
é cortada — vira um 502 intermitente. `GracefulShutdownMiddleware`:
|
|
61
|
+
|
|
62
|
+
1. Ao entrar em **drenagem**, responde `503` + `Retry-After` pra requests
|
|
63
|
+
novas, então o load balancer para de rotear pra esse pod.
|
|
64
|
+
2. **Conta** as requests em voo; `wait_drained()` espera elas terminarem
|
|
65
|
+
(com timeout) antes do processo sair.
|
|
66
|
+
|
|
67
|
+
Você segura a instância e dirige a drenagem pelo `lifespan` (o uvicorn
|
|
68
|
+
roda o shutdown do lifespan no `SIGTERM` — e é ele quem cuida do sinal):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from contextlib import asynccontextmanager
|
|
72
|
+
from collections.abc import AsyncIterator
|
|
73
|
+
|
|
74
|
+
from fastapi import FastAPI
|
|
75
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
76
|
+
|
|
77
|
+
from tempest_fastapi_sdk import GracefulShutdownMiddleware
|
|
78
|
+
|
|
79
|
+
shutdown: GracefulShutdownMiddleware = GracefulShutdownMiddleware(drain_timeout=25.0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@asynccontextmanager
|
|
83
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
84
|
+
"""Drena as requests em voo no shutdown."""
|
|
85
|
+
yield
|
|
86
|
+
shutdown.begin_drain()
|
|
87
|
+
await shutdown.wait_drained()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
app: FastAPI = FastAPI(lifespan=lifespan)
|
|
91
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=shutdown.dispatch)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Configure o grace period do orquestrador um pouco **acima** do
|
|
95
|
+
`drain_timeout`, e o `--timeout-graceful-shutdown` do uvicorn pra casar.
|
|
96
|
+
|
|
97
|
+
!!! warning "O sinal é do seu servidor"
|
|
98
|
+
O uvicorn já instala handlers de `SIGTERM` e dispara o shutdown do
|
|
99
|
+
lifespan — dirija a drenagem por lá. O método opt-in
|
|
100
|
+
`install_signal_handlers()` só serve pra servidores que **não**
|
|
101
|
+
gerenciam sinais sozinhos; ele encadeia o handler anterior e é no-op
|
|
102
|
+
fora da thread principal.
|
|
103
|
+
|
|
104
|
+
## Recap
|
|
105
|
+
|
|
106
|
+
- `AlembicHelper.safe_upgrade()` recusa migrations destrutivas
|
|
107
|
+
(`DestructiveMigrationError`); `force=True` libera; `pending_destructive_ops()`
|
|
108
|
+
só inspeciona.
|
|
109
|
+
- `GracefulShutdownMiddleware` responde `503` durante a drenagem e
|
|
110
|
+
`wait_drained()` espera as requests em voo — dirigido pelo `lifespan`.
|
|
@@ -10,6 +10,7 @@ Bite-sized "I want to wire X" walkthroughs. Each page starts with **what problem
|
|
|
10
10
|
| Theme | Covers |
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| **[Database »](database.md)** | `BaseRepository`, `BaseModel`, `AuditMixin` / `SoftDeleteMixin`, cursor pagination, Alembic helper |
|
|
13
|
+
| **[Multi-tenant »](multi-tenant.md)** | `TenantScopedRepository` — `tenant_id` isolation on every query |
|
|
13
14
|
| **[HTTP layer »](http.md)** | `apply_cors`, `RequestIDMiddleware`, `RateLimitMiddleware`, `make_health_router`, JWT / role / permission dependencies, webhook signature verifier, pagination Link headers, tool-spec router |
|
|
14
15
|
| **[HTTP client (outbound) »](http-client.md)** | `HTTPClient` — typed httpx with retry/backoff, circuit-breaker, X-Request-ID; `RetryPolicy`, `CircuitOpenError` |
|
|
15
16
|
| **[Cache »](cache.md)** | `AsyncRedisManager`, `@cached` decorator |
|
|
@@ -25,6 +26,7 @@ Bite-sized "I want to wire X" walkthroughs. Each page starts with **what problem
|
|
|
25
26
|
| **[Downloads »](downloads.md)** | `DownloadUtils` — `file_response`, `stream`, `build_content_disposition`, path-traversal safe |
|
|
26
27
|
| **[Utilities »](utilities.md)** | `utcnow`/`to_utc`, `modify_dict`, `get_client_ip`, opaque tokens (`generate_opaque_token`) |
|
|
27
28
|
| **[Testing »](testing.md)** | `test_session`, `test_database`, in-memory SQLite, pytest fixtures |
|
|
29
|
+
| **[Safe deploys »](deploy-safety.md)** | `AlembicHelper.safe_upgrade` (blocks DROPs), `GracefulShutdownMiddleware` |
|
|
28
30
|
| **[CLI »](cli.md)** | `tempest new` / `lint` / `fix` / `format` / `type` / `test` / `check` |
|
|
29
31
|
| **[Security »](security.md)** | `AttemptThrottle`, opaque-token helpers, `HardenedStaticFiles`, security headers |
|
|
30
32
|
| **[Brazilian helpers »](br-helpers.md)** | CPF / CNPJ / CEP / phone validation + normalization |
|
|
@@ -10,6 +10,7 @@ Passo a passo curtos no estilo "quero conectar X". Cada página começa com **qu
|
|
|
10
10
|
| Tema | Cobre |
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| **[Banco de dados »](database.md)** | `BaseRepository`, `BaseModel`, `AuditMixin` / `SoftDeleteMixin`, paginação por cursor, helper de Alembic |
|
|
13
|
+
| **[Multi-tenant »](multi-tenant.md)** | `TenantScopedRepository` — isolamento por `tenant_id` em toda query |
|
|
13
14
|
| **[Camada HTTP »](http.md)** | `apply_cors`, `RequestIDMiddleware`, `RateLimitMiddleware`, `make_health_router`, dependências de JWT / role / permissão, verificador de assinatura de webhook, headers Link de paginação, router de tool-spec |
|
|
14
15
|
| **[HTTP client (saída) »](http-client.md)** | `HTTPClient` — httpx tipado com retry/backoff, circuit-breaker, X-Request-ID; `RetryPolicy`, `CircuitOpenError` |
|
|
15
16
|
| **[Cache »](cache.md)** | `AsyncRedisManager`, decorator `@cached` |
|
|
@@ -25,6 +26,7 @@ Passo a passo curtos no estilo "quero conectar X". Cada página começa com **qu
|
|
|
25
26
|
| **[Downloads »](downloads.md)** | `DownloadUtils` — `file_response`, `stream`, `build_content_disposition`, anti path-traversal |
|
|
26
27
|
| **[Utilitários »](utilities.md)** | `utcnow`/`to_utc`, `modify_dict`, `get_client_ip`, tokens opacos (`generate_opaque_token`) |
|
|
27
28
|
| **[Testes »](testing.md)** | `test_session`, `test_database`, SQLite em memória, fixtures pytest |
|
|
29
|
+
| **[Deploy seguro »](deploy-safety.md)** | `AlembicHelper.safe_upgrade` (barra DROPs), `GracefulShutdownMiddleware` |
|
|
28
30
|
| **[CLI »](cli.md)** | `tempest new` / `lint` / `fix` / `format` / `type` / `test` / `check` |
|
|
29
31
|
| **[Segurança »](security.md)** | `AttemptThrottle`, helpers de token opaco, `HardenedStaticFiles`, headers de segurança |
|
|
30
32
|
| **[Helpers brasileiros »](br-helpers.md)** | validação + normalização de CPF / CNPJ / CEP / telefone |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Multi-tenant (TenantScopedRepository)
|
|
2
|
+
|
|
3
|
+
In a **shared-schema** multi-tenant database, every tenant's rows live in
|
|
4
|
+
the same table, told apart by a `tenant_id` column. The danger is obvious:
|
|
5
|
+
forget **one** `WHERE tenant_id = ?` and tenant A reads (or deletes)
|
|
6
|
+
tenant B's data. `TenantScopedRepository` takes that risk off the table —
|
|
7
|
+
you bind the `tenant_id` at construction and it injects the filter into
|
|
8
|
+
**every** read and stamps it onto **every** write. Call sites can't get it
|
|
9
|
+
wrong.
|
|
10
|
+
|
|
11
|
+
!!! info "Where this fits"
|
|
12
|
+
It's a `BaseRepository` that behaves identically — same API
|
|
13
|
+
([Database](database.md)). The difference is invisible to the caller:
|
|
14
|
+
tenant scoping is automatic.
|
|
15
|
+
|
|
16
|
+
## 1. The model needs a tenant column
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
|
|
21
|
+
from sqlalchemy import String, Uuid
|
|
22
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
23
|
+
|
|
24
|
+
from tempest_fastapi_sdk import BaseModel
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OrderModel(BaseModel):
|
|
28
|
+
"""Order — isolated per tenant."""
|
|
29
|
+
|
|
30
|
+
__tablename__ = "order"
|
|
31
|
+
|
|
32
|
+
tenant_id: Mapped[UUID] = mapped_column(Uuid(), nullable=False, index=True)
|
|
33
|
+
total: Mapped[int] = mapped_column(nullable=False)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Build the repository bound to the tenant
|
|
37
|
+
|
|
38
|
+
The `tenant_id` usually comes from the request's JWT / session / header:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from uuid import UUID
|
|
42
|
+
|
|
43
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
44
|
+
|
|
45
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
46
|
+
|
|
47
|
+
from src.db.models import OrderModel
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_order_repo(
|
|
51
|
+
session: AsyncSession, tenant_id: UUID
|
|
52
|
+
) -> TenantScopedRepository[OrderModel]:
|
|
53
|
+
"""Order repository locked to the request's tenant."""
|
|
54
|
+
return TenantScopedRepository(session, model=OrderModel, tenant_id=tenant_id)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the model has no `tenant_id` column, the constructor raises
|
|
58
|
+
`AttributeError` immediately — you catch the mistake at boot, not in
|
|
59
|
+
production. Different column name? Pass `tenant_field="org_id"`.
|
|
60
|
+
|
|
61
|
+
## 3. Use it like any repository
|
|
62
|
+
|
|
63
|
+
Every read comes filtered; every write comes stamped:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from uuid import UUID
|
|
67
|
+
|
|
68
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
69
|
+
|
|
70
|
+
from src.db.models import OrderModel
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def list_orders(repo: TenantScopedRepository[OrderModel]) -> list[OrderModel]:
|
|
74
|
+
"""Only THIS tenant's orders — no manual WHERE."""
|
|
75
|
+
return await repo.list() # WHERE tenant_id = <bound tenant>
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def create_order(
|
|
79
|
+
repo: TenantScopedRepository[OrderModel], total: int
|
|
80
|
+
) -> OrderModel:
|
|
81
|
+
"""tenant_id is stamped automatically on insert."""
|
|
82
|
+
return await repo.add(OrderModel(total=total))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Cross-tenant access is impossible, even by id
|
|
86
|
+
|
|
87
|
+
`get_by_id`, `delete` and `delete_batch` are scoped too. An id from another
|
|
88
|
+
tenant simply **doesn't match** — indistinguishable from a row that never
|
|
89
|
+
existed:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from uuid import UUID
|
|
93
|
+
|
|
94
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
95
|
+
|
|
96
|
+
from src.db.models import OrderModel
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def fetch(repo: TenantScopedRepository[OrderModel], order_id: UUID) -> OrderModel:
|
|
100
|
+
"""Raises NotFound if the order belongs to ANOTHER tenant — no existence leak."""
|
|
101
|
+
return await repo.get_by_id(order_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`delete_many({})` deletes only **this** tenant's rows, never the whole
|
|
105
|
+
table — the tenant predicate is always added.
|
|
106
|
+
|
|
107
|
+
!!! warning "Raw queries are your responsibility"
|
|
108
|
+
A subclass that builds its own `select(...)` without going through
|
|
109
|
+
`_apply_filters` — or a pre-built `query=` passed to `paginate` — is
|
|
110
|
+
**not** auto-scoped. In those cases, add
|
|
111
|
+
`.where(self.tenant_column == self.tenant_id)` yourself.
|
|
112
|
+
|
|
113
|
+
## Recap
|
|
114
|
+
|
|
115
|
+
- The model declares `tenant_id` (or another column, via `tenant_field=`).
|
|
116
|
+
- `TenantScopedRepository(session, model=..., tenant_id=...)` injects the
|
|
117
|
+
filter into every read and stamps every write.
|
|
118
|
+
- `get_by_id` / `delete` / `delete_batch` / `delete_many` are scoped —
|
|
119
|
+
cross-tenant access is impossible through the repository methods.
|
|
120
|
+
- The constructor validates the tenant column exists at boot.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Multi-tenant (TenantScopedRepository)
|
|
2
|
+
|
|
3
|
+
Num banco multi-tenant de **schema compartilhado**, as linhas de todos os
|
|
4
|
+
tenants moram na mesma tabela, separadas por uma coluna `tenant_id`. O
|
|
5
|
+
perigo é claro: esquecer **um** `WHERE tenant_id = ?` e o tenant A lê (ou
|
|
6
|
+
apaga) os dados do tenant B. `TenantScopedRepository` tira esse risco da
|
|
7
|
+
mesa — você amarra o `tenant_id` na construção e ele injeta o filtro em
|
|
8
|
+
**toda** leitura e carimba em **toda** escrita. Os call sites não têm como
|
|
9
|
+
errar.
|
|
10
|
+
|
|
11
|
+
!!! info "Onde isso encaixa"
|
|
12
|
+
É um `BaseRepository` que se comporta igualzinho — mesma API
|
|
13
|
+
([Banco de dados](database.md)). A diferença é invisível pro chamador:
|
|
14
|
+
o escopo de tenant é automático.
|
|
15
|
+
|
|
16
|
+
## 1. O modelo precisa da coluna de tenant
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
|
|
21
|
+
from sqlalchemy import String, Uuid
|
|
22
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
23
|
+
|
|
24
|
+
from tempest_fastapi_sdk import BaseModel
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OrderModel(BaseModel):
|
|
28
|
+
"""Pedido — isolado por tenant."""
|
|
29
|
+
|
|
30
|
+
__tablename__ = "order"
|
|
31
|
+
|
|
32
|
+
tenant_id: Mapped[UUID] = mapped_column(Uuid(), nullable=False, index=True)
|
|
33
|
+
total: Mapped[int] = mapped_column(nullable=False)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. Construa o repositório amarrado ao tenant
|
|
37
|
+
|
|
38
|
+
O `tenant_id` normalmente vem do JWT / sessão / header do request:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from uuid import UUID
|
|
42
|
+
|
|
43
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
44
|
+
|
|
45
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
46
|
+
|
|
47
|
+
from src.db.models import OrderModel
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_order_repo(
|
|
51
|
+
session: AsyncSession, tenant_id: UUID
|
|
52
|
+
) -> TenantScopedRepository[OrderModel]:
|
|
53
|
+
"""Repositório de pedidos travado no tenant do request."""
|
|
54
|
+
return TenantScopedRepository(session, model=OrderModel, tenant_id=tenant_id)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Se o modelo não tiver a coluna `tenant_id`, o construtor levanta
|
|
58
|
+
`AttributeError` na hora — você descobre o erro no boot, não em produção.
|
|
59
|
+
Coluna com outro nome? Passe `tenant_field="org_id"`.
|
|
60
|
+
|
|
61
|
+
## 3. Use como qualquer repositório
|
|
62
|
+
|
|
63
|
+
Toda leitura já vem filtrada; toda escrita já vem carimbada:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from uuid import UUID
|
|
67
|
+
|
|
68
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
69
|
+
|
|
70
|
+
from src.db.models import OrderModel
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def list_orders(repo: TenantScopedRepository[OrderModel]) -> list[OrderModel]:
|
|
74
|
+
"""Só os pedidos DESTE tenant — sem WHERE manual."""
|
|
75
|
+
return await repo.list() # WHERE tenant_id = <tenant amarrado>
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def create_order(
|
|
79
|
+
repo: TenantScopedRepository[OrderModel], total: int
|
|
80
|
+
) -> OrderModel:
|
|
81
|
+
"""tenant_id é carimbado automaticamente no insert."""
|
|
82
|
+
return await repo.add(OrderModel(total=total))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Acesso cruzado é impossível, mesmo por id
|
|
86
|
+
|
|
87
|
+
`get_by_id`, `delete` e `delete_batch` também são escopados. Um id de outro
|
|
88
|
+
tenant simplesmente **não casa** — é indistinguível de uma linha que nunca
|
|
89
|
+
existiu:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from uuid import UUID
|
|
93
|
+
|
|
94
|
+
from tempest_fastapi_sdk import TenantScopedRepository
|
|
95
|
+
|
|
96
|
+
from src.db.models import OrderModel
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def fetch(repo: TenantScopedRepository[OrderModel], order_id: UUID) -> OrderModel:
|
|
100
|
+
"""Levanta NotFound se o pedido for de OUTRO tenant — sem vazar existência."""
|
|
101
|
+
return await repo.get_by_id(order_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`delete_many({})` apaga só as linhas **deste** tenant, nunca a tabela
|
|
105
|
+
inteira — o predicado de tenant é sempre adicionado.
|
|
106
|
+
|
|
107
|
+
!!! warning "Queries cruas são responsabilidade sua"
|
|
108
|
+
Uma subclasse que monta o próprio `select(...)` sem passar por
|
|
109
|
+
`_apply_filters` — ou um `query=` pré-montado passado pro `paginate` —
|
|
110
|
+
**não** é escopado automaticamente. Nesses casos, adicione
|
|
111
|
+
`.where(self.tenant_column == self.tenant_id)` você mesmo.
|
|
112
|
+
|
|
113
|
+
## Recap
|
|
114
|
+
|
|
115
|
+
- O modelo declara `tenant_id` (ou outra coluna, via `tenant_field=`).
|
|
116
|
+
- `TenantScopedRepository(session, model=..., tenant_id=...)` injeta o
|
|
117
|
+
filtro em toda leitura e carimba toda escrita.
|
|
118
|
+
- `get_by_id` / `delete` / `delete_batch` / `delete_many` são escopados —
|
|
119
|
+
acesso cruzado entre tenants é impossível pelos métodos do repositório.
|
|
120
|
+
- Construtor valida a existência da coluna de tenant no boot.
|
|
@@ -31,6 +31,7 @@ Gerada automaticamente a partir das docstrings do SDK via [`mkdocstrings`](https
|
|
|
31
31
|
::: tempest_fastapi_sdk.db.user_recovery_code_model.BaseUserRecoveryCodeModel
|
|
32
32
|
::: tempest_fastapi_sdk.db.user_recovery_code_model.make_user_recovery_code_model
|
|
33
33
|
::: tempest_fastapi_sdk.db.repository.BaseRepository
|
|
34
|
+
::: tempest_fastapi_sdk.db.tenant.TenantScopedRepository
|
|
34
35
|
::: tempest_fastapi_sdk.db.mixins.SoftDeleteMixin
|
|
35
36
|
::: tempest_fastapi_sdk.db.mixins.AuditMixin
|
|
36
37
|
::: tempest_fastapi_sdk.db.mixins.MFAMixin
|
|
@@ -40,6 +41,7 @@ Gerada automaticamente a partir das docstrings do SDK via [`mkdocstrings`](https
|
|
|
40
41
|
::: tempest_fastapi_sdk.db.outbox.BaseOutboxModel
|
|
41
42
|
::: tempest_fastapi_sdk.db.outbox.OutboxRelay
|
|
42
43
|
::: tempest_fastapi_sdk.db.outbox.OutboxStatus
|
|
44
|
+
::: tempest_fastapi_sdk.db.migrations.DestructiveMigrationError
|
|
43
45
|
|
|
44
46
|
---
|
|
45
47
|
|
|
@@ -95,6 +97,7 @@ Gerada automaticamente a partir das docstrings do SDK via [`mkdocstrings`](https
|
|
|
95
97
|
::: tempest_fastapi_sdk.api.middlewares.idempotency.MemoryIdempotencyStore
|
|
96
98
|
::: tempest_fastapi_sdk.api.middlewares.idempotency.RedisIdempotencyStore
|
|
97
99
|
::: tempest_fastapi_sdk.api.middlewares.body_size.BodySizeLimitMiddleware
|
|
100
|
+
::: tempest_fastapi_sdk.api.middlewares.graceful.GracefulShutdownMiddleware
|
|
98
101
|
::: tempest_fastapi_sdk.api.middlewares.csrf.CSRFMiddleware
|
|
99
102
|
::: tempest_fastapi_sdk.api.middlewares.csrf.make_csrf_token_dependency
|
|
100
103
|
::: tempest_fastapi_sdk.api.middlewares.csrf.generate_csrf_token
|
|
@@ -86,6 +86,7 @@ plugins:
|
|
|
86
86
|
Tutorial: Tutorial
|
|
87
87
|
Receitas: Recipes
|
|
88
88
|
Banco de dados: Database
|
|
89
|
+
Multi-tenant: Multi-tenant
|
|
89
90
|
Camada HTTP: HTTP layer
|
|
90
91
|
"HTTP client (saída)": "HTTP client (outbound)"
|
|
91
92
|
Cache: Cache
|
|
@@ -106,6 +107,7 @@ plugins:
|
|
|
106
107
|
Métricas: Metrics
|
|
107
108
|
Painel admin: Admin site
|
|
108
109
|
Testes: Testing
|
|
110
|
+
"Deploy seguro (migrations + shutdown)": "Safe deploys (migrations + shutdown)"
|
|
109
111
|
CLI: CLI
|
|
110
112
|
Segurança: Security
|
|
111
113
|
Utilitários: Utilities
|
|
@@ -200,6 +202,7 @@ nav:
|
|
|
200
202
|
- Receitas:
|
|
201
203
|
- recipes/index.md
|
|
202
204
|
- Banco de dados: recipes/database.md
|
|
205
|
+
- Multi-tenant: recipes/multi-tenant.md
|
|
203
206
|
- Camada HTTP: recipes/http.md
|
|
204
207
|
- "HTTP client (saída)": recipes/http-client.md
|
|
205
208
|
- Cache: recipes/cache.md
|
|
@@ -221,6 +224,7 @@ nav:
|
|
|
221
224
|
- Métricas: recipes/metrics.md
|
|
222
225
|
- Painel admin: recipes/admin.md
|
|
223
226
|
- Testes: recipes/testing.md
|
|
227
|
+
- "Deploy seguro (migrations + shutdown)": recipes/deploy-safety.md
|
|
224
228
|
- CLI: recipes/cli.md
|
|
225
229
|
- Segurança: recipes/security.md
|
|
226
230
|
- Utilitários: recipes/utilities.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tempest-fastapi-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.46.0"
|
|
4
4
|
description = "Shared FastAPI building blocks: base schemas, ORM model, async repository, exceptions, pagination and settings — the conventions used across Tempest projects."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|