svc-infra 0.1.604__tar.gz → 0.1.606__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 (298) hide show
  1. {svc_infra-0.1.604 → svc_infra-0.1.606}/PKG-INFO +2 -1
  2. {svc_infra-0.1.604 → svc_infra-0.1.606}/README.md +1 -0
  3. {svc_infra-0.1.604 → svc_infra-0.1.606}/pyproject.toml +2 -1
  4. svc_infra-0.1.606/src/svc_infra/api/fastapi/docs/add.py +160 -0
  5. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/mutators.py +111 -0
  6. svc_infra-0.1.606/src/svc_infra/billing/__init__.py +23 -0
  7. svc_infra-0.1.606/src/svc_infra/billing/models.py +131 -0
  8. svc_infra-0.1.606/src/svc_infra/billing/service.py +115 -0
  9. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/__init__.py +8 -0
  10. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/__init__.py +4 -0
  11. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +28 -1
  12. svc_infra-0.1.606/src/svc_infra/cli/cmds/dx/__init__.py +12 -0
  13. svc_infra-0.1.606/src/svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  14. svc_infra-0.1.606/src/svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  15. svc_infra-0.1.606/src/svc_infra/dx/changelog.py +74 -0
  16. svc_infra-0.1.606/src/svc_infra/dx/checks.py +67 -0
  17. svc_infra-0.1.606/src/svc_infra/py.typed +0 -0
  18. svc_infra-0.1.604/src/svc_infra/api/fastapi/docs/add.py +0 -82
  19. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/__init__.py +0 -0
  20. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/README.md +0 -0
  21. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/__init__.py +0 -0
  22. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/alembic.py +0 -0
  23. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/models.py +0 -0
  24. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
  25. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
  26. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/provider/base.py +0 -0
  27. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/provider/registry.py +0 -0
  28. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
  29. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/schemas.py +0 -0
  30. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/service.py +0 -0
  31. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/apf_payments/settings.py +0 -0
  32. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/__init__.py +0 -0
  33. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/__init__.py +0 -0
  34. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  35. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
  36. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
  37. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
  38. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
  39. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/add.py +0 -0
  40. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
  41. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
  42. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
  43. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
  44. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
  45. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
  46. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
  47. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
  48. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
  49. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
  50. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
  51. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
  52. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
  53. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
  54. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
  55. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/security.py +0 -0
  56. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
  57. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
  58. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/auth/state.py +0 -0
  59. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
  60. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/cache/add.py +0 -0
  61. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  62. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/http.py +0 -0
  63. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
  64. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
  65. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
  66. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
  67. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
  68. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
  69. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
  70. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
  71. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
  72. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
  73. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
  74. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
  75. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
  76. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
  77. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
  78. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
  79. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
  80. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
  81. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
  82. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/public.py +0 -0
  83. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/router.py +0 -0
  84. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
  85. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/dx.py +0 -0
  86. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/ease.py +0 -0
  87. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
  88. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
  89. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
  90. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
  91. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  92. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
  93. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  94. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  95. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  96. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
  97. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
  98. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
  99. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
  100. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
  101. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
  102. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
  103. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
  104. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
  105. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
  106. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
  107. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
  108. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
  109. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
  110. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
  111. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/ops/add.py +0 -0
  112. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/pagination.py +0 -0
  113. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
  114. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
  115. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
  116. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
  117. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/paths/user.py +0 -0
  118. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  119. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  120. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/setup.py +0 -0
  121. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/tenancy/add.py +0 -0
  122. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/api/fastapi/tenancy/context.py +0 -0
  123. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/README.md +0 -0
  124. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/__init__.py +0 -0
  125. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/env.py +0 -0
  126. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/logging/__init__.py +0 -0
  127. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/logging/add.py +0 -0
  128. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/logging/filter.py +0 -0
  129. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/logging/formats.py +0 -0
  130. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/app/root.py +0 -0
  131. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/README.md +0 -0
  132. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/__init__.py +0 -0
  133. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/backend.py +0 -0
  134. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/decorators.py +0 -0
  135. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/demo.py +0 -0
  136. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/keys.py +0 -0
  137. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/recache.py +0 -0
  138. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/resources.py +0 -0
  139. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/tags.py +0 -0
  140. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/ttl.py +0 -0
  141. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cache/utils.py +0 -0
  142. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/__main__.py +0 -0
  143. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
  144. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
  145. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
  146. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
  147. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
  148. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
  149. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
  150. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +0 -0
  151. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
  152. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/help.py +0 -0
  153. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
  154. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
  155. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
  156. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
  157. {svc_infra-0.1.604/src/svc_infra/cli/foundation → svc_infra-0.1.606/src/svc_infra/cli/cmds/sdk}/__init__.py +0 -0
  158. {svc_infra-0.1.604/src/svc_infra/db → svc_infra-0.1.606/src/svc_infra/cli/foundation}/__init__.py +0 -0
  159. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/foundation/runner.py +0 -0
  160. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  161. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/data/add.py +0 -0
  162. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/data/backup.py +0 -0
  163. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/data/erasure.py +0 -0
  164. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/data/fixtures.py +0 -0
  165. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/data/retention.py +0 -0
  166. {svc_infra-0.1.604/src/svc_infra/db/nosql/mongo → svc_infra-0.1.606/src/svc_infra/db}/__init__.py +0 -0
  167. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/crud_schema.py +0 -0
  168. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/inbox.py +0 -0
  169. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/__init__.py +0 -0
  170. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/base.py +0 -0
  171. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/constants.py +0 -0
  172. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/core.py +0 -0
  173. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/indexes.py +0 -0
  174. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/management.py +0 -0
  175. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/README.md +0 -0
  176. {svc_infra-0.1.604/src/svc_infra/db/nosql/mongo/templates → svc_infra-0.1.606/src/svc_infra/db/nosql/mongo}/__init__.py +0 -0
  177. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/client.py +0 -0
  178. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
  179. {svc_infra-0.1.604/src/svc_infra/db/sql/templates/models_schemas → svc_infra-0.1.606/src/svc_infra/db/nosql/mongo/templates}/__init__.py +0 -0
  180. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
  181. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
  182. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
  183. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/repository.py +0 -0
  184. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/resource.py +0 -0
  185. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/scaffold.py +0 -0
  186. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/service.py +0 -0
  187. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
  188. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/types.py +0 -0
  189. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/nosql/utils.py +0 -0
  190. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/outbox.py +0 -0
  191. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/README.md +0 -0
  192. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/__init__.py +0 -0
  193. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/apikey.py +0 -0
  194. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/authref.py +0 -0
  195. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/base.py +0 -0
  196. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/constants.py +0 -0
  197. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/core.py +0 -0
  198. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/management.py +0 -0
  199. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/repository.py +0 -0
  200. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/resource.py +0 -0
  201. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/scaffold.py +0 -0
  202. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/service.py +0 -0
  203. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
  204. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/__init__.py +0 -0
  205. {svc_infra-0.1.604/src/svc_infra/db/sql/templates/setup → svc_infra-0.1.606/src/svc_infra/db/sql/templates/models_schemas}/__init__.py +0 -0
  206. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
  207. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  208. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
  209. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  210. {svc_infra-0.1.604/src/svc_infra/mcp → svc_infra-0.1.606/src/svc_infra/db/sql/templates/setup}/__init__.py +0 -0
  211. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
  212. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
  213. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
  214. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
  215. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/tenant.py +0 -0
  216. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/types.py +0 -0
  217. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/uniq.py +0 -0
  218. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
  219. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/utils.py +0 -0
  220. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/sql/versioning.py +0 -0
  221. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/db/utils.py +0 -0
  222. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/dx/add.py +0 -0
  223. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
  224. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
  225. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/easy.py +0 -0
  226. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/loader.py +0 -0
  227. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/queue.py +0 -0
  228. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/redis_queue.py +0 -0
  229. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/scheduler.py +0 -0
  230. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/jobs/worker.py +0 -0
  231. {svc_infra-0.1.604/src/svc_infra/obs/providers → svc_infra-0.1.606/src/svc_infra/mcp}/__init__.py +0 -0
  232. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  233. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/README.md +0 -0
  234. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/__init__.py +0 -0
  235. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/add.py +0 -0
  236. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/cloud_dash.py +0 -0
  237. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/grafana/dashboards/http-overview.json +0 -0
  238. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics/__init__.py +0 -0
  239. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics/asgi.py +0 -0
  240. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics/base.py +0 -0
  241. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics/http.py +0 -0
  242. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
  243. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/metrics.py +0 -0
  244. {svc_infra-0.1.604/src/svc_infra/obs/providers/compose_cloud → svc_infra-0.1.606/src/svc_infra/obs/providers}/__init__.py +0 -0
  245. {svc_infra-0.1.604/src/svc_infra/obs/providers/compose_cloud/templates → svc_infra-0.1.606/src/svc_infra/obs/providers/compose_cloud}/__init__.py +0 -0
  246. {svc_infra-0.1.604/src/svc_infra/obs/providers/grafana → svc_infra-0.1.606/src/svc_infra/obs/providers/compose_cloud/templates}/__init__.py +0 -0
  247. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
  248. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
  249. {svc_infra-0.1.604/src/svc_infra/obs/providers/grafana/dashboards → svc_infra-0.1.606/src/svc_infra/obs/providers/grafana}/__init__.py +0 -0
  250. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
  251. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
  252. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
  253. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
  254. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
  255. {svc_infra-0.1.604/src/svc_infra/obs/providers/grafana/templates → svc_infra-0.1.606/src/svc_infra/obs/providers/grafana/dashboards}/__init__.py +0 -0
  256. {svc_infra-0.1.604/src/svc_infra/obs/providers/grafana/templates/provisioning → svc_infra-0.1.606/src/svc_infra/obs/providers/grafana/templates}/__init__.py +0 -0
  257. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
  258. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
  259. {svc_infra-0.1.604/src/svc_infra/obs/templates/sidecars → svc_infra-0.1.606/src/svc_infra/obs/providers/grafana/templates/provisioning}/__init__.py +0 -0
  260. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
  261. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
  262. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/settings.py +0 -0
  263. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
  264. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
  265. {svc_infra-0.1.604/src/svc_infra/obs/templates/sidecars/compose → svc_infra-0.1.606/src/svc_infra/obs/templates/sidecars}/__init__.py +0 -0
  266. {svc_infra-0.1.604/src/svc_infra/obs/templates/sidecars/fly → svc_infra-0.1.606/src/svc_infra/obs/templates/sidecars/compose}/__init__.py +0 -0
  267. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
  268. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
  269. {svc_infra-0.1.604/src/svc_infra/obs/templates/sidecars/k8s → svc_infra-0.1.606/src/svc_infra/obs/templates/sidecars/fly}/__init__.py +0 -0
  270. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
  271. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
  272. {svc_infra-0.1.604/src/svc_infra/obs/templates/sidecars/railway → svc_infra-0.1.606/src/svc_infra/obs/templates/sidecars/k8s}/__init__.py +0 -0
  273. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
  274. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
  275. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
  276. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
  277. /svc_infra-0.1.604/src/svc_infra/py.typed → /svc_infra-0.1.606/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
  278. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
  279. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/add.py +0 -0
  280. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/audit.py +0 -0
  281. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/audit_service.py +0 -0
  282. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/headers.py +0 -0
  283. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/hibp.py +0 -0
  284. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/jwt_rotation.py +0 -0
  285. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/lockout.py +0 -0
  286. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/models.py +0 -0
  287. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/org_invites.py +0 -0
  288. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/passwords.py +0 -0
  289. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/permissions.py +0 -0
  290. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/session.py +0 -0
  291. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/security/signed_cookies.py +0 -0
  292. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/utils.py +0 -0
  293. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/webhooks/__init__.py +0 -0
  294. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/webhooks/add.py +0 -0
  295. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/webhooks/fastapi.py +0 -0
  296. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/webhooks/router.py +0 -0
  297. {svc_infra-0.1.604 → svc_infra-0.1.606}/src/svc_infra/webhooks/service.py +0 -0
  298. {svc_infra-0.1.604 → svc_infra-0.1.606}/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.604
3
+ Version: 0.1.606
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
@@ -94,6 +94,7 @@ svc-infra packages the shared building blocks we use to ship production FastAPI
94
94
  | Ops | Probes, breaker, SLOs & dashboards | [SLOs & Ops](docs/ops.md) |
95
95
  | Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
96
96
  | Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
97
+ | Contributing | Dev setup and quality gates | [Contributing guide](docs/contributing.md) |
97
98
  | Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](docs/data-lifecycle.md) |
98
99
 
99
100
  ## Minimal FastAPI bootstrap
@@ -18,6 +18,7 @@ svc-infra packages the shared building blocks we use to ship production FastAPI
18
18
  | Ops | Probes, breaker, SLOs & dashboards | [SLOs & Ops](docs/ops.md) |
19
19
  | Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
20
20
  | Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
21
+ | Contributing | Dev setup and quality gates | [Contributing guide](docs/contributing.md) |
21
22
  | Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](docs/data-lifecycle.md) |
22
23
 
23
24
  ## Minimal FastAPI bootstrap
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "svc-infra"
3
- version = "0.1.604"
3
+ version = "0.1.606"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -129,6 +129,7 @@ markers = [
129
129
  "tenancy: Tenancy isolation and enforcement tests",
130
130
  "data_lifecycle: Data lifecycle (fixtures, retention, erasure, backups)",
131
131
  "ops: SLOs & Ops tests (probes, breaker, instrumentation)",
132
+ "dx: Developer experience and quality gates tests",
132
133
  ]
133
134
  filterwarnings = [
134
135
  "ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
9
+ from fastapi.responses import HTMLResponse, JSONResponse
10
+
11
+ from .landing import CardSpec, DocTargets, render_index_html
12
+ from .scoped import DOC_SCOPES
13
+
14
+
15
+ def add_docs(
16
+ app: FastAPI,
17
+ *,
18
+ redoc_url: str = "/redoc",
19
+ swagger_url: str = "/docs",
20
+ openapi_url: str = "/openapi.json",
21
+ export_openapi_to: Optional[str] = None,
22
+ # Landing page options
23
+ landing_url: str = "/",
24
+ include_landing: bool = True,
25
+ ) -> None:
26
+ """Enable docs endpoints and optionally export OpenAPI schema to disk on startup.
27
+
28
+ We mount docs and OpenAPI routes explicitly so this works even when configured post-init.
29
+ """
30
+
31
+ # OpenAPI JSON route
32
+ async def openapi_handler() -> JSONResponse: # noqa: ANN201
33
+ return JSONResponse(app.openapi())
34
+
35
+ app.add_api_route(openapi_url, openapi_handler, methods=["GET"], include_in_schema=False)
36
+
37
+ # Swagger UI route
38
+ async def swagger_ui(request: Request) -> HTMLResponse: # noqa: ANN201
39
+ resp = get_swagger_ui_html(openapi_url=openapi_url, title="API Docs")
40
+ theme = request.query_params.get("theme")
41
+ if theme == "dark":
42
+ return _with_dark_mode(resp)
43
+ return resp
44
+
45
+ app.add_api_route(swagger_url, swagger_ui, methods=["GET"], include_in_schema=False)
46
+
47
+ # Redoc route
48
+ async def redoc_ui(request: Request) -> HTMLResponse: # noqa: ANN201
49
+ resp = get_redoc_html(openapi_url=openapi_url, title="API ReDoc")
50
+ theme = request.query_params.get("theme")
51
+ if theme == "dark":
52
+ return _with_dark_mode(resp)
53
+ return resp
54
+
55
+ app.add_api_route(redoc_url, redoc_ui, methods=["GET"], include_in_schema=False)
56
+
57
+ # Optional export to disk on startup
58
+ if export_openapi_to:
59
+ export_path = Path(export_openapi_to)
60
+
61
+ async def _export_docs() -> None:
62
+ # Startup export
63
+ spec = app.openapi()
64
+ export_path.parent.mkdir(parents=True, exist_ok=True)
65
+ export_path.write_text(json.dumps(spec, indent=2))
66
+
67
+ app.add_event_handler("startup", _export_docs)
68
+
69
+ # Optional landing page with the same look/feel as setup_service_api
70
+ if include_landing:
71
+ # Avoid path collision; if landing_url is already taken for GET, fallback to "/_docs"
72
+ existing_paths = {
73
+ (getattr(r, "path", None) or getattr(r, "path_format", None))
74
+ for r in getattr(app, "routes", [])
75
+ if getattr(r, "methods", None) and "GET" in r.methods
76
+ }
77
+ landing_path = landing_url or "/"
78
+ if landing_path in existing_paths:
79
+ landing_path = "/_docs"
80
+
81
+ async def _landing() -> HTMLResponse: # noqa: ANN201
82
+ cards: list[CardSpec] = []
83
+ # Root docs card using the provided paths
84
+ cards.append(
85
+ CardSpec(
86
+ tag="",
87
+ docs=DocTargets(swagger=swagger_url, redoc=redoc_url, openapi_json=openapi_url),
88
+ )
89
+ )
90
+ # Scoped docs (if any were registered via add_prefixed_docs)
91
+ for scope, swagger, redoc, openapi_json, _title in DOC_SCOPES:
92
+ cards.append(
93
+ CardSpec(
94
+ tag=scope.strip("/"),
95
+ docs=DocTargets(swagger=swagger, redoc=redoc, openapi_json=openapi_json),
96
+ )
97
+ )
98
+ html = render_index_html(
99
+ service_name=app.title or "API", release=app.version or "", cards=cards
100
+ )
101
+ return HTMLResponse(html)
102
+
103
+ app.add_api_route(landing_path, _landing, methods=["GET"], include_in_schema=False)
104
+
105
+
106
+ def _with_dark_mode(resp: HTMLResponse) -> HTMLResponse:
107
+ """Return a copy of the HTMLResponse with a minimal dark-theme CSS injected.
108
+
109
+ We avoid depending on custom Swagger/ReDoc builds; this works by inlining a small CSS
110
+ block and toggling a `.dark` class on the body element.
111
+ """
112
+ try:
113
+ body = resp.body.decode("utf-8", errors="ignore")
114
+ except Exception: # pragma: no cover - very unlikely
115
+ return resp
116
+
117
+ css = _DARK_CSS
118
+ if "</head>" in body:
119
+ body = body.replace("</head>", f"<style>\n{css}\n</style></head>", 1)
120
+ # add class to body to allow stronger selectors
121
+ body = body.replace("<body>", '<body class="dark">', 1)
122
+ return HTMLResponse(content=body, status_code=resp.status_code, headers=dict(resp.headers))
123
+
124
+
125
+ _DARK_CSS = """
126
+ /* Minimal dark mode override for Swagger/ReDoc */
127
+ @media (prefers-color-scheme: dark) { :root { color-scheme: dark; } }
128
+ html.dark, body.dark { background: #0b0e14; color: #e0e6f1; }
129
+ #swagger, .redoc-wrap { background: transparent; }
130
+ a { color: #62aef7; }
131
+ """
132
+
133
+
134
+ def add_sdk_generation_stub(
135
+ app: FastAPI,
136
+ *,
137
+ on_generate: Optional[callable] = None,
138
+ openapi_path: str = "/openapi.json",
139
+ ) -> None:
140
+ """Hook to add an SDK generation stub.
141
+
142
+ Provide `on_generate()` to run generation (e.g., openapi-generator). This is a stub only; we
143
+ don't ship a hard dependency. If `on_generate` is provided, we expose `/_docs/generate-sdk`.
144
+ """
145
+ from svc_infra.api.fastapi.dual.public import public_router
146
+
147
+ if not on_generate:
148
+ return
149
+
150
+ router = public_router(prefix="/_docs", include_in_schema=False)
151
+
152
+ @router.post("/generate-sdk")
153
+ async def _generate() -> dict: # noqa: ANN201
154
+ on_generate()
155
+ return {"status": "ok"}
156
+
157
+ app.include_router(router)
158
+
159
+
160
+ __all__ = ["add_docs", "add_sdk_generation_stub"]
@@ -1102,6 +1102,114 @@ def ensure_success_examples_mutator():
1102
1102
  return m
1103
1103
 
1104
1104
 
1105
+ # --- NEW: attach minimal x-codeSamples for common operations ---
1106
+ def attach_code_samples_mutator():
1107
+ """Attach minimal curl/httpie x-codeSamples for each operation if missing.
1108
+
1109
+ We avoid templating parameters; samples illustrate method and path only.
1110
+ """
1111
+
1112
+ def m(schema: dict) -> dict:
1113
+ schema = dict(schema)
1114
+ servers = schema.get("servers") or [{"url": ""}]
1115
+ base = servers[0].get("url") or ""
1116
+
1117
+ for path, method, op in _iter_ops(schema):
1118
+ # Don't override existing samples
1119
+ if isinstance(op.get("x-codeSamples"), list) and op["x-codeSamples"]:
1120
+ continue
1121
+ url = f"{base}{path}"
1122
+ method_up = method.upper()
1123
+ samples = [
1124
+ {
1125
+ "lang": "bash",
1126
+ "label": "curl",
1127
+ "source": f"curl -X {method_up} '{url}'",
1128
+ },
1129
+ {
1130
+ "lang": "bash",
1131
+ "label": "httpie",
1132
+ "source": f"http {method_up} '{url}'",
1133
+ },
1134
+ ]
1135
+ op["x-codeSamples"] = samples
1136
+ return schema
1137
+
1138
+ return m
1139
+
1140
+
1141
+ # --- NEW: ensure Problem+JSON examples exist for standard error responses ---
1142
+ def ensure_problem_examples_mutator():
1143
+ """Add example objects for 4xx/5xx responses using Problem schema if absent."""
1144
+
1145
+ try:
1146
+ # Internal helper with sensible defaults
1147
+ from .conventions import _problem_example # type: ignore
1148
+ except Exception: # pragma: no cover - fallback
1149
+
1150
+ def _problem_example(**kw): # type: ignore
1151
+ base = {
1152
+ "type": "about:blank",
1153
+ "title": "Error",
1154
+ "status": 500,
1155
+ "detail": "An error occurred.",
1156
+ "instance": "/request/trace",
1157
+ "code": "INTERNAL_ERROR",
1158
+ }
1159
+ base.update(kw)
1160
+ return base
1161
+
1162
+ def m(schema: dict) -> dict:
1163
+ schema = dict(schema)
1164
+ for _, _, op in _iter_ops(schema):
1165
+ resps = op.get("responses") or {}
1166
+ for code, resp in resps.items():
1167
+ if not isinstance(resp, dict):
1168
+ continue
1169
+ try:
1170
+ ic = int(code)
1171
+ except Exception:
1172
+ continue
1173
+ if ic < 400:
1174
+ continue
1175
+ content = resp.setdefault("content", {})
1176
+ # prefer problem+json but also set application/json if present
1177
+ for mt in ("application/problem+json", "application/json"):
1178
+ mt_obj = content.get(mt)
1179
+ if mt_obj is None:
1180
+ # Create a basic media type referencing Problem schema when appropriate
1181
+ if mt == "application/problem+json":
1182
+ mt_obj = {"schema": {"$ref": "#/components/schemas/Problem"}}
1183
+ content[mt] = mt_obj
1184
+ else:
1185
+ continue
1186
+ if not isinstance(mt_obj, dict):
1187
+ continue
1188
+ if "example" in mt_obj or "examples" in mt_obj:
1189
+ continue
1190
+ mt_obj["example"] = _problem_example(status=ic)
1191
+ return schema
1192
+
1193
+ return m
1194
+
1195
+
1196
+ # --- NEW: attach default tags from first path segment when missing ---
1197
+ def attach_default_tags_mutator():
1198
+ """If an operation has no tags, tag it by its first path segment."""
1199
+
1200
+ def m(schema: dict) -> dict:
1201
+ schema = dict(schema)
1202
+ for path, _method, op in _iter_ops(schema):
1203
+ tags = op.get("tags")
1204
+ if tags:
1205
+ continue
1206
+ seg = path.strip("/").split("/", 1)[0] or "root"
1207
+ op["tags"] = [seg]
1208
+ return schema
1209
+
1210
+ return m
1211
+
1212
+
1105
1213
  def dedupe_tags_mutator():
1106
1214
  def m(schema: dict) -> dict:
1107
1215
  schema = dict(schema)
@@ -1429,6 +1537,9 @@ def setup_mutators(
1429
1537
  ensure_media_type_schemas_mutator(),
1430
1538
  ensure_examples_for_json_mutator(),
1431
1539
  ensure_success_examples_mutator(),
1540
+ attach_default_tags_mutator(),
1541
+ attach_code_samples_mutator(),
1542
+ ensure_problem_examples_mutator(),
1432
1543
  ensure_media_examples_mutator(),
1433
1544
  scrub_invalid_object_examples_mutator(),
1434
1545
  normalize_no_content_204_mutator(),
@@ -0,0 +1,23 @@
1
+ from .models import (
2
+ Invoice,
3
+ InvoiceLine,
4
+ Plan,
5
+ PlanEntitlement,
6
+ Price,
7
+ Subscription,
8
+ UsageAggregate,
9
+ UsageEvent,
10
+ )
11
+ from .service import BillingService
12
+
13
+ __all__ = [
14
+ "UsageEvent",
15
+ "UsageAggregate",
16
+ "Plan",
17
+ "PlanEntitlement",
18
+ "Subscription",
19
+ "Price",
20
+ "Invoice",
21
+ "InvoiceLine",
22
+ "BillingService",
23
+ ]
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from sqlalchemy import JSON, DateTime, Index, Numeric, String, UniqueConstraint, text
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from svc_infra.db.sql.base import ModelBase
10
+
11
+ TENANT_ID_LEN = 64
12
+
13
+
14
+ class UsageEvent(ModelBase):
15
+ __tablename__ = "billing_usage_events"
16
+
17
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
18
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
19
+ metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
20
+ amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
21
+ at_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
22
+ idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
23
+ metadata_json: Mapped[dict] = mapped_column(JSON, default=dict)
24
+ created_at: Mapped[datetime] = mapped_column(
25
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
26
+ )
27
+
28
+ __table_args__ = (
29
+ UniqueConstraint("tenant_id", "metric", "idempotency_key", name="uq_usage_idem"),
30
+ Index("ix_usage_tenant_metric_ts", "tenant_id", "metric", "at_ts"),
31
+ )
32
+
33
+
34
+ class UsageAggregate(ModelBase):
35
+ __tablename__ = "billing_usage_aggregates"
36
+
37
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
38
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
39
+ metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
40
+ period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
41
+ granularity: Mapped[str] = mapped_column(String(8), nullable=False) # hour|day|month
42
+ total: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
43
+ updated_at: Mapped[datetime] = mapped_column(
44
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
45
+ )
46
+
47
+ __table_args__ = (
48
+ UniqueConstraint("tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"),
49
+ )
50
+
51
+
52
+ class Plan(ModelBase):
53
+ __tablename__ = "billing_plans"
54
+
55
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
56
+ key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
57
+ name: Mapped[str] = mapped_column(String(128), nullable=False)
58
+ description: Mapped[Optional[str]] = mapped_column(String(255))
59
+ created_at: Mapped[datetime] = mapped_column(
60
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
61
+ )
62
+
63
+
64
+ class PlanEntitlement(ModelBase):
65
+ __tablename__ = "billing_plan_entitlements"
66
+
67
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
68
+ plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
69
+ key: Mapped[str] = mapped_column(String(64), nullable=False)
70
+ limit_per_window: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
71
+ window: Mapped[str] = mapped_column(String(8), nullable=False) # day|month
72
+ created_at: Mapped[datetime] = mapped_column(
73
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
74
+ )
75
+
76
+
77
+ class Subscription(ModelBase):
78
+ __tablename__ = "billing_subscriptions"
79
+
80
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
81
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
82
+ plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
83
+ effective_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
84
+ ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
85
+ created_at: Mapped[datetime] = mapped_column(
86
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
87
+ )
88
+
89
+
90
+ class Price(ModelBase):
91
+ __tablename__ = "billing_prices"
92
+
93
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
94
+ key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
95
+ currency: Mapped[str] = mapped_column(String(8), nullable=False)
96
+ unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
97
+ metric: Mapped[Optional[str]] = mapped_column(String(64)) # null for fixed recurring
98
+ recurring_interval: Mapped[Optional[str]] = mapped_column(String(8)) # month|year
99
+ created_at: Mapped[datetime] = mapped_column(
100
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
101
+ )
102
+
103
+
104
+ class Invoice(ModelBase):
105
+ __tablename__ = "billing_invoices"
106
+
107
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
108
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
109
+ period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
110
+ period_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
111
+ status: Mapped[str] = mapped_column(String(16), index=True, nullable=False)
112
+ total_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
113
+ currency: Mapped[str] = mapped_column(String(8), nullable=False)
114
+ provider_invoice_id: Mapped[Optional[str]] = mapped_column(String(128), index=True)
115
+ created_at: Mapped[datetime] = mapped_column(
116
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
117
+ )
118
+
119
+
120
+ class InvoiceLine(ModelBase):
121
+ __tablename__ = "billing_invoice_lines"
122
+
123
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
124
+ invoice_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
125
+ price_id: Mapped[Optional[str]] = mapped_column(String(64), index=True)
126
+ metric: Mapped[Optional[str]] = mapped_column(String(64))
127
+ quantity: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
128
+ amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
129
+ created_at: Mapped[datetime] = mapped_column(
130
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
131
+ )
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import Callable, Optional
7
+
8
+ from sqlalchemy import select
9
+ from sqlalchemy.orm import Session
10
+
11
+ from .models import Invoice, InvoiceLine, UsageAggregate, UsageEvent
12
+
13
+ ProviderSyncHook = Callable[[Invoice, list[InvoiceLine]], None]
14
+
15
+
16
+ @dataclass
17
+ class BillingService:
18
+ session: Session
19
+ tenant_id: str
20
+ provider_sync: Optional[ProviderSyncHook] = None
21
+
22
+ def record_usage(
23
+ self, *, metric: str, amount: int, at: datetime, idempotency_key: str, metadata: dict | None
24
+ ) -> str:
25
+ # Ensure UTC
26
+ if at.tzinfo is None:
27
+ at = at.replace(tzinfo=timezone.utc)
28
+ evt = UsageEvent(
29
+ id=str(uuid.uuid4()),
30
+ tenant_id=self.tenant_id,
31
+ metric=metric,
32
+ amount=amount,
33
+ at_ts=at,
34
+ idempotency_key=idempotency_key,
35
+ metadata_json=metadata or {},
36
+ )
37
+ self.session.add(evt)
38
+ self.session.flush()
39
+ return evt.id
40
+
41
+ def aggregate_daily(self, *, metric: str, day_start: datetime) -> None:
42
+ # Compute [day_start, day_start+1d)
43
+ next_day = day_start.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
44
+ total = 0
45
+ rows = self.session.execute(
46
+ select(UsageEvent).where(
47
+ UsageEvent.tenant_id == self.tenant_id,
48
+ UsageEvent.metric == metric,
49
+ UsageEvent.at_ts >= day_start,
50
+ UsageEvent.at_ts < next_day,
51
+ )
52
+ ).scalars()
53
+ for r in rows:
54
+ total += int(r.amount)
55
+ # upsert aggregate
56
+ agg = self.session.execute(
57
+ select(UsageAggregate).where(
58
+ UsageAggregate.tenant_id == self.tenant_id,
59
+ UsageAggregate.metric == metric,
60
+ UsageAggregate.period_start == day_start,
61
+ UsageAggregate.granularity == "day",
62
+ )
63
+ ).scalar_one_or_none()
64
+ if agg:
65
+ agg.total = total
66
+ else:
67
+ self.session.add(
68
+ UsageAggregate(
69
+ id=str(uuid.uuid4()),
70
+ tenant_id=self.tenant_id,
71
+ metric=metric,
72
+ period_start=day_start,
73
+ granularity="day",
74
+ total=total,
75
+ )
76
+ )
77
+
78
+ def generate_monthly_invoice(
79
+ self, *, period_start: datetime, period_end: datetime, currency: str
80
+ ) -> str:
81
+ # Minimal: sum all daily aggregates and produce one line
82
+ total = 0
83
+ rows = self.session.execute(
84
+ select(UsageAggregate).where(
85
+ UsageAggregate.tenant_id == self.tenant_id,
86
+ UsageAggregate.period_start >= period_start,
87
+ UsageAggregate.period_start < period_end,
88
+ UsageAggregate.granularity == "day",
89
+ )
90
+ ).scalars()
91
+ for r in rows:
92
+ total += int(r.total)
93
+ inv = Invoice(
94
+ id=str(uuid.uuid4()),
95
+ tenant_id=self.tenant_id,
96
+ period_start=period_start,
97
+ period_end=period_end,
98
+ status="created",
99
+ total_amount=total,
100
+ currency=currency,
101
+ )
102
+ self.session.add(inv)
103
+ self.session.flush()
104
+ line = InvoiceLine(
105
+ id=str(uuid.uuid4()),
106
+ invoice_id=inv.id,
107
+ price_id=None,
108
+ metric=None,
109
+ quantity=1,
110
+ amount=total,
111
+ )
112
+ self.session.add(line)
113
+ if self.provider_sync:
114
+ self.provider_sync(inv, [line])
115
+ return inv.id
@@ -6,9 +6,11 @@ from svc_infra.cli.cmds import (
6
6
  _HELP,
7
7
  jobs_app,
8
8
  register_alembic,
9
+ register_dx,
9
10
  register_mongo,
10
11
  register_mongo_scaffold,
11
12
  register_obs,
13
+ register_sdk,
12
14
  register_sql_export,
13
15
  register_sql_scaffold,
14
16
  )
@@ -29,9 +31,15 @@ register_mongo_scaffold(app)
29
31
  # -- observability commands ---
30
32
  register_obs(app)
31
33
 
34
+ # -- dx commands ---
35
+ register_dx(app)
36
+
32
37
  # -- jobs commands ---
33
38
  app.add_typer(jobs_app, name="jobs")
34
39
 
40
+ # -- sdk commands ---
41
+ register_sdk(app)
42
+
35
43
 
36
44
  def main():
37
45
  app()
@@ -5,8 +5,10 @@ from svc_infra.cli.cmds.db.nosql.mongo.mongo_scaffold_cmds import (
5
5
  from svc_infra.cli.cmds.db.sql.alembic_cmds import register as register_alembic
6
6
  from svc_infra.cli.cmds.db.sql.sql_export_cmds import register as register_sql_export
7
7
  from svc_infra.cli.cmds.db.sql.sql_scaffold_cmds import register as register_sql_scaffold
8
+ from svc_infra.cli.cmds.dx import register_dx
8
9
  from svc_infra.cli.cmds.jobs.jobs_cmds import app as jobs_app
9
10
  from svc_infra.cli.cmds.obs.obs_cmds import register as register_obs
11
+ from svc_infra.cli.cmds.sdk.sdk_cmds import register as register_sdk
10
12
 
11
13
  from .help import _HELP
12
14
 
@@ -18,5 +20,7 @@ __all__ = [
18
20
  "register_mongo_scaffold",
19
21
  "register_obs",
20
22
  "jobs_app",
23
+ "register_sdk",
24
+ "register_dx",
21
25
  "_HELP",
22
26
  ]
@@ -217,7 +217,34 @@ def _import_callable(path: str):
217
217
  mod_name, _, fn_name = path.partition(":")
218
218
  if not mod_name or not fn_name:
219
219
  raise typer.BadParameter("Expected format 'module.path:callable'")
220
- mod = import_module(mod_name)
220
+ # Back-compat: after moving tests under tests/unit, allow legacy test module
221
+ # dotted paths like 'tests.db.sql.test_sql_seed_cli:my_seed'.
222
+ mod = None
223
+ unit_mod = None
224
+ if mod_name.startswith("tests.db."):
225
+ # Try legacy import first (shim module), then unit module fallback
226
+ try:
227
+ mod = import_module(mod_name)
228
+ except ModuleNotFoundError:
229
+ pass
230
+ unit_name = mod_name.replace("tests.db.", "tests.unit.db.", 1)
231
+ try:
232
+ unit_mod = import_module(unit_name)
233
+ except ModuleNotFoundError:
234
+ unit_mod = None
235
+ # If both exist, unify shared state where applicable
236
+ if mod is not None and unit_mod is not None:
237
+ # Example: tests use a global `called` dict; point legacy to unit
238
+ try:
239
+ if hasattr(unit_mod, "called"):
240
+ setattr(mod, "called", getattr(unit_mod, "called"))
241
+ except Exception:
242
+ pass
243
+ # If legacy mod missing but unit exists, use unit
244
+ if mod is None and unit_mod is not None:
245
+ mod = unit_mod
246
+ else:
247
+ mod = import_module(mod_name)
221
248
  fn = getattr(mod, fn_name, None)
222
249
  if not callable(fn):
223
250
  raise typer.BadParameter(f"Callable '{fn_name}' not found in module '{mod_name}'")