svc-infra 0.1.599__tar.gz → 0.1.601__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 (281) hide show
  1. {svc_infra-0.1.599 → svc_infra-0.1.601}/PKG-INFO +1 -1
  2. {svc_infra-0.1.599 → svc_infra-0.1.601}/pyproject.toml +2 -1
  3. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/gaurd.py +2 -2
  4. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/add.py +32 -13
  5. svc_infra-0.1.601/src/svc_infra/api/fastapi/db/sql/crud_router.py +292 -0
  6. svc_infra-0.1.601/src/svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
  7. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/ratelimit.py +41 -1
  8. svc_infra-0.1.601/src/svc_infra/api/fastapi/tenancy/add.py +19 -0
  9. svc_infra-0.1.601/src/svc_infra/api/fastapi/tenancy/context.py +112 -0
  10. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/__init__.py +2 -0
  11. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/__init__.py +2 -0
  12. svc_infra-0.1.601/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +82 -0
  13. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/repository.py +46 -9
  14. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/resource.py +5 -0
  15. svc_infra-0.1.601/src/svc_infra/db/sql/tenant.py +79 -0
  16. svc_infra-0.1.599/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -148
  17. svc_infra-0.1.599/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -66
  18. {svc_infra-0.1.599 → svc_infra-0.1.601}/README.md +0 -0
  19. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/__init__.py +0 -0
  20. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/README.md +0 -0
  21. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/__init__.py +0 -0
  22. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/alembic.py +0 -0
  23. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/models.py +0 -0
  24. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
  25. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
  26. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/base.py +0 -0
  27. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/registry.py +0 -0
  28. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
  29. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/schemas.py +0 -0
  30. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/service.py +0 -0
  31. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/settings.py +0 -0
  32. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/__init__.py +0 -0
  33. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/__init__.py +0 -0
  34. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  35. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
  36. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
  37. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
  38. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
  39. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/add.py +0 -0
  40. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
  41. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
  42. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
  43. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
  44. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
  45. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
  46. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
  47. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
  48. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
  49. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
  50. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
  51. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
  52. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
  53. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
  54. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/security.py +0 -0
  55. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
  56. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
  57. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/state.py +0 -0
  58. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
  59. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/add.py +0 -0
  60. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  61. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/http.py +0 -0
  62. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
  63. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
  64. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
  65. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
  66. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
  67. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
  68. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
  69. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
  70. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
  71. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
  72. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
  73. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
  74. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
  75. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
  76. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
  77. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
  78. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/public.py +0 -0
  79. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/router.py +0 -0
  80. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
  81. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dx.py +0 -0
  82. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/ease.py +0 -0
  83. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
  84. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
  85. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
  86. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
  87. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  88. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
  89. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  90. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  91. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  92. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
  93. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
  94. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
  95. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
  96. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
  97. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
  98. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
  99. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
  100. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
  101. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
  102. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
  103. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
  104. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
  105. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
  106. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
  107. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/pagination.py +0 -0
  108. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
  109. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
  110. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
  111. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
  112. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/user.py +0 -0
  113. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  114. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  115. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/setup.py +0 -0
  116. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/README.md +0 -0
  117. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/__init__.py +0 -0
  118. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/env.py +0 -0
  119. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/__init__.py +0 -0
  120. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/add.py +0 -0
  121. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/filter.py +0 -0
  122. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/formats.py +0 -0
  123. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/root.py +0 -0
  124. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/README.md +0 -0
  125. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/__init__.py +0 -0
  126. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/backend.py +0 -0
  127. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/decorators.py +0 -0
  128. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/demo.py +0 -0
  129. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/keys.py +0 -0
  130. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/recache.py +0 -0
  131. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/resources.py +0 -0
  132. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/tags.py +0 -0
  133. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/ttl.py +0 -0
  134. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/utils.py +0 -0
  135. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/__main__.py +0 -0
  136. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
  137. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
  138. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
  139. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
  140. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
  141. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
  142. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
  143. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
  144. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
  145. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/help.py +0 -0
  146. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
  147. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
  148. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
  149. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
  150. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/__init__.py +0 -0
  151. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/runner.py +0 -0
  152. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  153. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/__init__.py +0 -0
  154. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/crud_schema.py +0 -0
  155. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/inbox.py +0 -0
  156. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/__init__.py +0 -0
  157. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/base.py +0 -0
  158. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/constants.py +0 -0
  159. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/core.py +0 -0
  160. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/indexes.py +0 -0
  161. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/management.py +0 -0
  162. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/README.md +0 -0
  163. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
  164. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/client.py +0 -0
  165. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
  166. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
  167. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
  168. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
  169. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
  170. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/repository.py +0 -0
  171. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/resource.py +0 -0
  172. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/scaffold.py +0 -0
  173. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service.py +0 -0
  174. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
  175. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/types.py +0 -0
  176. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/utils.py +0 -0
  177. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/outbox.py +0 -0
  178. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/README.md +0 -0
  179. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/__init__.py +0 -0
  180. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/apikey.py +0 -0
  181. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/authref.py +0 -0
  182. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/base.py +0 -0
  183. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/constants.py +0 -0
  184. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/core.py +0 -0
  185. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/management.py +0 -0
  186. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/scaffold.py +0 -0
  187. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/service.py +0 -0
  188. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
  189. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/__init__.py +0 -0
  190. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
  191. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
  192. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  193. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
  194. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  195. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
  196. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
  197. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
  198. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
  199. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
  200. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/types.py +0 -0
  201. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq.py +0 -0
  202. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
  203. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/utils.py +0 -0
  204. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/versioning.py +0 -0
  205. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/utils.py +0 -0
  206. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
  207. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
  208. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/easy.py +0 -0
  209. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/loader.py +0 -0
  210. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/queue.py +0 -0
  211. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/redis_queue.py +0 -0
  212. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/scheduler.py +0 -0
  213. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/worker.py +0 -0
  214. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/mcp/__init__.py +0 -0
  215. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  216. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/README.md +0 -0
  217. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/__init__.py +0 -0
  218. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/add.py +0 -0
  219. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/cloud_dash.py +0 -0
  220. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/__init__.py +0 -0
  221. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/asgi.py +0 -0
  222. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/base.py +0 -0
  223. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/http.py +0 -0
  224. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
  225. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics.py +0 -0
  226. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/__init__.py +0 -0
  227. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
  228. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
  229. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
  230. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
  231. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
  232. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
  233. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
  234. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
  235. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
  236. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
  237. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
  238. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
  239. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
  240. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
  241. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
  242. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
  243. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
  244. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/settings.py +0 -0
  245. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
  246. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
  247. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
  248. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
  249. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
  250. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
  251. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
  252. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
  253. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
  254. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
  255. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
  256. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
  257. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
  258. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
  259. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
  260. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
  261. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/py.typed +0 -0
  262. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/add.py +0 -0
  263. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/audit.py +0 -0
  264. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/audit_service.py +0 -0
  265. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/headers.py +0 -0
  266. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/hibp.py +0 -0
  267. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/jwt_rotation.py +0 -0
  268. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/lockout.py +0 -0
  269. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/models.py +0 -0
  270. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/org_invites.py +0 -0
  271. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/passwords.py +0 -0
  272. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/permissions.py +0 -0
  273. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/session.py +0 -0
  274. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/signed_cookies.py +0 -0
  275. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/utils.py +0 -0
  276. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/__init__.py +0 -0
  277. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/add.py +0 -0
  278. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/fastapi.py +0 -0
  279. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/router.py +0 -0
  280. {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/service.py +0 -0
  281. {svc_infra-0.1.599 → svc_infra-0.1.601}/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.599
3
+ Version: 0.1.601
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.599"
3
+ version = "0.1.601"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -126,6 +126,7 @@ markers = [
126
126
  "concurrency: Idempotency and concurrency control tests",
127
127
  "jobs: Background jobs and scheduling tests",
128
128
  "webhooks: Webhooks framework tests",
129
+ "tenancy: Tenancy isolation and enforcement tests",
129
130
  ]
130
131
  filterwarnings = [
131
132
  "ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
@@ -12,6 +12,7 @@ from fastapi_users.password import PasswordHelper
12
12
  from svc_infra.api.fastapi.auth._cookies import compute_cookie_params
13
13
  from svc_infra.api.fastapi.auth.policy import AuthPolicy, DefaultAuthPolicy
14
14
  from svc_infra.api.fastapi.auth.settings import get_auth_settings
15
+ from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
15
16
  from svc_infra.api.fastapi.dual.public import public_router
16
17
 
17
18
  _pwd = PasswordHelper()
@@ -66,19 +67,18 @@ def auth_session_router(
66
67
  router = public_router()
67
68
  policy = auth_policy or DefaultAuthPolicy(get_auth_settings())
68
69
 
69
- from svc_infra.api.fastapi.db.sql import SqlSessionDep
70
70
  from svc_infra.security.lockout import get_lockout_status, record_attempt
71
71
 
72
72
  @router.post("/login", name="auth:jwt.login")
73
73
  async def login(
74
74
  request: Request,
75
+ session: SqlSessionDep,
75
76
  username: str = Form(...),
76
77
  password: str = Form(...),
77
78
  scope: str = Form(""),
78
79
  client_id: str | None = Form(None),
79
80
  client_secret: str | None = Form(None),
80
81
  user_manager=Depends(fapi.get_user_manager),
81
- session: SqlSessionDep = Depends(),
82
82
  ):
83
83
  strategy = auth_backend.get_strategy()
84
84
  email = username.strip().lower()
@@ -10,7 +10,7 @@ from svc_infra.db.sql.management import make_crud_schemas
10
10
  from svc_infra.db.sql.repository import SqlRepository
11
11
  from svc_infra.db.sql.resource import SqlResource
12
12
 
13
- from .crud_router import make_crud_router_plus_sql
13
+ from .crud_router import make_crud_router_plus_sql, make_tenant_crud_router_plus_sql
14
14
  from .health import _make_db_health_router
15
15
  from .session import dispose_session, initialize_session
16
16
 
@@ -37,18 +37,37 @@ def add_sql_resources(app: FastAPI, resources: Sequence[SqlResource]) -> None:
37
37
  update_name=r.update_name,
38
38
  )
39
39
 
40
- router = make_crud_router_plus_sql(
41
- model=r.model,
42
- service=svc,
43
- read_schema=Read,
44
- create_schema=Create,
45
- update_schema=Update,
46
- prefix=r.prefix,
47
- tags=r.tags,
48
- search_fields=r.search_fields,
49
- default_ordering=r.ordering_default,
50
- allowed_order_fields=r.allowed_order_fields,
51
- )
40
+ if r.tenant_field:
41
+ # wrap service factory/instance through tenant router
42
+ def _factory():
43
+ return svc
44
+
45
+ router = make_tenant_crud_router_plus_sql(
46
+ model=r.model,
47
+ service_factory=_factory,
48
+ read_schema=Read,
49
+ create_schema=Create,
50
+ update_schema=Update,
51
+ prefix=r.prefix,
52
+ tenant_field=r.tenant_field,
53
+ tags=r.tags,
54
+ search_fields=r.search_fields,
55
+ default_ordering=r.ordering_default,
56
+ allowed_order_fields=r.allowed_order_fields,
57
+ )
58
+ else:
59
+ router = make_crud_router_plus_sql(
60
+ model=r.model,
61
+ service=svc,
62
+ read_schema=Read,
63
+ create_schema=Create,
64
+ update_schema=Update,
65
+ prefix=r.prefix,
66
+ tags=r.tags,
67
+ search_fields=r.search_fields,
68
+ default_ordering=r.ordering_default,
69
+ allowed_order_fields=r.allowed_order_fields,
70
+ )
52
71
  app.include_router(router)
53
72
 
54
73
 
@@ -0,0 +1,292 @@
1
+ from typing import Annotated, Any, Optional, Sequence, Type, TypeVar, cast
2
+
3
+ from fastapi import APIRouter, Body, Depends, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from svc_infra.api.fastapi.db.http import (
7
+ LimitOffsetParams,
8
+ OrderParams,
9
+ Page,
10
+ SearchParams,
11
+ build_order_by,
12
+ dep_limit_offset,
13
+ dep_order,
14
+ dep_search,
15
+ )
16
+ from svc_infra.api.fastapi.dual.public import public_router
17
+ from svc_infra.db.sql.service import SqlService
18
+ from svc_infra.db.sql.tenant import TenantSqlService
19
+
20
+ from ...tenancy.context import TenantId
21
+ from .session import SqlSessionDep
22
+
23
+ CreateModel = TypeVar("CreateModel", bound=BaseModel)
24
+ ReadModel = TypeVar("ReadModel", bound=BaseModel)
25
+ UpdateModel = TypeVar("UpdateModel", bound=BaseModel)
26
+
27
+
28
+ def make_crud_router_plus_sql(
29
+ *,
30
+ model: type[Any],
31
+ service: SqlService,
32
+ read_schema: Type[ReadModel],
33
+ create_schema: Type[CreateModel],
34
+ update_schema: Type[UpdateModel],
35
+ prefix: str,
36
+ tags: list[str] | None = None,
37
+ search_fields: Optional[Sequence[str]] = None,
38
+ default_ordering: Optional[str] = None,
39
+ allowed_order_fields: Optional[list[str]] = None,
40
+ mount_under_db_prefix: bool = True,
41
+ ) -> APIRouter:
42
+ router_prefix = ("/_sql" + prefix) if mount_under_db_prefix else prefix
43
+ router = public_router(
44
+ prefix=router_prefix,
45
+ tags=tags or [prefix.strip("/")],
46
+ redirect_slashes=False,
47
+ )
48
+
49
+ def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
50
+ if not order_spec:
51
+ return []
52
+ pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
53
+ fields: list[str] = []
54
+ for p in pieces:
55
+ name = p[1:] if p.startswith("-") else p
56
+ if allowed_order_fields and name not in (allowed_order_fields or []):
57
+ continue
58
+ fields.append(p)
59
+ return fields
60
+
61
+ # -------- LIST --------
62
+ @router.get(
63
+ "",
64
+ response_model=cast(Any, Page[Any]),
65
+ description=f"List items of type {model.__name__}",
66
+ )
67
+ async def list_items(
68
+ lp: Annotated[LimitOffsetParams, Depends(dep_limit_offset)],
69
+ op: Annotated[OrderParams, Depends(dep_order)],
70
+ sp: Annotated[SearchParams, Depends(dep_search)],
71
+ session: SqlSessionDep, # type: ignore[name-defined]
72
+ ):
73
+ order_spec = op.order_by or default_ordering
74
+ order_fields = _parse_ordering_to_fields(order_spec)
75
+ order_by = build_order_by(model, order_fields)
76
+
77
+ if sp.q:
78
+ fields = [
79
+ f.strip()
80
+ for f in (sp.fields or (",".join(search_fields or []) or "")).split(",")
81
+ if f.strip()
82
+ ]
83
+ items = await service.search(
84
+ session, q=sp.q, fields=fields, limit=lp.limit, offset=lp.offset, order_by=order_by
85
+ )
86
+ total = await service.count_filtered(session, q=sp.q, fields=fields)
87
+ else:
88
+ items = await service.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
89
+ total = await service.count(session)
90
+ return Page[read_schema].from_items(
91
+ total=total, items=items, limit=lp.limit, offset=lp.offset
92
+ )
93
+
94
+ # -------- GET by id --------
95
+ @router.get(
96
+ "/{item_id}",
97
+ response_model=cast(Any, Any),
98
+ description=f"Get item of type {model.__name__}",
99
+ )
100
+ async def get_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
101
+ row = await service.get(session, item_id)
102
+ if not row:
103
+ raise HTTPException(404, "Not found")
104
+ return row
105
+
106
+ # -------- CREATE --------
107
+ @router.post(
108
+ "",
109
+ response_model=cast(Any, Any),
110
+ status_code=201,
111
+ description=f"Create item of type {model.__name__}",
112
+ )
113
+ async def create_item(
114
+ session: SqlSessionDep, # type: ignore[name-defined]
115
+ payload: Any = Body(...),
116
+ ):
117
+ if isinstance(payload, BaseModel):
118
+ data = payload.model_dump(exclude_unset=True)
119
+ elif isinstance(payload, dict):
120
+ data = payload
121
+ else:
122
+ raise HTTPException(422, "invalid_payload")
123
+ return await service.create(session, data)
124
+
125
+ # -------- UPDATE --------
126
+ @router.patch(
127
+ "/{item_id}",
128
+ response_model=cast(Any, Any),
129
+ description=f"Update item of type {model.__name__}",
130
+ )
131
+ async def update_item(
132
+ item_id: Any,
133
+ session: SqlSessionDep, # type: ignore[name-defined]
134
+ payload: Any = Body(...),
135
+ ):
136
+ if isinstance(payload, BaseModel):
137
+ data = payload.model_dump(exclude_unset=True)
138
+ elif isinstance(payload, dict):
139
+ data = payload
140
+ else:
141
+ raise HTTPException(422, "invalid_payload")
142
+ row = await service.update(session, item_id, data)
143
+ if not row:
144
+ raise HTTPException(404, "Not found")
145
+ return row
146
+
147
+ # -------- DELETE --------
148
+ @router.delete(
149
+ "/{item_id}", status_code=204, description=f"Delete item of type {model.__name__}"
150
+ )
151
+ async def delete_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
152
+ ok = await service.delete(session, item_id)
153
+ if not ok:
154
+ raise HTTPException(404, "Not found")
155
+ return
156
+
157
+ return router
158
+
159
+
160
+ def make_tenant_crud_router_plus_sql(
161
+ *,
162
+ model: type[Any],
163
+ service_factory: callable, # factory that returns a SqlService (will be wrapped)
164
+ read_schema: Type[ReadModel],
165
+ create_schema: Type[CreateModel],
166
+ update_schema: Type[UpdateModel],
167
+ prefix: str,
168
+ tenant_field: str = "tenant_id",
169
+ tags: list[str] | None = None,
170
+ search_fields: Optional[Sequence[str]] = None,
171
+ default_ordering: Optional[str] = None,
172
+ allowed_order_fields: Optional[list[str]] = None,
173
+ mount_under_db_prefix: bool = True,
174
+ ) -> APIRouter:
175
+ """Like make_crud_router_plus_sql, but requires TenantId and scopes all operations."""
176
+ router_prefix = ("/_sql" + prefix) if mount_under_db_prefix else prefix
177
+ router = public_router(
178
+ prefix=router_prefix,
179
+ tags=tags or [prefix.strip("/")],
180
+ redirect_slashes=False,
181
+ )
182
+
183
+ def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
184
+ if not order_spec:
185
+ return []
186
+ pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
187
+ fields: list[str] = []
188
+ for p in pieces:
189
+ name = p[1:] if p.startswith("-") else p
190
+ if allowed_order_fields and name not in (allowed_order_fields or []):
191
+ continue
192
+ fields.append(p)
193
+ return fields
194
+
195
+ # create per-request service with tenant scoping
196
+ async def _svc(session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
197
+ base = service_factory # consumer-provided factory or instance
198
+ svc = base # assume already a SqlService by default
199
+ if callable(base):
200
+ svc = base # the consumer likely closed over repo
201
+ # if callable returns a service, call it now
202
+ try:
203
+ svc = base() # type: ignore[misc]
204
+ except TypeError:
205
+ svc = base # already instance
206
+ if not isinstance(svc, TenantSqlService):
207
+ svc = TenantSqlService(getattr(svc, "repo", svc), tenant_id=tenant_id, tenant_field=tenant_field) # type: ignore[arg-type]
208
+ return svc # type: ignore[return-value]
209
+
210
+ @router.get("", response_model=cast(Any, Page[Any]))
211
+ async def list_items(
212
+ lp: Annotated[LimitOffsetParams, Depends(dep_limit_offset)],
213
+ op: Annotated[OrderParams, Depends(dep_order)],
214
+ sp: Annotated[SearchParams, Depends(dep_search)],
215
+ session: SqlSessionDep, # type: ignore[name-defined]
216
+ tenant_id: TenantId,
217
+ ):
218
+ svc = await _svc(session, tenant_id)
219
+ order_spec = op.order_by or default_ordering
220
+ order_fields = _parse_ordering_to_fields(order_spec)
221
+ order_by = build_order_by(model, order_fields)
222
+ if sp.q:
223
+ fields = [
224
+ f.strip()
225
+ for f in (sp.fields or (",".join(search_fields or []) or "")).split(",")
226
+ if f.strip()
227
+ ]
228
+ items = await svc.search(
229
+ session, q=sp.q, fields=fields, limit=lp.limit, offset=lp.offset, order_by=order_by
230
+ )
231
+ total = await svc.count_filtered(session, q=sp.q, fields=fields)
232
+ else:
233
+ items = await svc.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
234
+ total = await svc.count(session)
235
+ return Page[read_schema].from_items(
236
+ total=total, items=items, limit=lp.limit, offset=lp.offset
237
+ )
238
+
239
+ @router.get("/{item_id}", response_model=cast(Any, Any))
240
+ async def get_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
241
+ svc = await _svc(session, tenant_id)
242
+ row = await svc.get(session, item_id)
243
+ if not row:
244
+ raise HTTPException(404, "Not found")
245
+ return row
246
+
247
+ @router.post("", response_model=cast(Any, Any), status_code=201)
248
+ async def create_item(
249
+ session: SqlSessionDep, # type: ignore[name-defined]
250
+ tenant_id: TenantId,
251
+ payload: Any = Body(...),
252
+ ):
253
+ svc = await _svc(session, tenant_id)
254
+ if isinstance(payload, BaseModel):
255
+ data = payload.model_dump(exclude_unset=True)
256
+ elif isinstance(payload, dict):
257
+ data = payload
258
+ else:
259
+ raise HTTPException(422, "invalid_payload")
260
+ return await svc.create(session, data)
261
+
262
+ @router.patch("/{item_id}", response_model=cast(Any, Any))
263
+ async def update_item(
264
+ item_id: Any,
265
+ session: SqlSessionDep, # type: ignore[name-defined]
266
+ tenant_id: TenantId,
267
+ payload: Any = Body(...),
268
+ ):
269
+ svc = await _svc(session, tenant_id)
270
+ if isinstance(payload, BaseModel):
271
+ data = payload.model_dump(exclude_unset=True)
272
+ elif isinstance(payload, dict):
273
+ data = payload
274
+ else:
275
+ raise HTTPException(422, "invalid_payload")
276
+ row = await svc.update(session, item_id, data)
277
+ if not row:
278
+ raise HTTPException(404, "Not found")
279
+ return row
280
+
281
+ @router.delete("/{item_id}", status_code=204)
282
+ async def delete_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
283
+ svc = await _svc(session, tenant_id)
284
+ ok = await svc.delete(session, item_id)
285
+ if not ok:
286
+ raise HTTPException(404, "Not found")
287
+ return
288
+
289
+ return router
290
+
291
+
292
+ __all__ = ["make_crud_router_plus_sql", "make_tenant_crud_router_plus_sql"]
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Callable, Optional
5
+
6
+ from fastapi import HTTPException
7
+ from starlette.requests import Request
8
+
9
+ from svc_infra.api.fastapi.middleware.ratelimit_store import InMemoryRateLimitStore, RateLimitStore
10
+
11
+ try:
12
+ from svc_infra.api.fastapi.tenancy.context import resolve_tenant_id as _resolve_tenant_id
13
+ except Exception: # pragma: no cover - minimal builds
14
+ _resolve_tenant_id = None # type: ignore
15
+ from svc_infra.obs.metrics import emit_rate_limited
16
+
17
+
18
+ class RateLimiter:
19
+ def __init__(
20
+ self,
21
+ *,
22
+ limit: int,
23
+ window: int = 60,
24
+ key_fn: Callable = lambda r: "global",
25
+ limit_resolver: Optional[Callable[[Request, Optional[str]], Optional[int]]] = None,
26
+ scope_by_tenant: bool = False,
27
+ store: RateLimitStore | None = None,
28
+ ):
29
+ self.limit = limit
30
+ self.window = window
31
+ self.key_fn = key_fn
32
+ self._limit_resolver = limit_resolver
33
+ self.scope_by_tenant = scope_by_tenant
34
+ self.store = store or InMemoryRateLimitStore(limit=limit)
35
+
36
+ async def __call__(self, request: Request):
37
+ # Try resolving tenant when asked
38
+ tenant_id = None
39
+ if self.scope_by_tenant or self._limit_resolver:
40
+ try:
41
+ if _resolve_tenant_id is not None:
42
+ tenant_id = await _resolve_tenant_id(request)
43
+ except Exception:
44
+ tenant_id = None
45
+
46
+ key = self.key_fn(request)
47
+ if self.scope_by_tenant and tenant_id:
48
+ key = f"{key}:tenant:{tenant_id}"
49
+
50
+ eff_limit = self.limit
51
+ if self._limit_resolver:
52
+ try:
53
+ v = self._limit_resolver(request, tenant_id)
54
+ eff_limit = int(v) if v is not None else self.limit
55
+ except Exception:
56
+ eff_limit = self.limit
57
+
58
+ count, store_limit, reset = self.store.incr(str(key), self.window)
59
+ if count > eff_limit:
60
+ retry = max(0, reset - int(time.time()))
61
+ try:
62
+ emit_rate_limited(str(key), eff_limit, retry)
63
+ except Exception:
64
+ pass
65
+ raise HTTPException(
66
+ status_code=429, detail="Rate limit exceeded", headers={"Retry-After": str(retry)}
67
+ )
68
+
69
+
70
+ __all__ = ["RateLimiter"]
71
+
72
+
73
+ def rate_limiter(
74
+ *,
75
+ limit: int,
76
+ window: int = 60,
77
+ key_fn: Callable = lambda r: "global",
78
+ limit_resolver: Optional[Callable[[Request, Optional[str]], Optional[int]]] = None,
79
+ scope_by_tenant: bool = False,
80
+ store: RateLimitStore | None = None,
81
+ ):
82
+ store_ = store or InMemoryRateLimitStore(limit=limit)
83
+
84
+ async def dep(request: Request):
85
+ tenant_id = None
86
+ if scope_by_tenant or limit_resolver:
87
+ try:
88
+ if _resolve_tenant_id is not None:
89
+ tenant_id = await _resolve_tenant_id(request)
90
+ except Exception:
91
+ tenant_id = None
92
+
93
+ key = key_fn(request)
94
+ if scope_by_tenant and tenant_id:
95
+ key = f"{key}:tenant:{tenant_id}"
96
+
97
+ eff_limit = limit
98
+ if limit_resolver:
99
+ try:
100
+ v = limit_resolver(request, tenant_id)
101
+ eff_limit = int(v) if v is not None else limit
102
+ except Exception:
103
+ eff_limit = limit
104
+
105
+ count, _store_limit, reset = store_.incr(str(key), window)
106
+ if count > eff_limit:
107
+ retry = max(0, reset - int(time.time()))
108
+ try:
109
+ emit_rate_limited(str(key), eff_limit, retry)
110
+ except Exception:
111
+ pass
112
+ raise HTTPException(
113
+ status_code=429, detail="Rate limit exceeded", headers={"Retry-After": str(retry)}
114
+ )
115
+
116
+ return dep
@@ -7,6 +7,12 @@ from svc_infra.obs.metrics import emit_rate_limited
7
7
 
8
8
  from .ratelimit_store import InMemoryRateLimitStore, RateLimitStore
9
9
 
10
+ try:
11
+ # Optional import: tenancy may not be enabled in all apps
12
+ from svc_infra.api.fastapi.tenancy.context import resolve_tenant_id as _resolve_tenant_id
13
+ except Exception: # pragma: no cover - fallback for minimal builds
14
+ _resolve_tenant_id = None # type: ignore
15
+
10
16
 
11
17
  class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
12
18
  def __init__(
@@ -15,18 +21,52 @@ class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
15
21
  limit: int = 120,
16
22
  window: int = 60,
17
23
  key_fn=None,
24
+ *,
25
+ # When provided, dynamically computes a limit for the current request (e.g. per-tenant quotas)
26
+ # Signature: (request: Request, tenant_id: Optional[str]) -> int | None
27
+ limit_resolver=None,
28
+ # If True, automatically scopes the bucket key by tenant id when available
29
+ scope_by_tenant: bool = False,
18
30
  store: RateLimitStore | None = None,
19
31
  ):
20
32
  super().__init__(app)
21
33
  self.limit, self.window = limit, window
22
34
  self.key_fn = key_fn or (lambda r: r.headers.get("X-API-Key") or r.client.host)
35
+ self._limit_resolver = limit_resolver
36
+ self.scope_by_tenant = scope_by_tenant
23
37
  self.store = store or InMemoryRateLimitStore(limit=limit)
24
38
 
25
39
  async def dispatch(self, request, call_next):
40
+ # Resolve tenant when possible
41
+ tenant_id = None
42
+ if self.scope_by_tenant or self._limit_resolver:
43
+ try:
44
+ if _resolve_tenant_id is not None:
45
+ tenant_id = await _resolve_tenant_id(request)
46
+ except Exception:
47
+ tenant_id = None
48
+
26
49
  key = self.key_fn(request)
50
+ if self.scope_by_tenant and tenant_id:
51
+ key = f"{key}:tenant:{tenant_id}"
52
+
53
+ # Allow dynamic limit overrides
54
+ eff_limit = self.limit
55
+ if self._limit_resolver:
56
+ try:
57
+ v = self._limit_resolver(request, tenant_id)
58
+ eff_limit = int(v) if v is not None else self.limit
59
+ except Exception:
60
+ eff_limit = self.limit
61
+
27
62
  now = int(time.time())
28
63
  # Increment counter in store
29
- count, limit, reset = self.store.incr(str(key), self.window)
64
+ # Update store limit if it differs; stores capture configured limit internally
65
+ # For in-memory store, we can temporarily adjust per-request by swapping a new store instance
66
+ # but to keep API simple, we reuse store and clamp by eff_limit below.
67
+ count, store_limit, reset = self.store.incr(str(key), self.window)
68
+ # Enforce the effective limit selected for this request
69
+ limit = eff_limit
30
70
  remaining = max(0, limit - count)
31
71
 
32
72
  if remaining < 0: # defensive clamp
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from .context import set_tenant_resolver
8
+
9
+
10
+ def add_tenancy(app: FastAPI, *, resolver: Optional[Callable[..., Any]] = None) -> None:
11
+ """Wire tenancy resolver for the application.
12
+
13
+ Provide a resolver(request, identity, header) -> Optional[str] to override
14
+ the default resolution. Pass None to clear a previous override.
15
+ """
16
+ set_tenant_resolver(resolver)
17
+
18
+
19
+ __all__ = ["add_tenancy"]