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.
Files changed (388) hide show
  1. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/CHANGELOG.md +28 -0
  2. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/PKG-INFO +3 -3
  3. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/README.md +2 -2
  4. tempest_fastapi_sdk-0.46.0/docs/recipes/deploy-safety.en.md +111 -0
  5. tempest_fastapi_sdk-0.46.0/docs/recipes/deploy-safety.md +110 -0
  6. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.en.md +1 -0
  7. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/index.md +1 -0
  8. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.md +2 -0
  9. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/mkdocs.yml +2 -0
  10. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/pyproject.toml +1 -1
  11. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/__init__.py +5 -1
  12. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/__init__.py +2 -0
  13. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/__init__.py +2 -0
  14. tempest_fastapi_sdk-0.46.0/tempest_fastapi_sdk/api/middlewares/graceful.py +225 -0
  15. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/__init__.py +4 -0
  16. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/migrations.py +116 -0
  17. tempest_fastapi_sdk-0.46.0/tests/api/test_graceful.py +78 -0
  18. tempest_fastapi_sdk-0.46.0/tests/db/test_safe_upgrade.py +98 -0
  19. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/uv.lock +1 -1
  20. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/ci.yml +0 -0
  21. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/docs.yml +0 -0
  22. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.github/workflows/release-pypi.yml +0 -0
  23. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.gitignore +0 -0
  24. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/.python-version +0 -0
  25. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/CLAUDE.md +0 -0
  26. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/Makefile +0 -0
  27. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.en.md +0 -0
  28. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/architecture.md +0 -0
  29. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.en.md +0 -0
  30. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/changelog.md +0 -0
  31. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.en.md +0 -0
  32. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/contributing.md +0 -0
  33. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/index.en.md +0 -0
  34. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/index.md +0 -0
  35. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.en.md +0 -0
  36. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/installation.md +0 -0
  37. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.en.md +0 -0
  38. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/index.md +0 -0
  39. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.en.md +0 -0
  40. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/api.md +0 -0
  41. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.en.md +0 -0
  42. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/business-rules.md +0 -0
  43. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.en.md +0 -0
  44. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/domain.md +0 -0
  45. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.en.md +0 -0
  46. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/flows.md +0 -0
  47. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.en.md +0 -0
  48. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/learning/marketplace/index.md +0 -0
  49. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.en.md +0 -0
  50. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/migration.md +0 -0
  51. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.en.md +0 -0
  52. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/admin.md +0 -0
  53. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.en.md +0 -0
  54. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/auth-flow.md +0 -0
  55. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.en.md +0 -0
  56. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/br-helpers.md +0 -0
  57. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.en.md +0 -0
  58. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cache.md +0 -0
  59. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.en.md +0 -0
  60. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/cli.md +0 -0
  61. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.en.md +0 -0
  62. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/database.md +0 -0
  63. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.en.md +0 -0
  64. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/downloads.md +0 -0
  65. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.en.md +0 -0
  66. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/email.md +0 -0
  67. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.en.md +0 -0
  68. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http-client.md +0 -0
  69. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.en.md +0 -0
  70. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/http.md +0 -0
  71. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.en.md +0 -0
  72. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/idempotency.md +0 -0
  73. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.en.md +0 -0
  74. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/logging.md +0 -0
  75. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.en.md +0 -0
  76. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/metrics.md +0 -0
  77. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.en.md +0 -0
  78. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/mfa.md +0 -0
  79. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/multi-tenant.en.md +0 -0
  80. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/multi-tenant.md +0 -0
  81. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.en.md +0 -0
  82. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/observability.md +0 -0
  83. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.en.md +0 -0
  84. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/outbox.md +0 -0
  85. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.en.md +0 -0
  86. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/queue-tasks.md +0 -0
  87. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.en.md +0 -0
  88. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/realtime.md +0 -0
  89. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.en.md +0 -0
  90. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/security.md +0 -0
  91. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.en.md +0 -0
  92. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/sessions.md +0 -0
  93. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.en.md +0 -0
  94. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/storage.md +0 -0
  95. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.en.md +0 -0
  96. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/testing.md +0 -0
  97. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.en.md +0 -0
  98. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/uploads.md +0 -0
  99. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.en.md +0 -0
  100. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/utilities.md +0 -0
  101. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.en.md +0 -0
  102. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/webpush.md +0 -0
  103. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.en.md +0 -0
  104. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/recipes/websocket.md +0 -0
  105. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/reference.en.md +0 -0
  106. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.en.md +0 -0
  107. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/roadmap.md +0 -0
  108. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.en.md +0 -0
  109. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/docs/tutorial.md +0 -0
  110. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/scripts/extract_recipe.py +0 -0
  111. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/__init__.py +0 -0
  112. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/auth.py +0 -0
  113. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/config.py +0 -0
  114. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/forms.py +0 -0
  115. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/router.py +0 -0
  116. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/session.py +0 -0
  117. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/site.py +0 -0
  118. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/static/admin.css +0 -0
  119. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/base.html +0 -0
  120. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/dashboard.html +0 -0
  121. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/detail.html +0 -0
  122. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/form.html +0 -0
  123. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/list.html +0 -0
  124. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/login.html +0 -0
  125. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/logs.html +0 -0
  126. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/admin/templates/mfa.html +0 -0
  127. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/cookies.py +0 -0
  128. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/__init__.py +0 -0
  129. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/dependencies/auth.py +0 -0
  130. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/handlers.py +0 -0
  131. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/body_size.py +0 -0
  132. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/cors.py +0 -0
  133. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/csrf.py +0 -0
  134. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/idempotency.py +0 -0
  135. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/rate_limit.py +0 -0
  136. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/middlewares/request_id.py +0 -0
  137. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/oauth.py +0 -0
  138. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/__init__.py +0 -0
  139. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/health.py +0 -0
  140. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/logs.py +0 -0
  141. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/metrics.py +0 -0
  142. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/routers/tool_spec.py +0 -0
  143. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/server.py +0 -0
  144. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/static.py +0 -0
  145. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/tracing.py +0 -0
  146. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/api/webhooks.py +0 -0
  147. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/__init__.py +0 -0
  148. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/page_renderer.py +0 -0
  149. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/router.py +0 -0
  150. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/schemas.py +0 -0
  151. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/service.py +0 -0
  152. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation.html +0 -0
  153. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_error.html +0 -0
  154. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/activation_success.html +0 -0
  155. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset.html +0 -0
  156. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_error.html +0 -0
  157. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_form.html +0 -0
  158. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/auth/templates/password_reset_success.html +0 -0
  159. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/__init__.py +0 -0
  160. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/decorator.py +0 -0
  161. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cache/redis_manager.py +0 -0
  162. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/__init__.py +0 -0
  163. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/README.md.tmpl +0 -0
  164. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/env.example.tmpl +0 -0
  165. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/gitignore.tmpl +0 -0
  166. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/main.py.tmpl +0 -0
  167. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/pyproject.toml.tmpl +0 -0
  168. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/__init__.py.tmpl +0 -0
  169. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/__init__.py.tmpl +0 -0
  170. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/api/app.py.tmpl +0 -0
  171. {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
  172. {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
  173. {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
  174. {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
  175. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/controllers/__init__.py.tmpl +0 -0
  176. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/__init__.py.tmpl +0 -0
  177. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/exceptions.py.tmpl +0 -0
  178. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/core/settings.py.tmpl +0 -0
  179. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/db/__init__.py.tmpl +0 -0
  180. {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
  181. {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
  182. {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
  183. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/schemas/__init__.py.tmpl +0 -0
  184. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/server.py.tmpl +0 -0
  185. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/services/__init__.py.tmpl +0 -0
  186. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/src/utils/__init__.py.tmpl +0 -0
  187. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/__init__.py.tmpl +0 -0
  188. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/_templates/tests/test_smoke.py.tmpl +0 -0
  189. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/db.py +0 -0
  190. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/docker_compose.py +0 -0
  191. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/generate.py +0 -0
  192. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/lint.py +0 -0
  193. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/main.py +0 -0
  194. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/new.py +0 -0
  195. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/src_layers.py +0 -0
  196. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/cli/user.py +0 -0
  197. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/__init__.py +0 -0
  198. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/controllers/base.py +0 -0
  199. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/__init__.py +0 -0
  200. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/context.py +0 -0
  201. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/enums.py +0 -0
  202. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/core/logging.py +0 -0
  203. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/__init__.py +0 -0
  204. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/_alembic_templates/env.py.template +0 -0
  205. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/alembic_hooks.py +0 -0
  206. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/connection.py +0 -0
  207. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/mixins.py +0 -0
  208. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/model.py +0 -0
  209. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/outbox.py +0 -0
  210. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/repository.py +0 -0
  211. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/slow_query.py +0 -0
  212. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/tenant.py +0 -0
  213. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_model.py +0 -0
  214. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_recovery_code_model.py +0 -0
  215. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/db/user_token_model.py +0 -0
  216. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/__init__.py +0 -0
  217. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/base.py +0 -0
  218. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/conflict.py +0 -0
  219. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/forbidden.py +0 -0
  220. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/jwt.py +0 -0
  221. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/not_found.py +0 -0
  222. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/too_many_requests.py +0 -0
  223. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/unauthorized.py +0 -0
  224. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/upload.py +0 -0
  225. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/exceptions/validation.py +0 -0
  226. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/py.typed +0 -0
  227. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/__init__.py +0 -0
  228. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/queue/manager.py +0 -0
  229. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/__init__.py +0 -0
  230. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/base.py +0 -0
  231. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/link_headers.py +0 -0
  232. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/logs.py +0 -0
  233. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/pagination.py +0 -0
  234. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/schemas/response.py +0 -0
  235. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/__init__.py +0 -0
  236. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/services/base.py +0 -0
  237. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/__init__.py +0 -0
  238. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/dependencies.py +0 -0
  239. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/middleware.py +0 -0
  240. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/router.py +0 -0
  241. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/schemas.py +0 -0
  242. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/service.py +0 -0
  243. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sessions/store.py +0 -0
  244. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/__init__.py +0 -0
  245. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/base.py +0 -0
  246. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/settings/mixins.py +0 -0
  247. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/__init__.py +0 -0
  248. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/sse/event_stream.py +0 -0
  249. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/__init__.py +0 -0
  250. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/storage/minio_client.py +0 -0
  251. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/__init__.py +0 -0
  252. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/manager.py +0 -0
  253. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/tasks/scheduler.py +0 -0
  254. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/__init__.py +0 -0
  255. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/testing/database.py +0 -0
  256. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/__init__.py +0 -0
  257. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/client_ip.py +0 -0
  258. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/datetime.py +0 -0
  259. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/dict.py +0 -0
  260. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/download.py +0 -0
  261. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/email.py +0 -0
  262. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/http_client.py +0 -0
  263. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/jwt.py +0 -0
  264. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/log.py +0 -0
  265. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/metrics.py +0 -0
  266. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/opaque_token.py +0 -0
  267. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/password.py +0 -0
  268. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/regex.py +0 -0
  269. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/storage_backends.py +0 -0
  270. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/throttle.py +0 -0
  271. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/totp.py +0 -0
  272. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/utils/upload.py +0 -0
  273. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/__init__.py +0 -0
  274. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/dispatcher.py +0 -0
  275. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/webpush/schemas.py +0 -0
  276. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/__init__.py +0 -0
  277. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/hub.py +0 -0
  278. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/router.py +0 -0
  279. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tempest_fastapi_sdk/websockets/schemas.py +0 -0
  280. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/__init__.py +0 -0
  281. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/__init__.py +0 -0
  282. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_auth.py +0 -0
  283. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_forms.py +0 -0
  284. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_logs_nav.py +0 -0
  285. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_mfa.py +0 -0
  286. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_router.py +0 -0
  287. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_site.py +0 -0
  288. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/admin/test_user_model.py +0 -0
  289. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/__init__.py +0 -0
  290. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_body_size.py +0 -0
  291. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cookies.py +0 -0
  292. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_cors.py +0 -0
  293. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_csrf.py +0 -0
  294. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_dependencies_auth.py +0 -0
  295. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_handlers.py +0 -0
  296. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_health_router.py +0 -0
  297. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_idempotency.py +0 -0
  298. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_jwt_dependency.py +0 -0
  299. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_logs_router.py +0 -0
  300. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_oauth.py +0 -0
  301. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_prometheus.py +0 -0
  302. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_rate_limit.py +0 -0
  303. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_request_id_middleware.py +0 -0
  304. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_role_dependency.py +0 -0
  305. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_server.py +0 -0
  306. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_static.py +0 -0
  307. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tool_spec.py +0 -0
  308. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_tracing.py +0 -0
  309. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks.py +0 -0
  310. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/api/test_webhooks_rsa.py +0 -0
  311. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/__init__.py +0 -0
  312. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_mfa.py +0 -0
  313. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/auth/test_service.py +0 -0
  314. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/__init__.py +0 -0
  315. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_decorator.py +0 -0
  316. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cache/test_redis_manager.py +0 -0
  317. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/__init__.py +0 -0
  318. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_db.py +0 -0
  319. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_docker_compose.py +0 -0
  320. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_generate.py +0 -0
  321. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_main.py +0 -0
  322. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/cli/test_user.py +0 -0
  323. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/conftest.py +0 -0
  324. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/__init__.py +0 -0
  325. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/controllers/test_base.py +0 -0
  326. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/__init__.py +0 -0
  327. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_context.py +0 -0
  328. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_enums.py +0 -0
  329. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/core/test_logging.py +0 -0
  330. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/__init__.py +0 -0
  331. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_alembic_hooks.py +0 -0
  332. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_bulk_ops.py +0 -0
  333. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_connection.py +0 -0
  334. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_migrations.py +0 -0
  335. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_mixins.py +0 -0
  336. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_model.py +0 -0
  337. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_outbox.py +0 -0
  338. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_repository.py +0 -0
  339. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_slow_query.py +0 -0
  340. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/db/test_tenant.py +0 -0
  341. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/__init__.py +0 -0
  342. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/exceptions/test_exceptions.py +0 -0
  343. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/__init__.py +0 -0
  344. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/queue/test_manager.py +0 -0
  345. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/__init__.py +0 -0
  346. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_base.py +0 -0
  347. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_cursor_pagination.py +0 -0
  348. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_link_headers.py +0 -0
  349. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_pagination.py +0 -0
  350. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/schemas/test_response.py +0 -0
  351. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/services/__init__.py +0 -0
  352. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/services/test_base.py +0 -0
  353. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/__init__.py +0 -0
  354. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sessions/test_sessions.py +0 -0
  355. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/__init__.py +0 -0
  356. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_base.py +0 -0
  357. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/settings/test_mixins.py +0 -0
  358. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/__init__.py +0 -0
  359. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/sse/test_event_stream.py +0 -0
  360. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/__init__.py +0 -0
  361. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/storage/test_minio_client.py +0 -0
  362. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/__init__.py +0 -0
  363. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_manager.py +0 -0
  364. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/tasks/test_scheduler.py +0 -0
  365. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/__init__.py +0 -0
  366. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/testing/test_database.py +0 -0
  367. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/__init__.py +0 -0
  368. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_client_ip.py +0 -0
  369. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_datetime.py +0 -0
  370. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_dict.py +0 -0
  371. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_download.py +0 -0
  372. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_email.py +0 -0
  373. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_http_client.py +0 -0
  374. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_jwt.py +0 -0
  375. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_lazy_extras.py +0 -0
  376. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_log.py +0 -0
  377. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_metrics.py +0 -0
  378. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_opaque_token.py +0 -0
  379. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_password.py +0 -0
  380. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_regex.py +0 -0
  381. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_storage_backends.py +0 -0
  382. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_throttle.py +0 -0
  383. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/utils/test_upload.py +0 -0
  384. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/__init__.py +0 -0
  385. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_dispatcher.py +0 -0
  386. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/webpush/test_schemas.py +0 -0
  387. {tempest_fastapi_sdk-0.45.0 → tempest_fastapi_sdk-0.46.0}/tests/websockets/__init__.py +0 -0
  388. {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.45.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.45.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.45.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",
@@ -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
+ ]