tempest-fastapi-sdk 0.45.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.45.0 → tempest_fastapi_sdk-0.46.0}/CHANGELOG.md +28 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/PKG-INFO +3 -3
- {tempest_fastapi_sdk-0.45.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.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.en.md +1 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.md +1 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.md +2 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/mkdocs.yml +2 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/pyproject.toml +1 -1
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/__init__.py +5 -1
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/__init__.py +2 -0
- {tempest_fastapi_sdk-0.45.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.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/__init__.py +4 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/migrations.py +116 -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.45.0 → tempest_fastapi_sdk-0.46.0}/uv.lock +1 -1
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/ci.yml +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/docs.yml +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.gitignore +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.python-version +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/CLAUDE.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/Makefile +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/index.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/index.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/multi-tenant.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/multi-tenant.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.en.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.md +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/scripts/extract_recipe.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/auth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/config.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/forms.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/session.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/site.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/static/admin.css +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/base.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/dashboard.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/detail.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/form.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/list.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/login.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/logs.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/mfa.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/cookies.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/auth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/handlers.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/body_size.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/cors.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/csrf.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/idempotency.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/request_id.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/oauth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/health.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/logs.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/metrics.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/server.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/static.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/tracing.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/webhooks.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/page_renderer.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/schemas.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/service.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_error.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_success.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_error.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_form.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_success.html +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/decorator.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/README.md.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/env.example.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/gitignore.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/main.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/pyproject.toml.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/app.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/auth.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/resources.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/routers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/controllers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/exceptions.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/settings.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/user.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/repositories/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/schemas/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/server.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/services/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/utils/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/test_smoke.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/db.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/generate.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/lint.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/main.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/new.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/src_layers.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/user.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/context.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/enums.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/logging.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/env.py.template +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/connection.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/mixins.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/outbox.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/repository.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/slow_query.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/tenant.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_recovery_code_model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_token_model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/conflict.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/forbidden.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/jwt.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/not_found.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/too_many_requests.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/unauthorized.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/upload.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/validation.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/py.typed +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/link_headers.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/logs.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/pagination.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/response.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/dependencies.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/middleware.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/schemas.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/service.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/store.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/mixins.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/event_stream.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/minio_client.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/scheduler.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/database.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/client_ip.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/datetime.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/dict.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/download.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/email.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/http_client.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/jwt.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/log.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/metrics.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/password.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/regex.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/throttle.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/totp.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/upload.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/schemas.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/hub.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/schemas.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_auth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_forms.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_logs_nav.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_site.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_user_model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_body_size.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cookies.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cors.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_csrf.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_dependencies_auth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_handlers.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_health_router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_idempotency.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_jwt_dependency.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_logs_router.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_oauth.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_prometheus.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_request_id_middleware.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_role_dependency.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_server.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_static.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tracing.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks_rsa.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_service.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_decorator.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_db.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_generate.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_main.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_user.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/conftest.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/test_base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_context.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_enums.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_logging.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_bulk_ops.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_connection.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_migrations.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_model.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_outbox.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_repository.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_slow_query.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_tenant.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/test_exceptions.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_cursor_pagination.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_link_headers.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_pagination.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_response.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/services/test_base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/test_sessions.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_base.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/test_event_stream.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/test_minio_client.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_scheduler.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/test_database.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_client_ip.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_datetime.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_dict.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_download.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_email.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_http_client.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_jwt.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_lazy_extras.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_log.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_metrics.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_password.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_regex.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_throttle.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_upload.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_schemas.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/websockets/test_hub_and_router.py +0 -0
|
@@ -5,6 +5,34 @@ 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
|
+
|
|
8
36
|
## [0.45.0] — 2026-06-11
|
|
9
37
|
|
|
10
38
|
### 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]`, `TenantScopedRepository[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]`, `TenantScopedRepository[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`.
|
|
@@ -26,6 +26,7 @@ Bite-sized "I want to wire X" walkthroughs. Each page starts with **what problem
|
|
|
26
26
|
| **[Downloads »](downloads.md)** | `DownloadUtils` — `file_response`, `stream`, `build_content_disposition`, path-traversal safe |
|
|
27
27
|
| **[Utilities »](utilities.md)** | `utcnow`/`to_utc`, `modify_dict`, `get_client_ip`, opaque tokens (`generate_opaque_token`) |
|
|
28
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` |
|
|
29
30
|
| **[CLI »](cli.md)** | `tempest new` / `lint` / `fix` / `format` / `type` / `test` / `check` |
|
|
30
31
|
| **[Security »](security.md)** | `AttemptThrottle`, opaque-token helpers, `HardenedStaticFiles`, security headers |
|
|
31
32
|
| **[Brazilian helpers »](br-helpers.md)** | CPF / CNPJ / CEP / phone validation + normalization |
|
|
@@ -26,6 +26,7 @@ Passo a passo curtos no estilo "quero conectar X". Cada página começa com **qu
|
|
|
26
26
|
| **[Downloads »](downloads.md)** | `DownloadUtils` — `file_response`, `stream`, `build_content_disposition`, anti path-traversal |
|
|
27
27
|
| **[Utilitários »](utilities.md)** | `utcnow`/`to_utc`, `modify_dict`, `get_client_ip`, tokens opacos (`generate_opaque_token`) |
|
|
28
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` |
|
|
29
30
|
| **[CLI »](cli.md)** | `tempest new` / `lint` / `fix` / `format` / `type` / `test` / `check` |
|
|
30
31
|
| **[Segurança »](security.md)** | `AttemptThrottle`, helpers de token opaco, `HardenedStaticFiles`, headers de segurança |
|
|
31
32
|
| **[Helpers brasileiros »](br-helpers.md)** | validação + normalização de CPF / CNPJ / CEP / telefone |
|
|
@@ -41,6 +41,7 @@ Gerada automaticamente a partir das docstrings do SDK via [`mkdocstrings`](https
|
|
|
41
41
|
::: tempest_fastapi_sdk.db.outbox.BaseOutboxModel
|
|
42
42
|
::: tempest_fastapi_sdk.db.outbox.OutboxRelay
|
|
43
43
|
::: tempest_fastapi_sdk.db.outbox.OutboxStatus
|
|
44
|
+
::: tempest_fastapi_sdk.db.migrations.DestructiveMigrationError
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
@@ -96,6 +97,7 @@ Gerada automaticamente a partir das docstrings do SDK via [`mkdocstrings`](https
|
|
|
96
97
|
::: tempest_fastapi_sdk.api.middlewares.idempotency.MemoryIdempotencyStore
|
|
97
98
|
::: tempest_fastapi_sdk.api.middlewares.idempotency.RedisIdempotencyStore
|
|
98
99
|
::: tempest_fastapi_sdk.api.middlewares.body_size.BodySizeLimitMiddleware
|
|
100
|
+
::: tempest_fastapi_sdk.api.middlewares.graceful.GracefulShutdownMiddleware
|
|
99
101
|
::: tempest_fastapi_sdk.api.middlewares.csrf.CSRFMiddleware
|
|
100
102
|
::: tempest_fastapi_sdk.api.middlewares.csrf.make_csrf_token_dependency
|
|
101
103
|
::: tempest_fastapi_sdk.api.middlewares.csrf.generate_csrf_token
|
|
@@ -107,6 +107,7 @@ plugins:
|
|
|
107
107
|
Métricas: Metrics
|
|
108
108
|
Painel admin: Admin site
|
|
109
109
|
Testes: Testing
|
|
110
|
+
"Deploy seguro (migrations + shutdown)": "Safe deploys (migrations + shutdown)"
|
|
110
111
|
CLI: CLI
|
|
111
112
|
Segurança: Security
|
|
112
113
|
Utilitários: Utilities
|
|
@@ -223,6 +224,7 @@ nav:
|
|
|
223
224
|
- Métricas: recipes/metrics.md
|
|
224
225
|
- Painel admin: recipes/admin.md
|
|
225
226
|
- Testes: recipes/testing.md
|
|
227
|
+
- "Deploy seguro (migrations + shutdown)": recipes/deploy-safety.md
|
|
226
228
|
- CLI: recipes/cli.md
|
|
227
229
|
- Segurança: recipes/security.md
|
|
228
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"
|
|
@@ -21,6 +21,7 @@ from tempest_fastapi_sdk.api import (
|
|
|
21
21
|
CSRFMiddleware,
|
|
22
22
|
GitHubOAuthClient,
|
|
23
23
|
GoogleOAuthClient,
|
|
24
|
+
GracefulShutdownMiddleware,
|
|
24
25
|
HardenedStaticFiles,
|
|
25
26
|
HealthCheck,
|
|
26
27
|
IdempotencyMiddleware,
|
|
@@ -104,6 +105,7 @@ from tempest_fastapi_sdk.db import (
|
|
|
104
105
|
BaseUserModel,
|
|
105
106
|
BaseUserRecoveryCodeModel,
|
|
106
107
|
BaseUserTokenModel,
|
|
108
|
+
DestructiveMigrationError,
|
|
107
109
|
MFAMixin,
|
|
108
110
|
OutboxRelay,
|
|
109
111
|
OutboxStatus,
|
|
@@ -248,7 +250,7 @@ from tempest_fastapi_sdk.websockets import (
|
|
|
248
250
|
make_websocket_router,
|
|
249
251
|
)
|
|
250
252
|
|
|
251
|
-
__version__: str = "0.
|
|
253
|
+
__version__: str = "0.46.0"
|
|
252
254
|
|
|
253
255
|
__all__: list[str] = [
|
|
254
256
|
"BASE_COLUMN_ORDER",
|
|
@@ -306,6 +308,7 @@ __all__: list[str] = [
|
|
|
306
308
|
"CursorPaginationFilterSchema",
|
|
307
309
|
"CursorPaginationSchema",
|
|
308
310
|
"DatabaseSettings",
|
|
311
|
+
"DestructiveMigrationError",
|
|
309
312
|
"DiskMetrics",
|
|
310
313
|
"DownloadUtils",
|
|
311
314
|
"EmailSettings",
|
|
@@ -318,6 +321,7 @@ __all__: list[str] = [
|
|
|
318
321
|
"GPUMetrics",
|
|
319
322
|
"GitHubOAuthClient",
|
|
320
323
|
"GoogleOAuthClient",
|
|
324
|
+
"GracefulShutdownMiddleware",
|
|
321
325
|
"HTTPClient",
|
|
322
326
|
"HardenedStaticFiles",
|
|
323
327
|
"HealthCheck",
|
{tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/__init__.py
RENAMED
|
@@ -27,6 +27,7 @@ from tempest_fastapi_sdk.api.middlewares import (
|
|
|
27
27
|
BodySizeLimitMiddleware,
|
|
28
28
|
CachedResponse,
|
|
29
29
|
CSRFMiddleware,
|
|
30
|
+
GracefulShutdownMiddleware,
|
|
30
31
|
IdempotencyMiddleware,
|
|
31
32
|
IdempotencyStore,
|
|
32
33
|
MemoryIdempotencyStore,
|
|
@@ -79,6 +80,7 @@ __all__: list[str] = [
|
|
|
79
80
|
"CachedResponse",
|
|
80
81
|
"GitHubOAuthClient",
|
|
81
82
|
"GoogleOAuthClient",
|
|
83
|
+
"GracefulShutdownMiddleware",
|
|
82
84
|
"HardenedStaticFiles",
|
|
83
85
|
"HealthCheck",
|
|
84
86
|
"IdempotencyMiddleware",
|
|
@@ -9,6 +9,7 @@ from tempest_fastapi_sdk.api.middlewares.csrf import (
|
|
|
9
9
|
generate_csrf_token,
|
|
10
10
|
make_csrf_token_dependency,
|
|
11
11
|
)
|
|
12
|
+
from tempest_fastapi_sdk.api.middlewares.graceful import GracefulShutdownMiddleware
|
|
12
13
|
from tempest_fastapi_sdk.api.middlewares.idempotency import (
|
|
13
14
|
IDEMPOTENCY_HEADER,
|
|
14
15
|
CachedResponse,
|
|
@@ -27,6 +28,7 @@ __all__: list[str] = [
|
|
|
27
28
|
"BodySizeLimitMiddleware",
|
|
28
29
|
"CSRFMiddleware",
|
|
29
30
|
"CachedResponse",
|
|
31
|
+
"GracefulShutdownMiddleware",
|
|
30
32
|
"IdempotencyMiddleware",
|
|
31
33
|
"IdempotencyStore",
|
|
32
34
|
"MemoryIdempotencyStore",
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Graceful-shutdown helper: drain in-flight requests before exit.
|
|
2
|
+
|
|
3
|
+
When an orchestrator (Kubernetes, a process manager) wants a replica
|
|
4
|
+
gone, it sends ``SIGTERM`` and, after a grace period, ``SIGKILL``. If a
|
|
5
|
+
request is still mid-flight when the worker dies, that request is
|
|
6
|
+
severed — a 502 the client sees as a flaky API.
|
|
7
|
+
|
|
8
|
+
``GracefulShutdownMiddleware`` smooths the window:
|
|
9
|
+
|
|
10
|
+
1. Once **draining**, new requests get ``503 Service Unavailable`` with a
|
|
11
|
+
``Retry-After`` header, so a load balancer in front quickly stops
|
|
12
|
+
routing here and the client retries elsewhere.
|
|
13
|
+
2. In-flight requests are **counted**; :meth:`wait_drained` blocks until
|
|
14
|
+
they finish (or a timeout), so shutdown can wait them out instead of
|
|
15
|
+
severing them.
|
|
16
|
+
|
|
17
|
+
You hold the instance and wire its :meth:`dispatch` into the app, then
|
|
18
|
+
drive it from the lifespan shutdown (which uvicorn runs on ``SIGTERM``,
|
|
19
|
+
and uvicorn owns the signal handling):
|
|
20
|
+
|
|
21
|
+
from contextlib import asynccontextmanager
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
25
|
+
|
|
26
|
+
from tempest_fastapi_sdk.api.middlewares.graceful import (
|
|
27
|
+
GracefulShutdownMiddleware,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
shutdown = GracefulShutdownMiddleware(drain_timeout=25.0)
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def lifespan(app: FastAPI):
|
|
34
|
+
yield
|
|
35
|
+
shutdown.begin_drain()
|
|
36
|
+
await shutdown.wait_drained()
|
|
37
|
+
|
|
38
|
+
app = FastAPI(lifespan=lifespan)
|
|
39
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=shutdown.dispatch)
|
|
40
|
+
|
|
41
|
+
Set the orchestrator's grace period a little **above** ``drain_timeout``
|
|
42
|
+
and uvicorn's ``--timeout-graceful-shutdown`` to match.
|
|
43
|
+
|
|
44
|
+
!!! warning "Signal handling belongs to your server"
|
|
45
|
+
uvicorn already installs ``SIGTERM`` handlers and triggers the
|
|
46
|
+
lifespan shutdown — drive draining from there. The opt-in
|
|
47
|
+
:meth:`install_signal_handlers` is only for servers that do **not**
|
|
48
|
+
manage signals themselves; it chains the previous handler via
|
|
49
|
+
:func:`signal.signal` and is a no-op off the main thread.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import asyncio
|
|
55
|
+
import logging
|
|
56
|
+
import signal
|
|
57
|
+
from typing import TYPE_CHECKING, Any
|
|
58
|
+
|
|
59
|
+
from starlette.responses import JSONResponse
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
63
|
+
|
|
64
|
+
from starlette.requests import Request
|
|
65
|
+
from starlette.responses import Response
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger("tempest_fastapi_sdk.api.graceful")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GracefulShutdownMiddleware:
|
|
71
|
+
"""Track in-flight requests and reject new ones with 503 while draining.
|
|
72
|
+
|
|
73
|
+
Wire :meth:`dispatch` via
|
|
74
|
+
``app.add_middleware(BaseHTTPMiddleware, dispatch=shutdown.dispatch)``
|
|
75
|
+
and call :meth:`begin_drain` / :meth:`wait_drained` from the
|
|
76
|
+
lifespan shutdown.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
drain_timeout (float): Seconds :meth:`wait_drained` waits for
|
|
80
|
+
in-flight requests before giving up.
|
|
81
|
+
retry_after (int): ``Retry-After`` header value on the 503s
|
|
82
|
+
served while draining.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
drain_timeout: float = 30.0,
|
|
89
|
+
retry_after: int = 5,
|
|
90
|
+
exempt_paths: Sequence[str] = (),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Initialize the helper.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
drain_timeout (float): Max seconds to wait for in-flight
|
|
96
|
+
requests in :meth:`wait_drained`. Defaults to ``30.0``.
|
|
97
|
+
retry_after (int): ``Retry-After`` seconds advertised on the
|
|
98
|
+
503 served while draining. Defaults to ``5``.
|
|
99
|
+
exempt_paths (Sequence[str]): Paths that keep being served
|
|
100
|
+
during drain. Usually unnecessary — letting health
|
|
101
|
+
endpoints return 503 is what makes a load balancer
|
|
102
|
+
deregister the instance. Defaults to none.
|
|
103
|
+
"""
|
|
104
|
+
self.drain_timeout: float = drain_timeout
|
|
105
|
+
self.retry_after: int = retry_after
|
|
106
|
+
self._exempt: frozenset[str] = frozenset(exempt_paths)
|
|
107
|
+
self._in_flight: int = 0
|
|
108
|
+
self._draining: bool = False
|
|
109
|
+
self._idle: asyncio.Event = asyncio.Event()
|
|
110
|
+
self._idle.set()
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def in_flight(self) -> int:
|
|
114
|
+
"""Return the number of requests currently being served.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
int: The in-flight request count.
|
|
118
|
+
"""
|
|
119
|
+
return self._in_flight
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def is_draining(self) -> bool:
|
|
123
|
+
"""Return whether draining has begun.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
bool: ``True`` once :meth:`begin_drain` (or a signal) fired.
|
|
127
|
+
"""
|
|
128
|
+
return self._draining
|
|
129
|
+
|
|
130
|
+
async def dispatch(
|
|
131
|
+
self,
|
|
132
|
+
request: Request,
|
|
133
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
134
|
+
) -> Response:
|
|
135
|
+
"""Starlette ``BaseHTTPMiddleware`` dispatch hook.
|
|
136
|
+
|
|
137
|
+
Serves ``503`` for new non-exempt requests once draining;
|
|
138
|
+
otherwise counts the request as in-flight while it runs.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
request (Request): The incoming request.
|
|
142
|
+
call_next (Callable[[Request], Awaitable[Response]]): The
|
|
143
|
+
downstream handler.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Response: The downstream response, or a ``503`` while
|
|
147
|
+
draining.
|
|
148
|
+
"""
|
|
149
|
+
if self._draining and request.url.path not in self._exempt:
|
|
150
|
+
return JSONResponse(
|
|
151
|
+
{"detail": "Server is shutting down."},
|
|
152
|
+
status_code=503,
|
|
153
|
+
headers={
|
|
154
|
+
"Retry-After": str(self.retry_after),
|
|
155
|
+
"Connection": "close",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
self._in_flight += 1
|
|
159
|
+
self._idle.clear()
|
|
160
|
+
try:
|
|
161
|
+
return await call_next(request)
|
|
162
|
+
finally:
|
|
163
|
+
self._in_flight -= 1
|
|
164
|
+
if self._in_flight == 0:
|
|
165
|
+
self._idle.set()
|
|
166
|
+
|
|
167
|
+
def begin_drain(self) -> None:
|
|
168
|
+
"""Flip into draining mode (idempotent).
|
|
169
|
+
|
|
170
|
+
New non-exempt requests get ``503`` from now on. Call from the
|
|
171
|
+
lifespan shutdown hook.
|
|
172
|
+
"""
|
|
173
|
+
if not self._draining:
|
|
174
|
+
self._draining = True
|
|
175
|
+
logger.info("graceful shutdown: draining (in-flight=%d)", self._in_flight)
|
|
176
|
+
|
|
177
|
+
async def wait_drained(self) -> bool:
|
|
178
|
+
"""Wait until in-flight requests finish or the timeout elapses.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
bool: ``True`` if everything drained in time, ``False`` if
|
|
182
|
+
:attr:`drain_timeout` elapsed with requests still running.
|
|
183
|
+
"""
|
|
184
|
+
if self._in_flight == 0:
|
|
185
|
+
return True
|
|
186
|
+
try:
|
|
187
|
+
await asyncio.wait_for(self._idle.wait(), timeout=self.drain_timeout)
|
|
188
|
+
return True
|
|
189
|
+
except TimeoutError:
|
|
190
|
+
logger.warning(
|
|
191
|
+
"graceful shutdown: drain timed out with %d request(s) in flight",
|
|
192
|
+
self._in_flight,
|
|
193
|
+
)
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def install_signal_handlers(
|
|
197
|
+
self, signals: Sequence[int] = (signal.SIGTERM, signal.SIGINT)
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Chain a drain trigger onto the given signals via ``signal.signal``.
|
|
200
|
+
|
|
201
|
+
Only for servers that do **not** manage signals themselves —
|
|
202
|
+
uvicorn does, so prefer the lifespan hook. Best-effort: a no-op
|
|
203
|
+
when called off the main thread.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
signals (Sequence[int]): Signals to hook. Defaults to
|
|
207
|
+
``SIGTERM`` and ``SIGINT``.
|
|
208
|
+
"""
|
|
209
|
+
for sig in signals:
|
|
210
|
+
previous = signal.getsignal(sig)
|
|
211
|
+
|
|
212
|
+
def _handler(signum: int, frame: Any, _previous: Any = previous) -> None:
|
|
213
|
+
self.begin_drain()
|
|
214
|
+
if callable(_previous):
|
|
215
|
+
_previous(signum, frame)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
signal.signal(sig, _handler)
|
|
219
|
+
except (ValueError, OSError): # pragma: no cover - non-main thread
|
|
220
|
+
logger.debug("could not install handler for signal %s", sig)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__: list[str] = [
|
|
224
|
+
"GracefulShutdownMiddleware",
|
|
225
|
+
]
|