svc-infra 0.1.629__tar.gz → 0.1.630__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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (344) hide show
  1. {svc_infra-0.1.629 → svc_infra-0.1.630}/PKG-INFO +1 -1
  2. {svc_infra-0.1.629 → svc_infra-0.1.630}/pyproject.toml +3 -1
  3. svc_infra-0.1.630/src/svc_infra/api/fastapi/billing/router.py +64 -0
  4. svc_infra-0.1.630/src/svc_infra/api/fastapi/billing/setup.py +19 -0
  5. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/session.py +16 -0
  6. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +15 -0
  7. svc_infra-0.1.630/src/svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  8. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
  9. svc_infra-0.1.630/src/svc_infra/api/fastapi/middleware/timeout.py +144 -0
  10. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/setup.py +10 -0
  11. svc_infra-0.1.630/src/svc_infra/billing/async_service.py +147 -0
  12. svc_infra-0.1.630/src/svc_infra/billing/jobs.py +218 -0
  13. svc_infra-0.1.630/src/svc_infra/billing/quotas.py +101 -0
  14. svc_infra-0.1.630/src/svc_infra/billing/schemas.py +33 -0
  15. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0008-billing-primitives.md +34 -0
  16. svc_infra-0.1.630/src/svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  17. svc_infra-0.1.630/src/svc_infra/docs/billing.md +190 -0
  18. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/ops.md +4 -0
  19. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/rate-limiting.md +4 -0
  20. svc_infra-0.1.630/src/svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  21. svc_infra-0.1.630/src/svc_infra/http/__init__.py +13 -0
  22. svc_infra-0.1.630/src/svc_infra/http/client.py +64 -0
  23. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/builtins/webhook_delivery.py +14 -2
  24. svc_infra-0.1.630/src/svc_infra/jobs/runner.py +75 -0
  25. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/worker.py +17 -1
  26. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/hibp.py +6 -2
  27. {svc_infra-0.1.629 → svc_infra-0.1.630}/README.md +0 -0
  28. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/__init__.py +0 -0
  29. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/README.md +0 -0
  30. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/__init__.py +0 -0
  31. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/alembic.py +0 -0
  32. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/models.py +0 -0
  33. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
  34. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
  35. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/base.py +0 -0
  36. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/registry.py +0 -0
  37. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
  38. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/schemas.py +0 -0
  39. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/service.py +0 -0
  40. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/apf_payments/settings.py +0 -0
  41. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/__init__.py +0 -0
  42. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/__init__.py +0 -0
  43. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  44. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
  45. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
  46. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
  47. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
  48. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/add.py +0 -0
  49. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
  50. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
  51. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
  52. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
  53. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
  54. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
  55. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
  56. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
  57. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
  58. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
  59. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
  60. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
  61. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
  62. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
  63. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
  64. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/security.py +0 -0
  65. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
  66. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
  67. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/state.py +0 -0
  68. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
  69. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/cache/add.py +0 -0
  70. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  71. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/http.py +0 -0
  72. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
  73. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
  74. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
  75. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
  76. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
  77. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
  78. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
  79. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
  80. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
  81. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
  82. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
  83. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
  84. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
  85. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/add.py +0 -0
  86. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
  87. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
  88. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
  89. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
  90. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
  91. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/public.py +0 -0
  92. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/router.py +0 -0
  93. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
  94. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dx.py +0 -0
  95. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/ease.py +0 -0
  96. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
  97. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
  98. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
  99. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
  100. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  101. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
  102. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  103. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  104. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  105. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
  106. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
  107. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
  108. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
  109. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
  110. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
  111. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
  112. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
  113. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
  114. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
  115. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
  116. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
  117. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
  118. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
  119. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/ops/add.py +0 -0
  120. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/pagination.py +0 -0
  121. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
  122. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
  123. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
  124. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
  125. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/user.py +0 -0
  126. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  127. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  128. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/tenancy/add.py +0 -0
  129. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/tenancy/context.py +0 -0
  130. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/README.md +0 -0
  131. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/__init__.py +0 -0
  132. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/env.py +0 -0
  133. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/logging/__init__.py +0 -0
  134. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/logging/add.py +0 -0
  135. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/logging/filter.py +0 -0
  136. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/logging/formats.py +0 -0
  137. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/app/root.py +0 -0
  138. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/billing/__init__.py +0 -0
  139. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/billing/models.py +0 -0
  140. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/billing/service.py +0 -0
  141. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/README.md +0 -0
  142. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/__init__.py +0 -0
  143. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/getting-started.md +0 -0
  144. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/README.md +0 -0
  145. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/__init__.py +0 -0
  146. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/backend.py +0 -0
  147. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/decorators.py +0 -0
  148. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/demo.py +0 -0
  149. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/keys.py +0 -0
  150. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/recache.py +0 -0
  151. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/resources.py +0 -0
  152. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/tags.py +0 -0
  153. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/ttl.py +0 -0
  154. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cache/utils.py +0 -0
  155. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/__init__.py +0 -0
  156. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/__main__.py +0 -0
  157. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/__init__.py +0 -0
  158. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
  159. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
  160. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
  161. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
  162. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
  163. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
  164. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
  165. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
  166. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +0 -0
  167. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
  168. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/docs/docs_cmds.py +0 -0
  169. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/dx/__init__.py +0 -0
  170. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/dx/dx_cmds.py +0 -0
  171. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/help.py +0 -0
  172. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
  173. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
  174. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
  175. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
  176. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/sdk/__init__.py +0 -0
  177. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/sdk/sdk_cmds.py +0 -0
  178. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/__init__.py +0 -0
  179. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/runner.py +0 -0
  180. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  181. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/data/add.py +0 -0
  182. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/data/backup.py +0 -0
  183. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/data/erasure.py +0 -0
  184. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/data/fixtures.py +0 -0
  185. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/data/retention.py +0 -0
  186. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/__init__.py +0 -0
  187. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/crud_schema.py +0 -0
  188. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/inbox.py +0 -0
  189. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/__init__.py +0 -0
  190. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/base.py +0 -0
  191. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/constants.py +0 -0
  192. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/core.py +0 -0
  193. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/indexes.py +0 -0
  194. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/management.py +0 -0
  195. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/README.md +0 -0
  196. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
  197. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/client.py +0 -0
  198. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
  199. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
  200. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
  201. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
  202. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
  203. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/repository.py +0 -0
  204. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/resource.py +0 -0
  205. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/scaffold.py +0 -0
  206. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/service.py +0 -0
  207. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
  208. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/types.py +0 -0
  209. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/nosql/utils.py +0 -0
  210. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/outbox.py +0 -0
  211. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/README.md +0 -0
  212. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/__init__.py +0 -0
  213. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/apikey.py +0 -0
  214. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/authref.py +0 -0
  215. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/base.py +0 -0
  216. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/constants.py +0 -0
  217. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/core.py +0 -0
  218. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/management.py +0 -0
  219. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/repository.py +0 -0
  220. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/resource.py +0 -0
  221. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/scaffold.py +0 -0
  222. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/service.py +0 -0
  223. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
  224. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/__init__.py +0 -0
  225. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
  226. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
  227. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  228. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
  229. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  230. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
  231. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
  232. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
  233. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
  234. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
  235. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/tenant.py +0 -0
  236. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/types.py +0 -0
  237. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/uniq.py +0 -0
  238. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
  239. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/utils.py +0 -0
  240. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/sql/versioning.py +0 -0
  241. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/db/utils.py +0 -0
  242. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/acceptance-matrix.md +0 -0
  243. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/acceptance.md +0 -0
  244. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +0 -0
  245. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0003-webhooks-framework.md +0 -0
  246. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0004-tenancy-model.md +0 -0
  247. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0005-data-lifecycle.md +0 -0
  248. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0006-ops-slos-and-metrics.md +0 -0
  249. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0007-docs-and-sdks.md +0 -0
  250. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0009-acceptance-harness.md +0 -0
  251. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/api.md +0 -0
  252. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/auth.md +0 -0
  253. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/cache.md +0 -0
  254. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/cli.md +0 -0
  255. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/contributing.md +0 -0
  256. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/data-lifecycle.md +0 -0
  257. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/database.md +0 -0
  258. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/docs-and-sdks.md +0 -0
  259. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/environment.md +0 -0
  260. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/getting-started.md +0 -0
  261. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/idempotency.md +0 -0
  262. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/jobs.md +0 -0
  263. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/observability.md +0 -0
  264. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/repo-review.md +0 -0
  265. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/security.md +0 -0
  266. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/tenancy.md +0 -0
  267. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/docs/webhooks.md +0 -0
  268. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/dx/add.py +0 -0
  269. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/dx/changelog.py +0 -0
  270. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/dx/checks.py +0 -0
  271. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
  272. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/easy.py +0 -0
  273. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/loader.py +0 -0
  274. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/queue.py +0 -0
  275. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/redis_queue.py +0 -0
  276. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/jobs/scheduler.py +0 -0
  277. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/mcp/__init__.py +0 -0
  278. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  279. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/README.md +0 -0
  280. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/__init__.py +0 -0
  281. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/add.py +0 -0
  282. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/cloud_dash.py +0 -0
  283. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/grafana/dashboards/http-overview.json +0 -0
  284. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/__init__.py +0 -0
  285. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/asgi.py +0 -0
  286. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/base.py +0 -0
  287. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/http.py +0 -0
  288. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
  289. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/metrics.py +0 -0
  290. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/__init__.py +0 -0
  291. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
  292. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
  293. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
  294. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
  295. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
  296. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
  297. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
  298. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
  299. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
  300. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
  301. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
  302. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
  303. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
  304. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
  305. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
  306. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
  307. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
  308. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/settings.py +0 -0
  309. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
  310. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
  311. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
  312. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
  313. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
  314. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
  315. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
  316. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
  317. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
  318. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
  319. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
  320. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
  321. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
  322. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
  323. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
  324. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
  325. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/py.typed +0 -0
  326. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/add.py +0 -0
  327. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/audit.py +0 -0
  328. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/audit_service.py +0 -0
  329. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/headers.py +0 -0
  330. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/jwt_rotation.py +0 -0
  331. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/lockout.py +0 -0
  332. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/models.py +0 -0
  333. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/org_invites.py +0 -0
  334. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/passwords.py +0 -0
  335. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/permissions.py +0 -0
  336. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/session.py +0 -0
  337. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/security/signed_cookies.py +0 -0
  338. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/utils.py +0 -0
  339. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/__init__.py +0 -0
  340. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/add.py +0 -0
  341. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/fastapi.py +0 -0
  342. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/router.py +0 -0
  343. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/service.py +0 -0
  344. {svc_infra-0.1.629 → svc_infra-0.1.630}/src/svc_infra/webhooks/signing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.629
3
+ Version: 0.1.630
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "svc-infra"
3
- version = "0.1.629"
3
+ version = "0.1.630"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -122,11 +122,13 @@ python_files = ["test_*.py", "*_test.py"]
122
122
  python_classes = ["Test*",]
123
123
  python_functions = ["test_*"]
124
124
  markers = [
125
+ "acceptance: End-to-end acceptance tests running against the acceptance app or BASE_URL",
125
126
  "security: Security and auth hardening tests",
126
127
  "ratelimit: Rate limiting and abuse protection tests",
127
128
  "concurrency: Idempotency and concurrency control tests",
128
129
  "jobs: Background jobs and scheduling tests",
129
130
  "webhooks: Webhooks framework tests",
131
+ "billing: Billing primitives tests",
130
132
  "tenancy: Tenancy isolation and enforcement tests",
131
133
  "data_lifecycle: Data lifecycle (fixtures, retention, erasure, backups)",
132
134
  "ops: SLOs & Ops tests (probes, breaker, instrumentation)",
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Annotated, Optional
5
+
6
+ from fastapi import APIRouter, Depends, Response, status
7
+
8
+ from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
9
+ from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
10
+ from svc_infra.api.fastapi.tenancy.context import TenantId
11
+ from svc_infra.billing.async_service import AsyncBillingService
12
+ from svc_infra.billing.schemas import UsageAckOut, UsageAggregateRow, UsageAggregatesOut, UsageIn
13
+
14
+ router = APIRouter(prefix="/_billing", tags=["Billing"])
15
+
16
+
17
+ def get_service(tenant_id: TenantId, session: SqlSessionDep) -> AsyncBillingService:
18
+ return AsyncBillingService(session=session, tenant_id=tenant_id)
19
+
20
+
21
+ @router.post(
22
+ "/usage",
23
+ name="billing_record_usage",
24
+ status_code=status.HTTP_202_ACCEPTED,
25
+ response_model=UsageAckOut,
26
+ dependencies=[Depends(require_idempotency_key)],
27
+ )
28
+ async def record_usage(
29
+ data: UsageIn, svc: Annotated[AsyncBillingService, Depends(get_service)], response: Response
30
+ ):
31
+ at = data.at or datetime.now(tz=timezone.utc)
32
+ evt_id = await svc.record_usage(
33
+ metric=data.metric,
34
+ amount=int(data.amount),
35
+ at=at,
36
+ idempotency_key=data.idempotency_key,
37
+ metadata=data.metadata,
38
+ )
39
+ # For 202, no Location header is required, but we can surface the id in the body
40
+ return UsageAckOut(id=evt_id, accepted=True)
41
+
42
+
43
+ @router.get(
44
+ "/usage",
45
+ name="billing_list_aggregates",
46
+ response_model=UsageAggregatesOut,
47
+ )
48
+ async def list_aggregates(
49
+ metric: str,
50
+ date_from: Optional[datetime] = None,
51
+ date_to: Optional[datetime] = None,
52
+ svc: Annotated[AsyncBillingService, Depends(get_service)] = None,
53
+ ):
54
+ rows = await svc.list_daily_aggregates(metric=metric, date_from=date_from, date_to=date_to)
55
+ items = [
56
+ UsageAggregateRow(
57
+ period_start=r.period_start,
58
+ granularity=r.granularity,
59
+ metric=r.metric,
60
+ total=int(r.total),
61
+ )
62
+ for r in rows
63
+ ]
64
+ return UsageAggregatesOut(items=items, next_cursor=None)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from .router import router as billing_router
6
+
7
+
8
+ def add_billing(app: FastAPI, *, prefix: str = "/_billing") -> None:
9
+ # Mount under the chosen prefix; default is /_billing
10
+ if prefix and prefix != "/_billing":
11
+ # If a custom prefix is desired, clone router with new prefix
12
+ from fastapi import APIRouter
13
+
14
+ custom = APIRouter(prefix=prefix, tags=["Billing"])
15
+ for route in billing_router.routes:
16
+ custom.routes.append(route)
17
+ app.include_router(custom)
18
+ else:
19
+ app.include_router(billing_router)
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import os
4
5
  from typing import Annotated, AsyncIterator, Tuple
5
6
 
6
7
  from fastapi import Depends
8
+ from sqlalchemy import text
7
9
  from sqlalchemy.ext.asyncio import (
8
10
  AsyncEngine,
9
11
  AsyncSession,
@@ -53,6 +55,20 @@ async def get_session() -> AsyncIterator[AsyncSession]:
53
55
  if _SessionLocal is None:
54
56
  raise RuntimeError("Database not initialized. Call add_sql_db(app, ...) first.")
55
57
  async with _SessionLocal() as session:
58
+ # Optional: set a per-transaction statement timeout for Postgres if configured
59
+ raw_ms = os.getenv("DB_STATEMENT_TIMEOUT_MS")
60
+ if raw_ms:
61
+ try:
62
+ ms = int(raw_ms)
63
+ if ms > 0:
64
+ try:
65
+ # SET LOCAL applies for the duration of the current transaction only
66
+ await session.execute(text("SET LOCAL statement_timeout = :ms"), {"ms": ms})
67
+ except Exception:
68
+ # Non-PG dialects (e.g., SQLite) will error; ignore silently
69
+ pass
70
+ except ValueError:
71
+ pass
56
72
  try:
57
73
  yield session
58
74
  await session.commit()
@@ -2,6 +2,7 @@ import logging
2
2
  import traceback
3
3
  from typing import Any, Dict, Optional
4
4
 
5
+ import httpx
5
6
  from fastapi import Request
6
7
  from fastapi.exceptions import HTTPException, RequestValidationError
7
8
  from fastapi.responses import JSONResponse, Response
@@ -67,6 +68,20 @@ def problem_response(
67
68
 
68
69
 
69
70
  def register_error_handlers(app):
71
+ @app.exception_handler(httpx.TimeoutException)
72
+ async def handle_httpx_timeout(request: Request, exc: httpx.TimeoutException):
73
+ trace_id = _trace_id_from_request(request)
74
+ # Map outbound HTTP client timeouts to 504 Gateway Timeout
75
+ # Keep details generic in prod
76
+ return problem_response(
77
+ status=504,
78
+ title="Gateway Timeout",
79
+ detail=("Upstream request timed out." if IS_PROD else (str(exc) or "httpx timeout")),
80
+ code="GATEWAY_TIMEOUT",
81
+ instance=str(request.url),
82
+ trace_id=trace_id,
83
+ )
84
+
70
85
  @app.exception_handler(FastApiException)
71
86
  async def handle_app_exception(request: Request, exc: FastApiException):
72
87
  trace_id = _trace_id_from_request(request)
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ from contextlib import asynccontextmanager
7
+ from typing import Optional
8
+
9
+ from fastapi import FastAPI
10
+ from starlette.types import ASGIApp, Receive, Scope, Send
11
+
12
+ from svc_infra.app.env import pick
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _get_grace_period_seconds() -> float:
18
+ default = pick(prod=20.0, nonprod=5.0)
19
+ raw = os.getenv("SHUTDOWN_GRACE_PERIOD_SECONDS")
20
+ if raw is None or raw == "":
21
+ return float(default)
22
+ try:
23
+ return float(raw)
24
+ except ValueError:
25
+ return float(default)
26
+
27
+
28
+ class InflightTrackerMiddleware:
29
+ """Tracks number of in-flight requests to support graceful shutdown drains."""
30
+
31
+ def __init__(self, app: ASGIApp):
32
+ self.app = app
33
+
34
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
35
+ if scope.get("type") != "http":
36
+ await self.app(scope, receive, send)
37
+ return
38
+ state = scope.get("app").state # type: ignore[attr-defined]
39
+ state._inflight_requests = getattr(state, "_inflight_requests", 0) + 1
40
+ try:
41
+ await self.app(scope, receive, send)
42
+ finally:
43
+ state._inflight_requests = max(0, getattr(state, "_inflight_requests", 1) - 1)
44
+
45
+
46
+ async def _wait_for_drain(app: FastAPI, grace: float) -> None:
47
+ interval = 0.1
48
+ waited = 0.0
49
+ while waited < grace:
50
+ inflight = int(getattr(app.state, "_inflight_requests", 0))
51
+ if inflight <= 0:
52
+ return
53
+ await asyncio.sleep(interval)
54
+ waited += interval
55
+ inflight = int(getattr(app.state, "_inflight_requests", 0))
56
+ if inflight > 0:
57
+ logger.warning(
58
+ "Graceful shutdown timeout: %s in-flight request(s) after %.2fs", inflight, waited
59
+ )
60
+
61
+
62
+ def install_graceful_shutdown(app: FastAPI, *, grace_seconds: Optional[float] = None) -> None:
63
+ """Install inflight tracking and lifespan hooks to wait for requests to drain.
64
+
65
+ - Adds InflightTrackerMiddleware
66
+ - Registers a lifespan handler that initializes state and waits up to grace_seconds on shutdown
67
+ """
68
+ app.add_middleware(InflightTrackerMiddleware)
69
+
70
+ g = float(grace_seconds) if grace_seconds is not None else _get_grace_period_seconds()
71
+
72
+ # Preserve any existing lifespan and wrap it so our drain runs on shutdown.
73
+ previous_lifespan = getattr(app.router, "lifespan_context", None)
74
+
75
+ @asynccontextmanager
76
+ async def _lifespan(a: FastAPI): # noqa: ANN202
77
+ # Startup: initialize inflight counter
78
+ a.state._inflight_requests = 0
79
+ if previous_lifespan is not None:
80
+ async with previous_lifespan(a):
81
+ yield
82
+ else:
83
+ yield
84
+ # Shutdown: wait for in-flight requests to drain (up to grace period)
85
+ await _wait_for_drain(a, g)
86
+
87
+ app.router.lifespan_context = _lifespan
@@ -16,14 +16,20 @@ class RateLimitStore(Protocol):
16
16
  class InMemoryRateLimitStore:
17
17
  def __init__(self, limit: int = 120):
18
18
  self.limit = limit
19
- self._buckets: dict[tuple[str, int], int] = {}
19
+ # Track per-key rolling windows: key -> (count, window_start_epoch)
20
+ self._state: dict[str, tuple[int, float]] = {}
20
21
 
21
22
  def incr(self, key: str, window: int) -> Tuple[int, int, int]:
22
- now = int(time.time())
23
- win = now - (now % window)
24
- count = self._buckets.get((key, win), 0) + 1
25
- self._buckets[(key, win)] = count
26
- reset = win + window
23
+ now = time.time()
24
+ count, window_start = self._state.get(key, (0, now))
25
+ # If outside the rolling window, reset
26
+ if now >= window_start + window:
27
+ count = 1
28
+ window_start = now
29
+ else:
30
+ count += 1
31
+ self._state[key] = (count, window_start)
32
+ reset = int(window_start + window)
27
33
  return count, self.limit, reset
28
34
 
29
35
 
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+
6
+ from fastapi import Request
7
+ from starlette.types import ASGIApp, Receive, Scope, Send
8
+
9
+ from svc_infra.api.fastapi.middleware.errors.handlers import problem_response
10
+ from svc_infra.app.env import pick
11
+
12
+
13
+ def _env_int(name: str, default: int) -> int:
14
+ v = os.getenv(name)
15
+ if v is None:
16
+ return default
17
+ try:
18
+ return int(v)
19
+ except Exception:
20
+ return default
21
+
22
+
23
+ REQUEST_BODY_TIMEOUT_SECONDS: int = pick(
24
+ prod=_env_int("REQUEST_BODY_TIMEOUT_SECONDS", 15),
25
+ nonprod=_env_int("REQUEST_BODY_TIMEOUT_SECONDS", 30),
26
+ )
27
+ REQUEST_TIMEOUT_SECONDS: int = pick(
28
+ prod=_env_int("REQUEST_TIMEOUT_SECONDS", 30),
29
+ nonprod=_env_int("REQUEST_TIMEOUT_SECONDS", 15),
30
+ )
31
+
32
+
33
+ class HandlerTimeoutMiddleware:
34
+ """
35
+ Caps total handler execution time. If exceeded, returns 504 Problem+JSON.
36
+ """
37
+
38
+ def __init__(self, app: ASGIApp, timeout_seconds: int | None = None) -> None:
39
+ self.app = app
40
+ self.timeout_seconds = timeout_seconds or REQUEST_TIMEOUT_SECONDS
41
+
42
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
43
+ if scope.get("type") != "http":
44
+ await self.app(scope, receive, send)
45
+ return
46
+
47
+ async def _call_next() -> None:
48
+ await self.app(scope, receive, send)
49
+
50
+ try:
51
+ await asyncio.wait_for(_call_next(), timeout=self.timeout_seconds)
52
+ except asyncio.TimeoutError:
53
+ # Build a minimal Request to extract headers and URL for trace info
54
+ request = Request(scope, receive=receive)
55
+ trace_id = None
56
+ for h in ("x-request-id", "x-correlation-id", "x-trace-id"):
57
+ v = request.headers.get(h)
58
+ if v:
59
+ trace_id = v
60
+ break
61
+ resp = problem_response(
62
+ status=504,
63
+ title="Gateway Timeout",
64
+ detail="The request took too long to complete.",
65
+ code="GATEWAY_TIMEOUT",
66
+ instance=str(request.url),
67
+ trace_id=trace_id,
68
+ )
69
+ await resp(scope, receive, send)
70
+
71
+
72
+ class BodyReadTimeoutMiddleware:
73
+ """
74
+ Enforces a timeout while reading the request body to mitigate slowloris.
75
+ If body read does not make progress within the timeout, returns 408 Problem+JSON.
76
+ """
77
+
78
+ def __init__(self, app: ASGIApp, timeout_seconds: int | None = None) -> None:
79
+ self.app = app
80
+ self.timeout_seconds = timeout_seconds or REQUEST_BODY_TIMEOUT_SECONDS
81
+
82
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
83
+ if scope.get("type") != "http":
84
+ await self.app(scope, receive, send)
85
+ return
86
+
87
+ # Strategy: greedily drain the incoming request body here while enforcing
88
+ # per-receive timeout, then replay it to the downstream app from a buffer.
89
+ # This ensures we can detect slowloris-style uploads even if the app only
90
+ # reads the body later (after the server has finished buffering).
91
+ buffered = bytearray()
92
+
93
+ try:
94
+ while True:
95
+ message = await asyncio.wait_for(receive(), timeout=self.timeout_seconds)
96
+
97
+ mtype = message.get("type")
98
+ if mtype == "http.request":
99
+ chunk = message.get("body", b"") or b""
100
+ if chunk:
101
+ buffered.extend(chunk)
102
+ # Stop when server indicates no more body
103
+ if not message.get("more_body", False):
104
+ break
105
+ # else: continue reading remaining chunks with timeout
106
+ continue
107
+
108
+ if mtype == "http.disconnect": # client disconnected mid-upload
109
+ # Treat as end of body for the purposes of replay; downstream
110
+ # will see an empty body. No timeout response needed here.
111
+ break
112
+ # Ignore other message types and continue
113
+ except asyncio.TimeoutError:
114
+ # Timed out while waiting for the next body chunk → return 408
115
+ request = Request(scope, receive=receive)
116
+ trace_id = None
117
+ for h in ("x-request-id", "x-correlation-id", "x-trace-id"):
118
+ v = request.headers.get(h)
119
+ if v:
120
+ trace_id = v
121
+ break
122
+ resp = problem_response(
123
+ status=408,
124
+ title="Request Timeout",
125
+ detail="Timed out while reading request body.",
126
+ code="REQUEST_TIMEOUT",
127
+ instance=str(request.url),
128
+ trace_id=trace_id,
129
+ )
130
+ await resp(scope, receive, send)
131
+ return
132
+
133
+ # Replay the drained body to the app as a single http.request message.
134
+ sent = False
135
+
136
+ async def _replay_receive() -> dict:
137
+ nonlocal sent
138
+ if not sent:
139
+ sent = True
140
+ return {"type": "http.request", "body": bytes(buffered), "more_body": False}
141
+ # Subsequent calls return an empty terminal body event
142
+ return {"type": "http.request", "body": b"", "more_body": False}
143
+
144
+ await self.app(scope, _replay_receive, send)
@@ -14,9 +14,14 @@ from svc_infra.api.fastapi.docs.landing import CardSpec, DocTargets, render_inde
14
14
  from svc_infra.api.fastapi.docs.scoped import DOC_SCOPES
15
15
  from svc_infra.api.fastapi.middleware.errors.catchall import CatchAllExceptionMiddleware
16
16
  from svc_infra.api.fastapi.middleware.errors.handlers import register_error_handlers
17
+ from svc_infra.api.fastapi.middleware.graceful_shutdown import install_graceful_shutdown
17
18
  from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
18
19
  from svc_infra.api.fastapi.middleware.ratelimit import SimpleRateLimitMiddleware
19
20
  from svc_infra.api.fastapi.middleware.request_id import RequestIdMiddleware
21
+ from svc_infra.api.fastapi.middleware.timeout import (
22
+ BodyReadTimeoutMiddleware,
23
+ HandlerTimeoutMiddleware,
24
+ )
20
25
  from svc_infra.api.fastapi.openapi.models import APIVersionSpec, ServiceInfo
21
26
  from svc_infra.api.fastapi.openapi.mutators import setup_mutators
22
27
  from svc_infra.api.fastapi.openapi.pipeline import apply_mutators
@@ -79,11 +84,16 @@ def _setup_cors(app: FastAPI, public_cors_origins: list[str] | str | None = None
79
84
 
80
85
  def _setup_middlewares(app: FastAPI):
81
86
  app.add_middleware(RequestIdMiddleware)
87
+ # Timeouts: enforce body read timeout first, then total handler timeout
88
+ app.add_middleware(BodyReadTimeoutMiddleware)
89
+ app.add_middleware(HandlerTimeoutMiddleware)
82
90
  app.add_middleware(CatchAllExceptionMiddleware)
83
91
  app.add_middleware(IdempotencyMiddleware)
84
92
  app.add_middleware(SimpleRateLimitMiddleware)
85
93
  register_error_handlers(app)
86
94
  _add_route_logger(app)
95
+ # Graceful shutdown: track in-flight and wait on shutdown
96
+ install_graceful_shutdown(app)
87
97
 
88
98
 
89
99
  def _coerce_list(value: str | Iterable[str] | None) -> list[str]:
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Optional, Sequence
6
+
7
+ from sqlalchemy import select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from .models import Invoice, InvoiceLine, UsageAggregate, UsageEvent
11
+
12
+
13
+ class AsyncBillingService:
14
+ def __init__(self, session: AsyncSession, tenant_id: str):
15
+ self.session = session
16
+ self.tenant_id = tenant_id
17
+
18
+ async def record_usage(
19
+ self,
20
+ *,
21
+ metric: str,
22
+ amount: int,
23
+ at: datetime,
24
+ idempotency_key: str,
25
+ metadata: dict | None,
26
+ ) -> str:
27
+ if at.tzinfo is None:
28
+ at = at.replace(tzinfo=timezone.utc)
29
+ evt = UsageEvent(
30
+ id=str(uuid.uuid4()),
31
+ tenant_id=self.tenant_id,
32
+ metric=metric,
33
+ amount=amount,
34
+ at_ts=at,
35
+ idempotency_key=idempotency_key,
36
+ metadata_json=metadata or {},
37
+ )
38
+ self.session.add(evt)
39
+ await self.session.flush()
40
+ return evt.id
41
+
42
+ async def aggregate_daily(self, *, metric: str, day_start: datetime) -> int:
43
+ day_start = day_start.replace(
44
+ hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc
45
+ )
46
+ next_day = day_start + timedelta(days=1)
47
+ total = 0
48
+ rows: Sequence[UsageEvent] = (
49
+ (
50
+ await self.session.execute(
51
+ select(UsageEvent).where(
52
+ UsageEvent.tenant_id == self.tenant_id,
53
+ UsageEvent.metric == metric,
54
+ UsageEvent.at_ts >= day_start,
55
+ UsageEvent.at_ts < next_day,
56
+ )
57
+ )
58
+ )
59
+ .scalars()
60
+ .all()
61
+ )
62
+ for r in rows:
63
+ total += int(r.amount)
64
+
65
+ agg = (
66
+ await self.session.execute(
67
+ select(UsageAggregate).where(
68
+ UsageAggregate.tenant_id == self.tenant_id,
69
+ UsageAggregate.metric == metric,
70
+ UsageAggregate.period_start == day_start,
71
+ UsageAggregate.granularity == "day",
72
+ )
73
+ )
74
+ ).scalar_one_or_none()
75
+ if agg:
76
+ agg.total = total
77
+ else:
78
+ self.session.add(
79
+ UsageAggregate(
80
+ id=str(uuid.uuid4()),
81
+ tenant_id=self.tenant_id,
82
+ metric=metric,
83
+ period_start=day_start,
84
+ granularity="day",
85
+ total=total,
86
+ )
87
+ )
88
+ return total
89
+
90
+ async def list_daily_aggregates(
91
+ self, *, metric: str, date_from: Optional[datetime], date_to: Optional[datetime]
92
+ ) -> list[UsageAggregate]:
93
+ q = select(UsageAggregate).where(
94
+ UsageAggregate.tenant_id == self.tenant_id,
95
+ UsageAggregate.metric == metric,
96
+ UsageAggregate.granularity == "day",
97
+ )
98
+ if date_from is not None:
99
+ q = q.where(UsageAggregate.period_start >= date_from)
100
+ if date_to is not None:
101
+ q = q.where(UsageAggregate.period_start < date_to)
102
+ rows: list[UsageAggregate] = (await self.session.execute(q)).scalars().all()
103
+ return rows
104
+
105
+ async def generate_monthly_invoice(
106
+ self, *, period_start: datetime, period_end: datetime, currency: str
107
+ ) -> str:
108
+ total = 0
109
+ aggs: Sequence[UsageAggregate] = (
110
+ (
111
+ await self.session.execute(
112
+ select(UsageAggregate).where(
113
+ UsageAggregate.tenant_id == self.tenant_id,
114
+ UsageAggregate.period_start >= period_start,
115
+ UsageAggregate.period_start < period_end,
116
+ UsageAggregate.granularity == "day",
117
+ )
118
+ )
119
+ )
120
+ .scalars()
121
+ .all()
122
+ )
123
+ for r in aggs:
124
+ total += int(r.total)
125
+
126
+ inv = Invoice(
127
+ id=str(uuid.uuid4()),
128
+ tenant_id=self.tenant_id,
129
+ period_start=period_start,
130
+ period_end=period_end,
131
+ status="created",
132
+ total_amount=total,
133
+ currency=currency,
134
+ )
135
+ self.session.add(inv)
136
+ await self.session.flush()
137
+
138
+ line = InvoiceLine(
139
+ id=str(uuid.uuid4()),
140
+ invoice_id=inv.id,
141
+ price_id=None,
142
+ metric=None,
143
+ quantity=1,
144
+ amount=total,
145
+ )
146
+ self.session.add(line)
147
+ return inv.id