tempest-fastapi-sdk 0.43.0__tar.gz → 0.45.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.43.0 → tempest_fastapi_sdk-0.45.0}/CHANGELOG.md +52 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/PKG-INFO +2 -2
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/README.md +1 -1
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/index.en.md +2 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/index.md +2 -0
- tempest_fastapi_sdk-0.45.0/docs/recipes/multi-tenant.en.md +120 -0
- tempest_fastapi_sdk-0.45.0/docs/recipes/multi-tenant.md +120 -0
- tempest_fastapi_sdk-0.45.0/docs/recipes/outbox.en.md +121 -0
- tempest_fastapi_sdk-0.45.0/docs/recipes/outbox.md +120 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/reference.md +4 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/mkdocs.yml +4 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/pyproject.toml +1 -1
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/__init__.py +9 -1
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/__init__.py +10 -0
- tempest_fastapi_sdk-0.45.0/tempest_fastapi_sdk/db/outbox.py +352 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/repository.py +47 -0
- tempest_fastapi_sdk-0.45.0/tempest_fastapi_sdk/db/tenant.py +348 -0
- tempest_fastapi_sdk-0.45.0/tests/db/test_outbox.py +179 -0
- tempest_fastapi_sdk-0.45.0/tests/db/test_tenant.py +170 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/uv.lock +45 -45
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/.github/workflows/ci.yml +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/.github/workflows/docs.yml +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/.gitignore +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/.python-version +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/CLAUDE.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/Makefile +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/architecture.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/architecture.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/changelog.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/changelog.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/contributing.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/contributing.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/index.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/index.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/installation.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/installation.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/index.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/index.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/api.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/api.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/business-rules.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/business-rules.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/domain.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/domain.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/flows.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/flows.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/index.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/learning/marketplace/index.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/migration.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/migration.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/admin.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/admin.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/auth-flow.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/auth-flow.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/br-helpers.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/br-helpers.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/cache.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/cache.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/cli.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/cli.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/database.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/database.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/downloads.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/downloads.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/email.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/email.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/http-client.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/http-client.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/http.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/http.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/idempotency.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/idempotency.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/logging.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/logging.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/metrics.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/metrics.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/mfa.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/mfa.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/observability.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/observability.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/queue-tasks.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/queue-tasks.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/realtime.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/realtime.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/security.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/security.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/sessions.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/sessions.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/storage.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/storage.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/testing.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/testing.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/uploads.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/uploads.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/utilities.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/utilities.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/webpush.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/webpush.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/websocket.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/recipes/websocket.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/reference.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/roadmap.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/roadmap.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/tutorial.en.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/docs/tutorial.md +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/scripts/extract_recipe.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/auth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/config.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/forms.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/session.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/site.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/static/admin.css +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/base.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/dashboard.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/detail.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/form.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/list.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/login.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/logs.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/admin/templates/mfa.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/cookies.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/dependencies/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/dependencies/auth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/handlers.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/body_size.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/cors.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/csrf.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/idempotency.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/middlewares/request_id.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/oauth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/routers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/routers/health.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/routers/logs.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/routers/metrics.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/routers/tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/server.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/static.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/tracing.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/api/webhooks.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/page_renderer.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/schemas.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/service.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/activation.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/activation_error.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/activation_success.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/password_reset.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/password_reset_error.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/password_reset_form.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/auth/templates/password_reset_success.html +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cache/decorator.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cache/redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/README.md.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/env.example.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/gitignore.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/main.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/pyproject.toml.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/app.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/auth.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/resources.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/api/routers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/controllers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/core/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/core/exceptions.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/core/settings.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/db/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/user.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/db/repositories/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/schemas/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/server.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/services/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/src/utils/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/tests/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/_templates/tests/test_smoke.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/db.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/generate.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/lint.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/main.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/new.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/src_layers.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/cli/user.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/controllers/base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/core/context.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/core/enums.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/core/logging.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/_alembic_templates/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/_alembic_templates/env.py.template +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/connection.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/migrations.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/mixins.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/slow_query.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/user_model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/user_recovery_code_model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/db/user_token_model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/conflict.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/forbidden.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/jwt.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/not_found.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/too_many_requests.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/unauthorized.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/upload.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/exceptions/validation.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/py.typed +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/queue/manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/link_headers.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/logs.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/pagination.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/schemas/response.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/services/base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/dependencies.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/middleware.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/schemas.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/service.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sessions/store.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/settings/base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/settings/mixins.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/sse/event_stream.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/storage/minio_client.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/tasks/manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/tasks/scheduler.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/testing/database.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/client_ip.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/datetime.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/dict.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/download.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/email.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/http_client.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/jwt.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/log.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/metrics.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/password.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/regex.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/throttle.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/totp.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/utils/upload.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/webpush/dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/webpush/schemas.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/websockets/hub.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/websockets/router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tempest_fastapi_sdk/websockets/schemas.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_auth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_forms.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_logs_nav.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_site.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/admin/test_user_model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_body_size.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_cookies.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_cors.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_csrf.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_dependencies_auth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_handlers.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_health_router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_idempotency.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_jwt_dependency.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_logs_router.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_oauth.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_prometheus.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_request_id_middleware.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_role_dependency.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_server.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_static.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_tracing.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_webhooks.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/api/test_webhooks_rsa.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/auth/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/auth/test_service.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cache/test_decorator.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cache/test_redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/test_db.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/test_docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/test_generate.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/test_main.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/cli/test_user.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/conftest.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/controllers/test_base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/core/test_context.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/core/test_enums.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/core/test_logging.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_bulk_ops.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_connection.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_migrations.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_model.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_repository.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/db/test_slow_query.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/exceptions/test_exceptions.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/queue/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/test_base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/test_cursor_pagination.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/test_link_headers.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/test_pagination.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/schemas/test_response.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/services/test_base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/sessions/test_sessions.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/settings/test_base.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/settings/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/sse/test_event_stream.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/storage/test_minio_client.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/tasks/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/tasks/test_scheduler.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/testing/test_database.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_client_ip.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_datetime.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_dict.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_download.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_email.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_http_client.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_jwt.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_lazy_extras.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_log.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_metrics.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_password.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_regex.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_throttle.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/utils/test_upload.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/webpush/test_dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/webpush/test_schemas.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.43.0 → tempest_fastapi_sdk-0.45.0}/tests/websockets/test_hub_and_router.py +0 -0
|
@@ -5,6 +5,58 @@ 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.45.0] — 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`TenantScopedRepository[ModelType]`** — a `BaseRepository` locked
|
|
13
|
+
to a single tenant for shared-schema multi-tenancy. Bind a
|
|
14
|
+
`tenant_id` (and optional `tenant_field`, default `"tenant_id"`) at
|
|
15
|
+
construction; it injects `WHERE tenant_id = ?` into **every** read
|
|
16
|
+
(`get`/`get_or_none`/`get_by_id`/`exists`/`first`/`list`/`count`/
|
|
17
|
+
`paginate`/`cursor_paginate`/`delete_many`) and stamps the tenant id
|
|
18
|
+
onto **every** write (`add`/`add_all`). `delete` / `delete_batch` add
|
|
19
|
+
the tenant predicate to the `DELETE` so a guessed id from another
|
|
20
|
+
tenant matches nothing — cross-tenant access (even probing existence
|
|
21
|
+
by id) is impossible through the repository. The constructor raises
|
|
22
|
+
`AttributeError` at boot if the model lacks the tenant column.
|
|
23
|
+
`tenant_column` property exposes the mapped column for custom queries.
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
|
|
27
|
+
- New bilingual recipe **Multi-tenant**, added to the nav (under
|
|
28
|
+
Database), the recipes index, and the API reference.
|
|
29
|
+
|
|
30
|
+
## [0.44.0] — 2026-06-11
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Transactional outbox.** New `BaseOutboxModel` (abstract — the
|
|
35
|
+
project subclasses it and picks `__tablename__`) carrying `topic`,
|
|
36
|
+
`payload` (JSON), `status`, `attempts` / `max_attempts`,
|
|
37
|
+
`available_at`, `sent_at` and `last_error`. `OutboxModel.new_event(
|
|
38
|
+
topic, payload)` builds a pending row.
|
|
39
|
+
- **`BaseRepository.save_with_outbox(model, event)`** inserts the
|
|
40
|
+
business row and the outbox event in the **same transaction**, so an
|
|
41
|
+
event can never reference a rolled-back row (and a committed row
|
|
42
|
+
always has its event durably queued) — the fix for the dual-write
|
|
43
|
+
problem.
|
|
44
|
+
- **`OutboxRelay`** drains pending rows and publishes them through a
|
|
45
|
+
caller-supplied async `publish` callable (no hard broker dependency
|
|
46
|
+
— works with `AsyncBrokerManager`, a webhook, a test spy). Marks each
|
|
47
|
+
row `sent`; on failure increments `attempts`, records `last_error`,
|
|
48
|
+
reschedules with exponential backoff, and marks `failed` once the
|
|
49
|
+
attempt budget is spent. Locks the batch with `FOR UPDATE SKIP
|
|
50
|
+
LOCKED` on PostgreSQL/MySQL (multi-worker safe), falling back to a
|
|
51
|
+
plain select on SQLite. `drain_once()` for one batch (tests/cron),
|
|
52
|
+
`run(poll_interval=...)` for the loop.
|
|
53
|
+
- **`OutboxStatus`** enum (`pending` / `sent` / `failed`).
|
|
54
|
+
|
|
55
|
+
### Docs
|
|
56
|
+
|
|
57
|
+
- New bilingual recipe **Outbox transacional / Transactional outbox**,
|
|
58
|
+
added to the nav, the recipes index, and the API reference.
|
|
59
|
+
|
|
8
60
|
## [0.43.0] — 2026-06-11
|
|
9
61
|
|
|
10
62
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempest-fastapi-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.45.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,7 +238,7 @@ 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` |
|
|
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` |
|
|
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
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]`)* |
|
|
@@ -136,7 +136,7 @@ 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` |
|
|
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` |
|
|
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
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]`)* |
|
|
@@ -10,11 +10,13 @@ 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 |
|
|
16
17
|
| **[Real-time »](realtime.md)** | Server-Sent Events (`EventStream`, `sse_response`) |
|
|
17
18
|
| **[Queue & Tasks »](queue-tasks.md)** | FastStream (`AsyncBrokerManager`), TaskIQ (`AsyncTaskBrokerManager`), `AsyncTaskScheduler`, transactional outbox |
|
|
19
|
+
| **[Transactional outbox »](outbox.md)** | `BaseOutboxModel`, `OutboxRelay`, `save_with_outbox` — reliable events |
|
|
18
20
|
| **[Transactional email »](email.md)** | `EmailUtils` — SMTP, text/HTML body, attachments, Jinja2 templates |
|
|
19
21
|
| **[Web Push »](webpush.md)** | `WebPushDispatcher`, VAPID schemas, broadcast with pruning |
|
|
20
22
|
| **[Logging »](logging.md)** | `LogUtils`, structured JSON logging, request-ID propagation |
|
|
@@ -10,11 +10,13 @@ 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` |
|
|
16
17
|
| **[Tempo real »](realtime.md)** | Server-Sent Events (`EventStream`, `sse_response`) |
|
|
17
18
|
| **[Fila e Tarefas »](queue-tasks.md)** | FastStream (`AsyncBrokerManager`), TaskIQ (`AsyncTaskBrokerManager`), `AsyncTaskScheduler`, outbox transacional |
|
|
19
|
+
| **[Outbox transacional »](outbox.md)** | `BaseOutboxModel`, `OutboxRelay`, `save_with_outbox` — eventos confiáveis |
|
|
18
20
|
| **[Email transacional »](email.md)** | `EmailUtils` — SMTP, corpo texto/HTML, anexos, templates Jinja2 |
|
|
19
21
|
| **[Web Push »](webpush.md)** | `WebPushDispatcher`, schemas VAPID, broadcast com poda |
|
|
20
22
|
| **[Logging »](logging.md)** | `LogUtils`, logging JSON estruturado, propagação de request-ID |
|
|
@@ -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.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Transactional outbox (reliable events)
|
|
2
|
+
|
|
3
|
+
When a handler **writes a row** and **publishes an event**, doing the two
|
|
4
|
+
as independent operations is fragile: if the process dies *after* the
|
|
5
|
+
commit but *before* the publish, the event is lost; if it dies after the
|
|
6
|
+
publish but before the commit, a phantom event references a row that never
|
|
7
|
+
existed. This is the **dual-write problem**.
|
|
8
|
+
|
|
9
|
+
The **outbox** pattern fixes it: write the business row **and** an
|
|
10
|
+
`outbox` row in the **same transaction**. Either both commit or neither
|
|
11
|
+
does. A separate *relay* reads the pending rows and publishes them to the
|
|
12
|
+
broker, marking each one sent. The broker can be down for minutes — the
|
|
13
|
+
events wait, durably, in the table.
|
|
14
|
+
|
|
15
|
+
!!! info "Where this fits"
|
|
16
|
+
It complements the [`AsyncBrokerManager`](queue-tasks.md): the broker
|
|
17
|
+
*publishes*, the outbox *guarantees* the event exists to be published.
|
|
18
|
+
The relay takes any publish callable — so it works with FastStream, a
|
|
19
|
+
webhook, whatever.
|
|
20
|
+
|
|
21
|
+
## 1. The outbox table
|
|
22
|
+
|
|
23
|
+
`BaseOutboxModel` is abstract — the project creates the concrete table
|
|
24
|
+
(just like `BaseUserModel`):
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from tempest_fastapi_sdk import BaseOutboxModel
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OutboxModel(BaseOutboxModel):
|
|
31
|
+
"""The service's pending-events table."""
|
|
32
|
+
|
|
33
|
+
__tablename__ = "outbox"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It ships `topic`, `payload` (JSON), `status`, `attempts`, `max_attempts`,
|
|
37
|
+
`available_at`, `sent_at` and `last_error` — on top of `BaseModel`'s four
|
|
38
|
+
canonical columns (`id` / `is_active` / `created_at` / `updated_at`).
|
|
39
|
+
Generate the migration with the [`AlembicHelper`](database.md) like any
|
|
40
|
+
other table.
|
|
41
|
+
|
|
42
|
+
## 2. Write atomically
|
|
43
|
+
|
|
44
|
+
In the service/repository, use `save_with_outbox` instead of `add`: it
|
|
45
|
+
inserts the business model **and** the event in one transaction.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tempest_fastapi_sdk import BaseRepository
|
|
49
|
+
|
|
50
|
+
from src.db.models import OrderModel, OutboxModel
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def place_order(repo: BaseRepository[OrderModel], data: dict[str, object]) -> OrderModel:
|
|
54
|
+
"""Create the order and queue the event in the same transaction."""
|
|
55
|
+
order = OrderModel(**data)
|
|
56
|
+
event = OutboxModel.new_event("orders.created", {"order": data})
|
|
57
|
+
return await repo.save_with_outbox(order, event)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If the `commit` fails (e.g. a unique constraint), **both** rows are rolled
|
|
61
|
+
back — no orphan event is ever left behind.
|
|
62
|
+
|
|
63
|
+
## 3. Drain and publish (the relay)
|
|
64
|
+
|
|
65
|
+
`OutboxRelay` reads pending rows and calls your publish callable. It does
|
|
66
|
+
not import any specific broker — you pass the function:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
from tempest_fastapi_sdk import AsyncDatabaseManager, BaseOutboxModel, OutboxRelay
|
|
72
|
+
|
|
73
|
+
from src.db.models import OutboxModel
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def run_relay(db: AsyncDatabaseManager, broker: object) -> None:
|
|
77
|
+
"""Publish pending events continuously."""
|
|
78
|
+
|
|
79
|
+
async def publish(event: BaseOutboxModel) -> None:
|
|
80
|
+
"""Forward one event to the broker."""
|
|
81
|
+
await broker.publish(event.payload, event.topic) # type: ignore[attr-defined]
|
|
82
|
+
|
|
83
|
+
relay: OutboxRelay = OutboxRelay(db, model=OutboxModel, publish=publish)
|
|
84
|
+
await relay.run(poll_interval=1.0) # loops until the task is cancelled
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run the relay as a separate process/worker (or a task in the lifespan).
|
|
88
|
+
Each published event becomes `status="sent"` with `sent_at` set.
|
|
89
|
+
|
|
90
|
+
### Failures and retry
|
|
91
|
+
|
|
92
|
+
If `publish` raises, the relay does **not** mark the event sent: it
|
|
93
|
+
increments `attempts`, records the error in `last_error`, and reschedules
|
|
94
|
+
the event with exponential backoff (`available_at` in the future). Once
|
|
95
|
+
`attempts` reaches `max_attempts`, the row becomes `status="failed"` and
|
|
96
|
+
stays in the table for manual inspection (never auto-retried again).
|
|
97
|
+
|
|
98
|
+
!!! tip "Multiple workers"
|
|
99
|
+
On PostgreSQL/MySQL the relay locks the batch with `FOR UPDATE SKIP
|
|
100
|
+
LOCKED`, so you can run **several** relay workers without publishing
|
|
101
|
+
the same event twice. On SQLite (no row locks) it falls back to a
|
|
102
|
+
plain `SELECT` — use a single worker.
|
|
103
|
+
|
|
104
|
+
### Drain once (tests / cron)
|
|
105
|
+
|
|
106
|
+
For loop-free scenarios (a test, a cron job), call `drain_once()`, which
|
|
107
|
+
returns how many events were published:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
published: int = await relay.drain_once()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Recap
|
|
114
|
+
|
|
115
|
+
- `BaseOutboxModel` → concrete `OutboxModel(__tablename__="outbox")`.
|
|
116
|
+
- `repo.save_with_outbox(model, event)` writes business + event
|
|
117
|
+
**atomically**.
|
|
118
|
+
- `OutboxRelay(db, model=..., publish=...).run()` publishes pending rows,
|
|
119
|
+
with retry/backoff and `sent` / `failed` marking.
|
|
120
|
+
- `OutboxModel.new_event(topic, payload)` builds the event; `drain_once()`
|
|
121
|
+
drains one batch for tests/cron.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Outbox transacional (eventos confiáveis)
|
|
2
|
+
|
|
3
|
+
Quando um handler **grava uma linha** e **publica um evento**, fazer as
|
|
4
|
+
duas coisas como operações independentes é frágil: se o processo morre
|
|
5
|
+
*depois* do commit mas *antes* do publish, o evento some; se morre depois
|
|
6
|
+
do publish mas antes do commit, sobra um evento fantasma apontando pra uma
|
|
7
|
+
linha que nunca existiu. Isso é o **dual-write problem**.
|
|
8
|
+
|
|
9
|
+
O padrão **outbox** resolve: grave a linha de negócio **e** uma linha
|
|
10
|
+
`outbox` na **mesma transação**. Ou as duas comitam, ou nenhuma. Um
|
|
11
|
+
*relay* separado lê as linhas pendentes e publica no broker, marcando cada
|
|
12
|
+
uma como enviada. O broker pode ficar minutos fora do ar — os eventos
|
|
13
|
+
esperam, duráveis, na tabela.
|
|
14
|
+
|
|
15
|
+
!!! info "Onde isso encaixa"
|
|
16
|
+
Complementa o [`AsyncBrokerManager`](queue-tasks.md): o broker
|
|
17
|
+
*publica*, o outbox *garante* que o evento existe pra ser publicado. O
|
|
18
|
+
relay usa qualquer callable de publish — então funciona com FastStream,
|
|
19
|
+
webhook, o que for.
|
|
20
|
+
|
|
21
|
+
## 1. A tabela outbox
|
|
22
|
+
|
|
23
|
+
`BaseOutboxModel` é abstrata — o projeto cria a tabela concreta (igual a
|
|
24
|
+
`BaseUserModel`):
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from tempest_fastapi_sdk import BaseOutboxModel
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OutboxModel(BaseOutboxModel):
|
|
31
|
+
"""Tabela de eventos pendentes do serviço."""
|
|
32
|
+
|
|
33
|
+
__tablename__ = "outbox"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Ela já traz `topic`, `payload` (JSON), `status`, `attempts`,
|
|
37
|
+
`max_attempts`, `available_at`, `sent_at` e `last_error` — além das quatro
|
|
38
|
+
colunas canônicas do `BaseModel` (`id` / `is_active` / `created_at` /
|
|
39
|
+
`updated_at`). Gere a migration com o [`AlembicHelper`](database.md) como
|
|
40
|
+
qualquer outra tabela.
|
|
41
|
+
|
|
42
|
+
## 2. Gravar de forma atômica
|
|
43
|
+
|
|
44
|
+
No service/repository, use `save_with_outbox` em vez de `add`: ele insere o
|
|
45
|
+
modelo de negócio **e** o evento numa transação só.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tempest_fastapi_sdk import BaseRepository
|
|
49
|
+
|
|
50
|
+
from src.db.models import OrderModel, OutboxModel
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def place_order(repo: BaseRepository[OrderModel], data: dict[str, object]) -> OrderModel:
|
|
54
|
+
"""Cria o pedido e enfileira o evento na mesma transação."""
|
|
55
|
+
order = OrderModel(**data)
|
|
56
|
+
event = OutboxModel.new_event("orders.created", {"order": data})
|
|
57
|
+
return await repo.save_with_outbox(order, event)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Se o `commit` falhar (ex.: constraint única), **as duas** linhas são
|
|
61
|
+
revertidas — nunca sobra evento órfão.
|
|
62
|
+
|
|
63
|
+
## 3. Drenar e publicar (o relay)
|
|
64
|
+
|
|
65
|
+
`OutboxRelay` lê as linhas pendentes e chama o seu callable de publish. Ele
|
|
66
|
+
não importa nenhum broker específico — você passa a função:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
from tempest_fastapi_sdk import AsyncDatabaseManager, BaseOutboxModel, OutboxRelay
|
|
72
|
+
|
|
73
|
+
from src.db.models import OutboxModel
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def run_relay(db: AsyncDatabaseManager, broker: object) -> None:
|
|
77
|
+
"""Publica eventos pendentes continuamente."""
|
|
78
|
+
|
|
79
|
+
async def publish(event: BaseOutboxModel) -> None:
|
|
80
|
+
"""Encaminha um evento pro broker."""
|
|
81
|
+
await broker.publish(event.payload, event.topic) # type: ignore[attr-defined]
|
|
82
|
+
|
|
83
|
+
relay: OutboxRelay = OutboxRelay(db, model=OutboxModel, publish=publish)
|
|
84
|
+
await relay.run(poll_interval=1.0) # loop até a task ser cancelada
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Rode o relay como um processo/worker separado (ou uma task no lifespan).
|
|
88
|
+
Cada evento publicado vira `status="sent"` com `sent_at` preenchido.
|
|
89
|
+
|
|
90
|
+
### Falhas e retry
|
|
91
|
+
|
|
92
|
+
Se o `publish` levanta exceção, o relay **não** marca como enviado: ele
|
|
93
|
+
incrementa `attempts`, guarda o erro em `last_error` e reagenda o evento
|
|
94
|
+
com backoff exponencial (`available_at` no futuro). Quando `attempts`
|
|
95
|
+
chega em `max_attempts`, a linha vira `status="failed"` e fica na tabela
|
|
96
|
+
pra inspeção manual (nunca mais é retentada automaticamente).
|
|
97
|
+
|
|
98
|
+
!!! tip "Múltiplos workers"
|
|
99
|
+
Em PostgreSQL/MySQL o relay trava o lote com `FOR UPDATE SKIP LOCKED`,
|
|
100
|
+
então você pode rodar **vários** workers de relay sem publicar o mesmo
|
|
101
|
+
evento duas vezes. Em SQLite (sem lock de linha) ele cai pra um
|
|
102
|
+
`SELECT` simples — use um worker só.
|
|
103
|
+
|
|
104
|
+
### Drenar uma vez (testes / cron)
|
|
105
|
+
|
|
106
|
+
Pra cenários sem loop (um teste, um cron job), chame `drain_once()`, que
|
|
107
|
+
devolve quantos eventos foram publicados:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
published: int = await relay.drain_once()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Recap
|
|
114
|
+
|
|
115
|
+
- `BaseOutboxModel` → tabela concreta `OutboxModel(__tablename__="outbox")`.
|
|
116
|
+
- `repo.save_with_outbox(model, event)` grava negócio + evento **atômico**.
|
|
117
|
+
- `OutboxRelay(db, model=..., publish=...).run()` publica os pendentes,
|
|
118
|
+
com retry/backoff e marcação `sent` / `failed`.
|
|
119
|
+
- `OutboxModel.new_event(topic, payload)` monta o evento; `drain_once()`
|
|
120
|
+
drena um lote pra testes/cron.
|
|
@@ -31,12 +31,16 @@ 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
|
|
37
38
|
::: tempest_fastapi_sdk.db.connection.AsyncDatabaseManager
|
|
38
39
|
::: tempest_fastapi_sdk.db.migrations.AlembicHelper
|
|
39
40
|
::: tempest_fastapi_sdk.db.slow_query.SlowQueryLogger
|
|
41
|
+
::: tempest_fastapi_sdk.db.outbox.BaseOutboxModel
|
|
42
|
+
::: tempest_fastapi_sdk.db.outbox.OutboxRelay
|
|
43
|
+
::: tempest_fastapi_sdk.db.outbox.OutboxStatus
|
|
40
44
|
|
|
41
45
|
---
|
|
42
46
|
|
|
@@ -86,12 +86,14 @@ 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
|
|
92
93
|
Tempo real: Real-time
|
|
93
94
|
WebSocket router: WebSocket router
|
|
94
95
|
Fila e Tarefas: Queue & Tasks
|
|
96
|
+
Outbox transacional: Transactional outbox
|
|
95
97
|
Email transacional: Transactional email
|
|
96
98
|
Web Push: Web Push
|
|
97
99
|
Logging: Logging
|
|
@@ -199,12 +201,14 @@ nav:
|
|
|
199
201
|
- Receitas:
|
|
200
202
|
- recipes/index.md
|
|
201
203
|
- Banco de dados: recipes/database.md
|
|
204
|
+
- Multi-tenant: recipes/multi-tenant.md
|
|
202
205
|
- Camada HTTP: recipes/http.md
|
|
203
206
|
- "HTTP client (saída)": recipes/http-client.md
|
|
204
207
|
- Cache: recipes/cache.md
|
|
205
208
|
- Tempo real: recipes/realtime.md
|
|
206
209
|
- WebSocket router: recipes/websocket.md
|
|
207
210
|
- Fila e Tarefas: recipes/queue-tasks.md
|
|
211
|
+
- Outbox transacional: recipes/outbox.md
|
|
208
212
|
- Email transacional: recipes/email.md
|
|
209
213
|
- Web Push: recipes/webpush.md
|
|
210
214
|
- Logging: recipes/logging.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tempest-fastapi-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.45.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"
|