svc-infra 0.1.706__py3-none-any.whl → 1.1.0__py3-none-any.whl

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 (227) hide show
  1. svc_infra/apf_payments/models.py +47 -108
  2. svc_infra/apf_payments/provider/__init__.py +2 -2
  3. svc_infra/apf_payments/provider/aiydan.py +42 -100
  4. svc_infra/apf_payments/provider/base.py +10 -26
  5. svc_infra/apf_payments/provider/registry.py +3 -5
  6. svc_infra/apf_payments/provider/stripe.py +63 -135
  7. svc_infra/apf_payments/schemas.py +82 -90
  8. svc_infra/apf_payments/service.py +40 -86
  9. svc_infra/apf_payments/settings.py +10 -13
  10. svc_infra/api/__init__.py +13 -13
  11. svc_infra/api/fastapi/__init__.py +19 -0
  12. svc_infra/api/fastapi/admin/add.py +13 -18
  13. svc_infra/api/fastapi/apf_payments/router.py +47 -84
  14. svc_infra/api/fastapi/apf_payments/setup.py +7 -13
  15. svc_infra/api/fastapi/auth/__init__.py +1 -1
  16. svc_infra/api/fastapi/auth/_cookies.py +3 -9
  17. svc_infra/api/fastapi/auth/add.py +4 -8
  18. svc_infra/api/fastapi/auth/gaurd.py +9 -26
  19. svc_infra/api/fastapi/auth/mfa/models.py +4 -7
  20. svc_infra/api/fastapi/auth/mfa/pre_auth.py +3 -3
  21. svc_infra/api/fastapi/auth/mfa/router.py +9 -15
  22. svc_infra/api/fastapi/auth/mfa/security.py +3 -5
  23. svc_infra/api/fastapi/auth/mfa/utils.py +3 -2
  24. svc_infra/api/fastapi/auth/mfa/verify.py +2 -9
  25. svc_infra/api/fastapi/auth/providers.py +4 -6
  26. svc_infra/api/fastapi/auth/routers/apikey_router.py +16 -18
  27. svc_infra/api/fastapi/auth/routers/oauth_router.py +37 -85
  28. svc_infra/api/fastapi/auth/routers/session_router.py +3 -6
  29. svc_infra/api/fastapi/auth/security.py +17 -28
  30. svc_infra/api/fastapi/auth/sender.py +1 -3
  31. svc_infra/api/fastapi/auth/settings.py +18 -19
  32. svc_infra/api/fastapi/auth/state.py +6 -7
  33. svc_infra/api/fastapi/auth/ws_security.py +2 -2
  34. svc_infra/api/fastapi/billing/router.py +6 -8
  35. svc_infra/api/fastapi/db/http.py +10 -11
  36. svc_infra/api/fastapi/db/nosql/mongo/add.py +5 -15
  37. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +14 -15
  38. svc_infra/api/fastapi/db/sql/add.py +6 -14
  39. svc_infra/api/fastapi/db/sql/crud_router.py +27 -40
  40. svc_infra/api/fastapi/db/sql/health.py +1 -3
  41. svc_infra/api/fastapi/db/sql/session.py +4 -5
  42. svc_infra/api/fastapi/db/sql/users.py +8 -11
  43. svc_infra/api/fastapi/dependencies/ratelimit.py +4 -6
  44. svc_infra/api/fastapi/docs/add.py +13 -23
  45. svc_infra/api/fastapi/docs/landing.py +6 -8
  46. svc_infra/api/fastapi/docs/scoped.py +34 -42
  47. svc_infra/api/fastapi/dual/dualize.py +1 -1
  48. svc_infra/api/fastapi/dual/protected.py +12 -21
  49. svc_infra/api/fastapi/dual/router.py +14 -31
  50. svc_infra/api/fastapi/ease.py +57 -13
  51. svc_infra/api/fastapi/http/conditional.py +3 -5
  52. svc_infra/api/fastapi/middleware/errors/catchall.py +2 -6
  53. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -4
  54. svc_infra/api/fastapi/middleware/errors/handlers.py +12 -18
  55. svc_infra/api/fastapi/middleware/graceful_shutdown.py +4 -13
  56. svc_infra/api/fastapi/middleware/idempotency.py +11 -16
  57. svc_infra/api/fastapi/middleware/idempotency_store.py +14 -14
  58. svc_infra/api/fastapi/middleware/optimistic_lock.py +5 -8
  59. svc_infra/api/fastapi/middleware/ratelimit.py +8 -8
  60. svc_infra/api/fastapi/middleware/ratelimit_store.py +7 -8
  61. svc_infra/api/fastapi/middleware/request_id.py +1 -3
  62. svc_infra/api/fastapi/middleware/timeout.py +9 -10
  63. svc_infra/api/fastapi/object_router.py +1060 -0
  64. svc_infra/api/fastapi/openapi/apply.py +5 -6
  65. svc_infra/api/fastapi/openapi/conventions.py +4 -4
  66. svc_infra/api/fastapi/openapi/mutators.py +13 -31
  67. svc_infra/api/fastapi/openapi/pipeline.py +2 -2
  68. svc_infra/api/fastapi/openapi/responses.py +4 -6
  69. svc_infra/api/fastapi/openapi/security.py +1 -3
  70. svc_infra/api/fastapi/ops/add.py +7 -9
  71. svc_infra/api/fastapi/pagination.py +25 -37
  72. svc_infra/api/fastapi/routers/__init__.py +16 -38
  73. svc_infra/api/fastapi/setup.py +13 -31
  74. svc_infra/api/fastapi/tenancy/add.py +3 -2
  75. svc_infra/api/fastapi/tenancy/context.py +8 -7
  76. svc_infra/api/fastapi/versioned.py +3 -2
  77. svc_infra/app/env.py +5 -7
  78. svc_infra/app/logging/add.py +2 -1
  79. svc_infra/app/logging/filter.py +1 -1
  80. svc_infra/app/logging/formats.py +3 -2
  81. svc_infra/app/root.py +3 -3
  82. svc_infra/billing/__init__.py +19 -2
  83. svc_infra/billing/async_service.py +27 -7
  84. svc_infra/billing/jobs.py +23 -33
  85. svc_infra/billing/models.py +21 -52
  86. svc_infra/billing/quotas.py +5 -7
  87. svc_infra/billing/schemas.py +4 -6
  88. svc_infra/cache/__init__.py +12 -5
  89. svc_infra/cache/add.py +6 -9
  90. svc_infra/cache/backend.py +6 -5
  91. svc_infra/cache/decorators.py +17 -28
  92. svc_infra/cache/keys.py +2 -2
  93. svc_infra/cache/recache.py +22 -35
  94. svc_infra/cache/resources.py +8 -16
  95. svc_infra/cache/ttl.py +2 -3
  96. svc_infra/cache/utils.py +5 -6
  97. svc_infra/cli/__init__.py +4 -12
  98. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +11 -10
  99. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +6 -9
  100. svc_infra/cli/cmds/db/ops_cmds.py +3 -6
  101. svc_infra/cli/cmds/db/sql/alembic_cmds.py +24 -41
  102. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +9 -17
  103. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +10 -10
  104. svc_infra/cli/cmds/docs/docs_cmds.py +7 -10
  105. svc_infra/cli/cmds/dx/dx_cmds.py +5 -11
  106. svc_infra/cli/cmds/jobs/jobs_cmds.py +2 -7
  107. svc_infra/cli/cmds/obs/obs_cmds.py +4 -7
  108. svc_infra/cli/cmds/sdk/sdk_cmds.py +5 -15
  109. svc_infra/cli/foundation/runner.py +6 -11
  110. svc_infra/cli/foundation/typer_bootstrap.py +1 -2
  111. svc_infra/data/__init__.py +83 -0
  112. svc_infra/data/add.py +5 -5
  113. svc_infra/data/backup.py +8 -10
  114. svc_infra/data/erasure.py +3 -2
  115. svc_infra/data/fixtures.py +3 -3
  116. svc_infra/data/retention.py +8 -13
  117. svc_infra/db/crud_schema.py +9 -8
  118. svc_infra/db/nosql/__init__.py +0 -1
  119. svc_infra/db/nosql/constants.py +1 -1
  120. svc_infra/db/nosql/core.py +7 -14
  121. svc_infra/db/nosql/indexes.py +11 -10
  122. svc_infra/db/nosql/management.py +3 -3
  123. svc_infra/db/nosql/mongo/client.py +3 -3
  124. svc_infra/db/nosql/mongo/settings.py +2 -6
  125. svc_infra/db/nosql/repository.py +27 -28
  126. svc_infra/db/nosql/resource.py +15 -20
  127. svc_infra/db/nosql/scaffold.py +13 -17
  128. svc_infra/db/nosql/service.py +3 -4
  129. svc_infra/db/nosql/service_with_hooks.py +4 -3
  130. svc_infra/db/nosql/types.py +2 -6
  131. svc_infra/db/nosql/utils.py +4 -4
  132. svc_infra/db/ops.py +14 -18
  133. svc_infra/db/outbox.py +15 -18
  134. svc_infra/db/sql/apikey.py +12 -21
  135. svc_infra/db/sql/authref.py +3 -7
  136. svc_infra/db/sql/constants.py +9 -9
  137. svc_infra/db/sql/core.py +11 -11
  138. svc_infra/db/sql/management.py +2 -6
  139. svc_infra/db/sql/repository.py +17 -24
  140. svc_infra/db/sql/resource.py +14 -13
  141. svc_infra/db/sql/scaffold.py +13 -17
  142. svc_infra/db/sql/service.py +7 -16
  143. svc_infra/db/sql/service_with_hooks.py +4 -3
  144. svc_infra/db/sql/tenant.py +6 -14
  145. svc_infra/db/sql/uniq.py +8 -7
  146. svc_infra/db/sql/uniq_hooks.py +14 -19
  147. svc_infra/db/sql/utils.py +24 -53
  148. svc_infra/db/utils.py +3 -3
  149. svc_infra/deploy/__init__.py +8 -15
  150. svc_infra/documents/add.py +7 -8
  151. svc_infra/documents/ease.py +8 -8
  152. svc_infra/documents/models.py +3 -3
  153. svc_infra/documents/storage.py +11 -13
  154. svc_infra/dx/__init__.py +58 -0
  155. svc_infra/dx/add.py +1 -3
  156. svc_infra/dx/changelog.py +2 -2
  157. svc_infra/dx/checks.py +1 -1
  158. svc_infra/health/__init__.py +15 -16
  159. svc_infra/http/client.py +10 -14
  160. svc_infra/jobs/__init__.py +79 -0
  161. svc_infra/jobs/builtins/outbox_processor.py +3 -5
  162. svc_infra/jobs/builtins/webhook_delivery.py +1 -3
  163. svc_infra/jobs/loader.py +4 -5
  164. svc_infra/jobs/queue.py +14 -24
  165. svc_infra/jobs/redis_queue.py +20 -34
  166. svc_infra/jobs/runner.py +7 -11
  167. svc_infra/jobs/scheduler.py +5 -5
  168. svc_infra/jobs/worker.py +1 -1
  169. svc_infra/loaders/base.py +5 -4
  170. svc_infra/loaders/github.py +1 -3
  171. svc_infra/loaders/url.py +3 -9
  172. svc_infra/logging/__init__.py +7 -6
  173. svc_infra/mcp/__init__.py +82 -0
  174. svc_infra/mcp/svc_infra_mcp.py +2 -2
  175. svc_infra/obs/add.py +4 -3
  176. svc_infra/obs/cloud_dash.py +1 -1
  177. svc_infra/obs/metrics/__init__.py +3 -3
  178. svc_infra/obs/metrics/asgi.py +9 -14
  179. svc_infra/obs/metrics/base.py +13 -13
  180. svc_infra/obs/metrics/http.py +5 -9
  181. svc_infra/obs/metrics/sqlalchemy.py +9 -12
  182. svc_infra/obs/metrics.py +3 -3
  183. svc_infra/obs/settings.py +2 -6
  184. svc_infra/resilience/__init__.py +44 -0
  185. svc_infra/resilience/circuit_breaker.py +328 -0
  186. svc_infra/resilience/retry.py +289 -0
  187. svc_infra/security/__init__.py +167 -0
  188. svc_infra/security/add.py +5 -9
  189. svc_infra/security/audit.py +14 -17
  190. svc_infra/security/audit_service.py +9 -9
  191. svc_infra/security/hibp.py +3 -6
  192. svc_infra/security/jwt_rotation.py +7 -10
  193. svc_infra/security/lockout.py +12 -11
  194. svc_infra/security/models.py +37 -46
  195. svc_infra/security/oauth_models.py +8 -8
  196. svc_infra/security/org_invites.py +11 -13
  197. svc_infra/security/passwords.py +4 -6
  198. svc_infra/security/permissions.py +8 -7
  199. svc_infra/security/session.py +6 -7
  200. svc_infra/security/signed_cookies.py +9 -9
  201. svc_infra/storage/add.py +5 -8
  202. svc_infra/storage/backends/local.py +13 -21
  203. svc_infra/storage/backends/memory.py +4 -7
  204. svc_infra/storage/backends/s3.py +17 -36
  205. svc_infra/storage/base.py +2 -2
  206. svc_infra/storage/easy.py +4 -8
  207. svc_infra/storage/settings.py +16 -18
  208. svc_infra/testing/__init__.py +36 -39
  209. svc_infra/utils.py +169 -8
  210. svc_infra/webhooks/__init__.py +1 -1
  211. svc_infra/webhooks/add.py +17 -29
  212. svc_infra/webhooks/encryption.py +2 -2
  213. svc_infra/webhooks/fastapi.py +2 -4
  214. svc_infra/webhooks/router.py +3 -3
  215. svc_infra/webhooks/service.py +5 -6
  216. svc_infra/webhooks/signing.py +5 -5
  217. svc_infra/websocket/add.py +2 -3
  218. svc_infra/websocket/client.py +3 -2
  219. svc_infra/websocket/config.py +6 -18
  220. svc_infra/websocket/manager.py +9 -10
  221. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/METADATA +11 -5
  222. svc_infra-1.1.0.dist-info/RECORD +364 -0
  223. svc_infra/billing/service.py +0 -123
  224. svc_infra-0.1.706.dist-info/RECORD +0 -357
  225. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/LICENSE +0 -0
  226. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
  227. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,16 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from typing import Optional
5
4
 
6
5
  from pydantic import BaseModel, SecretStr
7
6
 
8
7
  STRIPE_KEY = os.getenv("STRIPE_SECRET") or os.getenv("STRIPE_API_KEY")
9
8
  STRIPE_WH = os.getenv("STRIPE_WH_SECRET")
10
9
  PROVIDER = (
11
- os.getenv("APF_PAYMENTS_PROVIDER")
12
- or os.getenv("PAYMENTS_PROVIDER", "stripe")
13
- or "stripe"
10
+ os.getenv("APF_PAYMENTS_PROVIDER") or os.getenv("PAYMENTS_PROVIDER", "stripe") or "stripe"
14
11
  ).lower()
15
12
 
16
13
  AIYDAN_KEY = os.getenv("AIYDAN_API_KEY")
@@ -23,23 +20,23 @@ AIYDAN_WH = os.getenv("AIYDAN_WH_SECRET")
23
20
 
24
21
  class StripeConfig(BaseModel):
25
22
  secret_key: SecretStr
26
- webhook_secret: Optional[SecretStr] = None
23
+ webhook_secret: SecretStr | None = None
27
24
 
28
25
 
29
26
  class AiydanConfig(BaseModel):
30
27
  api_key: SecretStr
31
- client_key: Optional[SecretStr] = None
32
- merchant_account: Optional[str] = None
33
- hmac_key: Optional[SecretStr] = None
34
- base_url: Optional[str] = None
35
- webhook_secret: Optional[SecretStr] = None
28
+ client_key: SecretStr | None = None
29
+ merchant_account: str | None = None
30
+ hmac_key: SecretStr | None = None
31
+ base_url: str | None = None
32
+ webhook_secret: SecretStr | None = None
36
33
 
37
34
 
38
35
  class PaymentsSettings(BaseModel):
39
36
  default_provider: str = PROVIDER
40
37
 
41
38
  # optional multi-tenant/provider map hook can be added later
42
- stripe: Optional[StripeConfig] = (
39
+ stripe: StripeConfig | None = (
43
40
  StripeConfig(
44
41
  secret_key=SecretStr(STRIPE_KEY),
45
42
  webhook_secret=SecretStr(STRIPE_WH) if STRIPE_WH else None,
@@ -47,7 +44,7 @@ class PaymentsSettings(BaseModel):
47
44
  if STRIPE_KEY
48
45
  else None
49
46
  )
50
- aiydan: Optional[AiydanConfig] = (
47
+ aiydan: AiydanConfig | None = (
51
48
  AiydanConfig(
52
49
  api_key=SecretStr(AIYDAN_KEY),
53
50
  client_key=SecretStr(AIYDAN_CLIENT_KEY) if AIYDAN_CLIENT_KEY else None,
@@ -61,7 +58,7 @@ class PaymentsSettings(BaseModel):
61
58
  )
62
59
 
63
60
 
64
- _SETTINGS: Optional[PaymentsSettings] = None
61
+ _SETTINGS: PaymentsSettings | None = None
65
62
 
66
63
 
67
64
  def get_payments_settings() -> PaymentsSettings:
svc_infra/api/__init__.py CHANGED
@@ -7,30 +7,30 @@ from __future__ import annotations
7
7
 
8
8
  # Re-export from fastapi submodule
9
9
  from svc_infra.api.fastapi import (
10
+ APIVersionSpec,
10
11
  # Dual routers
11
12
  DualAPIRouter,
12
- dualize_protected,
13
- dualize_public,
14
- dualize_user,
15
13
  # Service setup
16
14
  ServiceInfo,
17
- APIVersionSpec,
18
- setup_service_api,
19
- easy_service_api,
20
- easy_service_app,
21
- setup_caching,
15
+ add_dependency_health,
16
+ add_health_routes,
22
17
  # Health checks
23
18
  add_startup_probe,
24
- add_health_routes,
25
- add_dependency_health,
26
19
  check_database,
27
20
  check_redis,
28
21
  check_url,
22
+ cursor_window,
23
+ dualize_protected,
24
+ dualize_public,
25
+ dualize_user,
26
+ easy_service_api,
27
+ easy_service_app,
28
+ setup_caching,
29
+ setup_service_api,
30
+ sort_by,
31
+ text_filter,
29
32
  # Pagination
30
33
  use_pagination,
31
- text_filter,
32
- sort_by,
33
- cursor_window,
34
34
  )
35
35
 
36
36
  __all__ = [
@@ -4,6 +4,16 @@ from svc_infra.api.fastapi.dual import (
4
4
  dualize_public,
5
5
  dualize_user,
6
6
  )
7
+ from svc_infra.api.fastapi.object_router import (
8
+ DEFAULT_EXCEPTION_MAP,
9
+ STATUS_TITLES,
10
+ endpoint,
11
+ endpoint_exclude,
12
+ map_exception_to_http,
13
+ router_from_object,
14
+ router_from_object_with_websocket,
15
+ websocket_endpoint,
16
+ )
7
17
  from svc_infra.api.fastapi.openapi.models import APIVersionSpec, ServiceInfo
8
18
  from svc_infra.health import (
9
19
  add_dependency_health,
@@ -43,4 +53,13 @@ __all__ = [
43
53
  "text_filter",
44
54
  "sort_by",
45
55
  "cursor_window",
56
+ # Object Router
57
+ "router_from_object",
58
+ "router_from_object_with_websocket",
59
+ "endpoint",
60
+ "endpoint_exclude",
61
+ "websocket_endpoint",
62
+ "map_exception_to_http",
63
+ "DEFAULT_EXCEPTION_MAP",
64
+ "STATUS_TITLES",
46
65
  ]
@@ -7,9 +7,10 @@ import json
7
7
  import logging
8
8
  import os
9
9
  import time
10
+ from collections.abc import Callable
10
11
  from hashlib import sha256
11
12
  from types import SimpleNamespace
12
- from typing import Any, Callable, Optional, cast
13
+ from typing import Any, cast
13
14
 
14
15
  from fastapi import APIRouter, Depends, HTTPException, Request, Response
15
16
 
@@ -49,18 +50,18 @@ def _verify(token: str, *, secret: str) -> dict:
49
50
  payload = json.loads(body)
50
51
  if int(payload.get("exp", 0)) < int(time.time()):
51
52
  raise ValueError("expired")
52
- return cast(dict[Any, Any], payload)
53
+ return cast("dict[Any, Any]", payload)
53
54
  except Exception as e:
54
55
  raise ValueError("invalid_token") from e
55
56
 
56
57
 
57
- def admin_router(*, dependencies: Optional[list[Any]] = None, **kwargs) -> APIRouter:
58
+ def admin_router(*, dependencies: list[Any] | None = None, **kwargs) -> APIRouter:
58
59
  """Role-gated admin router for coarse access control.
59
60
 
60
61
  Use permission guards inside endpoints for fine-grained control.
61
62
  """
62
63
 
63
- return cast(APIRouter, roles_router("admin", **kwargs))
64
+ return cast("APIRouter", roles_router("admin", **kwargs))
64
65
 
65
66
 
66
67
  def add_admin(
@@ -68,10 +69,10 @@ def add_admin(
68
69
  *,
69
70
  base_path: str = "/admin",
70
71
  enable_impersonation: bool = True,
71
- secret: Optional[str] = None,
72
+ secret: str | None = None,
72
73
  ttl_seconds: int = 15 * 60,
73
74
  cookie_name: str = "impersonation",
74
- impersonation_user_getter: Optional[Callable[[Any, str], Any]] = None,
75
+ impersonation_user_getter: Callable[[Any, str], Any] | None = None,
75
76
  ) -> None:
76
77
  """Wire admin surfaces with sensible defaults.
77
78
 
@@ -98,15 +99,13 @@ def add_admin(
98
99
 
99
100
  r = admin_router(prefix=base_path, tags=["admin"]) # role-gated
100
101
 
101
- async def _default_user_getter(
102
- request: Request, user_id: str, session: SqlSessionDep
103
- ):
102
+ async def _default_user_getter(request: Request, user_id: str, session: SqlSessionDep):
104
103
  try:
105
104
  UserModel, _, _ = get_auth_state()
106
105
  except Exception:
107
106
  # Fallback: simple shim if auth state not configured
108
107
  return SimpleNamespace(id=user_id)
109
- obj = await cast(Any, session).get(UserModel, user_id)
108
+ obj = await cast("Any", session).get(UserModel, user_id)
110
109
  if not obj:
111
110
  raise HTTPException(404, "user_not_found")
112
111
  return obj
@@ -182,9 +181,7 @@ def add_admin(
182
181
  def _compose_override():
183
182
  existing = app.dependency_overrides.get(_current_principal)
184
183
  if existing and getattr(existing, "_is_admin_impersonation_override", False):
185
- dep_provider = getattr(
186
- existing, "_admin_impersonation_base", _current_principal
187
- )
184
+ dep_provider = getattr(existing, "_admin_impersonation_base", _current_principal)
188
185
  else:
189
186
  dep_provider = existing or _current_principal
190
187
 
@@ -214,16 +211,14 @@ def add_admin(
214
211
  )
215
212
  target = await _res if inspect.isawaitable(_res) else _res
216
213
  # Swap user but keep actor for audit if needed
217
- setattr(base, "actor", getattr(base, "user", None))
214
+ base.actor = getattr(base, "user", None) # type: ignore[attr-defined]
218
215
  # If target lacks roles, inherit actor roles to maintain permission checks
219
216
  try:
220
217
  if not getattr(target, "roles", None):
221
- setattr(target, "roles", actor_roles)
218
+ target.roles = actor_roles
222
219
  except Exception:
223
220
  # Best-effort; if target object is immutable, fallback by wrapping
224
- target = SimpleNamespace(
225
- id=getattr(target, "id", target_id), roles=actor_roles
226
- )
221
+ target = SimpleNamespace(id=getattr(target, "id", target_id), roles=actor_roles)
227
222
  base.user = target
228
223
  base.via = "impersonated"
229
224
  return base
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
- from typing import Callable, Literal, Optional, cast
4
+ from collections.abc import Callable
5
+ from typing import Literal, cast
5
6
 
6
7
  from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
7
8
  from starlette.responses import JSONResponse
@@ -72,7 +73,7 @@ _TX_KINDS = {"payment", "refund", "fee", "payout", "capture"}
72
73
  def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "capture"]:
73
74
  if kind not in _TX_KINDS:
74
75
  raise ValueError(f"Unknown ledger kind: {kind!r}")
75
- return cast(Literal["payment", "refund", "fee", "payout", "capture"], kind)
76
+ return cast("Literal['payment', 'refund', 'fee', 'payout', 'capture']", kind)
76
77
 
77
78
 
78
79
  # --- tenant resolution ---
@@ -101,16 +102,16 @@ async def resolve_payments_tenant_id(
101
102
  if inspect.isawaitable(val):
102
103
  val = await val
103
104
  if val:
104
- return cast(str, val)
105
+ return cast("str", val)
105
106
  # if None, continue default flow
106
107
 
107
108
  # 2) Principal (user)
108
109
  if identity and getattr(identity.user or object(), "tenant_id", None):
109
- return cast(str, getattr(identity.user, "tenant_id"))
110
+ return cast("str", identity.user.tenant_id)
110
111
 
111
112
  # 3) Principal (api key)
112
113
  if identity and getattr(identity.api_key or object(), "tenant_id", None):
113
- return cast(str, getattr(identity.api_key, "tenant_id"))
114
+ return cast("str", identity.api_key.tenant_id)
114
115
 
115
116
  # 4) Explicit header argument (tests pass this)
116
117
  if tenant_header:
@@ -119,7 +120,7 @@ async def resolve_payments_tenant_id(
119
120
  # 5) Request state
120
121
  state_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
121
122
  if state_tid:
122
- return cast(str, state_tid)
123
+ return cast("str", state_tid)
123
124
 
124
125
  raise HTTPException(status_code=400, detail="tenant_context_missing")
125
126
 
@@ -140,15 +141,11 @@ async def get_service(
140
141
  else:
141
142
  # allow tests to call without a Request; try identity or fallback
142
143
  if identity and getattr(identity.user or object(), "tenant_id", None):
143
- tid = getattr(identity.user, "tenant_id")
144
- elif identity and getattr(
145
- identity.api_key or object(), "tenant_id", None
146
- ):
147
- tid = getattr(identity.api_key, "tenant_id")
144
+ tid = identity.user.tenant_id
145
+ elif identity and getattr(identity.api_key or object(), "tenant_id", None):
146
+ tid = identity.api_key.tenant_id
148
147
  else:
149
- raise HTTPException(
150
- status_code=400, detail="tenant_context_missing"
151
- )
148
+ raise HTTPException(status_code=400, detail="tenant_context_missing")
152
149
  except HTTPException:
153
150
  # fallback for routes/tests that don't set context; preserve prior default
154
151
  tid = "test_tenant"
@@ -172,9 +169,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
172
169
  dependencies=[Depends(require_idempotency_key)],
173
170
  tags=["Customers"],
174
171
  )
175
- async def upsert_customer(
176
- data: CustomerUpsertIn, svc: PaymentsService = Depends(get_service)
177
- ):
172
+ async def upsert_customer(data: CustomerUpsertIn, svc: PaymentsService = Depends(get_service)):
178
173
  out = await svc.ensure_customer(data)
179
174
  await svc.session.flush()
180
175
  return out
@@ -197,9 +192,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
197
192
  out = await svc.create_intent(user_id=None, data=data)
198
193
  await svc.session.flush()
199
194
  response.headers["Location"] = str(
200
- request.url_for(
201
- "payments_get_intent", provider_intent_id=out.provider_intent_id
202
- )
195
+ request.url_for("payments_get_intent", provider_intent_id=out.provider_intent_id)
203
196
  )
204
197
  return out
205
198
 
@@ -213,9 +206,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
213
206
  dependencies=[Depends(require_idempotency_key)],
214
207
  tags=["Payment Intents"],
215
208
  )
216
- async def confirm_intent(
217
- provider_intent_id: str, svc: PaymentsService = Depends(get_service)
218
- ):
209
+ async def confirm_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
219
210
  out = await svc.confirm_intent(provider_intent_id)
220
211
  await svc.session.flush()
221
212
  return out
@@ -227,9 +218,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
227
218
  dependencies=[Depends(require_idempotency_key)],
228
219
  tags=["Payment Intents"],
229
220
  )
230
- async def cancel_intent(
231
- provider_intent_id: str, svc: PaymentsService = Depends(get_service)
232
- ):
221
+ async def cancel_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
233
222
  out = await svc.cancel_intent(provider_intent_id)
234
223
  await svc.session.flush()
235
224
  return out
@@ -302,7 +291,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
302
291
  provider: str,
303
292
  request: Request,
304
293
  svc: PaymentsService = Depends(get_service),
305
- signature: Optional[str] = Header(None, alias="Stripe-Signature"),
294
+ signature: str | None = Header(None, alias="Stripe-Signature"),
306
295
  ):
307
296
  payload = await request.body()
308
297
  out = await svc.handle_webhook(provider.lower(), signature, payload)
@@ -359,9 +348,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
359
348
  dependencies=[Depends(require_idempotency_key)],
360
349
  tags=["Payment Methods"],
361
350
  )
362
- async def detach_method(
363
- provider_method_id: str, svc: PaymentsService = Depends(get_service)
364
- ):
351
+ async def detach_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
365
352
  out = await svc.detach_payment_method(provider_method_id)
366
353
  await svc.session.flush()
367
354
  return out
@@ -378,9 +365,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
378
365
  customer_provider_id: str,
379
366
  svc: PaymentsService = Depends(get_service),
380
367
  ):
381
- out = await svc.set_default_payment_method(
382
- customer_provider_id, provider_method_id
383
- )
368
+ out = await svc.set_default_payment_method(customer_provider_id, provider_method_id)
384
369
  await svc.session.flush()
385
370
  return out
386
371
 
@@ -393,9 +378,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
393
378
  dependencies=[Depends(require_idempotency_key)],
394
379
  tags=["Products"],
395
380
  )
396
- async def create_product(
397
- data: ProductCreateIn, svc: PaymentsService = Depends(get_service)
398
- ):
381
+ async def create_product(data: ProductCreateIn, svc: PaymentsService = Depends(get_service)):
399
382
  out = await svc.create_product(data)
400
383
  await svc.session.flush()
401
384
  return out
@@ -408,9 +391,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
408
391
  dependencies=[Depends(require_idempotency_key)],
409
392
  tags=["Prices"],
410
393
  )
411
- async def create_price(
412
- data: PriceCreateIn, svc: PaymentsService = Depends(get_service)
413
- ):
394
+ async def create_price(data: PriceCreateIn, svc: PaymentsService = Depends(get_service)):
414
395
  out = await svc.create_price(data)
415
396
  await svc.session.flush()
416
397
  return out
@@ -481,9 +462,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
481
462
  out = await svc.create_invoice(data)
482
463
  await svc.session.flush()
483
464
  response.headers["Location"] = str(
484
- request.url_for(
485
- "payments_get_invoice", provider_invoice_id=out.provider_invoice_id
486
- )
465
+ request.url_for("payments_get_invoice", provider_invoice_id=out.provider_invoice_id)
487
466
  )
488
467
  return out
489
468
 
@@ -508,9 +487,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
508
487
  dependencies=[Depends(require_idempotency_key)],
509
488
  tags=["Invoices"],
510
489
  )
511
- async def void_invoice(
512
- provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
513
- ):
490
+ async def void_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
514
491
  out = await svc.void_invoice(provider_invoice_id)
515
492
  await svc.session.flush()
516
493
  return out
@@ -522,9 +499,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
522
499
  dependencies=[Depends(require_idempotency_key)],
523
500
  tags=["Invoices"],
524
501
  )
525
- async def pay_invoice(
526
- provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
527
- ):
502
+ async def pay_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
528
503
  out = await svc.pay_invoice(provider_invoice_id)
529
504
  await svc.session.flush()
530
505
  return out
@@ -536,9 +511,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
536
511
  name="payments_get_intent",
537
512
  tags=["Payment Intents"],
538
513
  )
539
- async def get_intent(
540
- provider_intent_id: str, svc: PaymentsService = Depends(get_service)
541
- ):
514
+ async def get_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
542
515
  return await svc.get_intent(provider_intent_id)
543
516
 
544
517
  # STATEMENTS (rollup)
@@ -580,8 +553,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
580
553
  tags=["Payment Intents"],
581
554
  )
582
555
  async def list_intents_endpoint(
583
- customer_provider_id: Optional[str] = None,
584
- status: Optional[str] = None,
556
+ customer_provider_id: str | None = None,
557
+ status: str | None = None,
585
558
  svc: PaymentsService = Depends(get_service),
586
559
  ):
587
560
  ctx = use_pagination()
@@ -621,8 +594,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
621
594
  tags=["Invoices"],
622
595
  )
623
596
  async def list_invoices_endpoint(
624
- customer_provider_id: Optional[str] = None,
625
- status: Optional[str] = None,
597
+ customer_provider_id: str | None = None,
598
+ status: str | None = None,
626
599
  svc: PaymentsService = Depends(get_service),
627
600
  ):
628
601
  ctx = use_pagination()
@@ -656,7 +629,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
656
629
  )
657
630
  async def preview_invoice_endpoint(
658
631
  customer_provider_id: str,
659
- subscription_id: Optional[str] = None,
632
+ subscription_id: str | None = None,
660
633
  svc: PaymentsService = Depends(get_service),
661
634
  ):
662
635
  return await svc.preview_invoice(customer_provider_id, subscription_id)
@@ -744,7 +717,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
744
717
  tags=["Disputes"],
745
718
  )
746
719
  async def list_disputes(
747
- status: Optional[str] = None,
720
+ status: str | None = None,
748
721
  svc: PaymentsService = Depends(get_service),
749
722
  ):
750
723
  ctx = use_pagination()
@@ -759,9 +732,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
759
732
  response_model=DisputeOut,
760
733
  tags=["Disputes"],
761
734
  )
762
- async def get_dispute(
763
- provider_dispute_id: str, svc: PaymentsService = Depends(get_service)
764
- ):
735
+ async def get_dispute(provider_dispute_id: str, svc: PaymentsService = Depends(get_service)):
765
736
  return await svc.get_dispute(provider_dispute_id)
766
737
 
767
738
  @prot.post(
@@ -773,9 +744,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
773
744
  )
774
745
  async def submit_dispute_evidence(
775
746
  provider_dispute_id: str,
776
- evidence: dict = Body(
777
- ..., embed=True
778
- ), # free-form evidence blob you validate internally
747
+ evidence: dict = Body(..., embed=True), # free-form evidence blob you validate internally
779
748
  svc: PaymentsService = Depends(get_service),
780
749
  ):
781
750
  out = await svc.submit_dispute_evidence(provider_dispute_id, evidence)
@@ -810,9 +779,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
810
779
  response_model=PayoutOut,
811
780
  tags=["Payouts"],
812
781
  )
813
- async def get_payout(
814
- provider_payout_id: str, svc: PaymentsService = Depends(get_service)
815
- ):
782
+ async def get_payout(provider_payout_id: str, svc: PaymentsService = Depends(get_service)):
816
783
  return await svc.get_payout(provider_payout_id)
817
784
 
818
785
  # ===== Webhook replay (operational) =====
@@ -824,8 +791,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
824
791
  tags=["Webhooks"],
825
792
  )
826
793
  async def replay_webhooks(
827
- since: Optional[str] = None,
828
- until: Optional[str] = None,
794
+ since: str | None = None,
795
+ until: str | None = None,
829
796
  data: WebhookReplayIn = Body(default=WebhookReplayIn()),
830
797
  svc: PaymentsService = Depends(get_service),
831
798
  ):
@@ -842,8 +809,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
842
809
  tags=["Customers"],
843
810
  )
844
811
  async def list_customers_endpoint(
845
- provider: Optional[str] = None,
846
- user_id: Optional[str] = None,
812
+ provider: str | None = None,
813
+ user_id: str | None = None,
847
814
  svc: PaymentsService = Depends(get_service),
848
815
  ):
849
816
  ctx = use_pagination()
@@ -872,9 +839,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
872
839
  name="payments_get_method",
873
840
  tags=["Payment Methods"],
874
841
  )
875
- async def get_method(
876
- provider_method_id: str, svc: PaymentsService = Depends(get_service)
877
- ):
842
+ async def get_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
878
843
  return await svc.get_payment_method(provider_method_id)
879
844
 
880
845
  @prot.post(
@@ -913,7 +878,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
913
878
  tags=["Products"],
914
879
  )
915
880
  async def list_products_endpoint(
916
- active: Optional[bool] = None,
881
+ active: bool | None = None,
917
882
  svc: PaymentsService = Depends(get_service),
918
883
  ):
919
884
  ctx = use_pagination()
@@ -958,8 +923,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
958
923
  tags=["Prices"],
959
924
  )
960
925
  async def list_prices_endpoint(
961
- provider_product_id: Optional[str] = None,
962
- active: Optional[bool] = None,
926
+ provider_product_id: str | None = None,
927
+ active: bool | None = None,
963
928
  svc: PaymentsService = Depends(get_service),
964
929
  ):
965
930
  ctx = use_pagination()
@@ -1007,8 +972,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
1007
972
  tags=["Subscriptions"],
1008
973
  )
1009
974
  async def list_subscriptions_endpoint(
1010
- customer_provider_id: Optional[str] = None,
1011
- status: Optional[str] = None,
975
+ customer_provider_id: str | None = None,
976
+ status: str | None = None,
1012
977
  svc: PaymentsService = Depends(get_service),
1013
978
  ):
1014
979
  ctx = use_pagination()
@@ -1047,7 +1012,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
1047
1012
  tags=["Refunds"],
1048
1013
  )
1049
1014
  async def list_refunds_endpoint(
1050
- provider_payment_intent_id: Optional[str] = None,
1015
+ provider_payment_intent_id: str | None = None,
1051
1016
  svc: PaymentsService = Depends(get_service),
1052
1017
  ):
1053
1018
  ctx = use_pagination()
@@ -1078,8 +1043,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
1078
1043
  tags=["Usage Records"],
1079
1044
  )
1080
1045
  async def list_usage_records_endpoint(
1081
- subscription_item: Optional[str] = None,
1082
- provider_price_id: Optional[str] = None,
1046
+ subscription_item: str | None = None,
1047
+ provider_price_id: str | None = None,
1083
1048
  svc: PaymentsService = Depends(get_service),
1084
1049
  ):
1085
1050
  ctx = use_pagination()
@@ -1113,9 +1078,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
1113
1078
  dependencies=[Depends(require_idempotency_key)],
1114
1079
  tags=["Payment Methods"],
1115
1080
  )
1116
- async def delete_method_alias(
1117
- alias_id: str, svc: PaymentsService = Depends(get_service)
1118
- ):
1081
+ async def delete_method_alias(alias_id: str, svc: PaymentsService = Depends(get_service)):
1119
1082
  """
1120
1083
  Removes the local alias/association to a payment method.
1121
1084
  This does **not** delete the underlying payment method at the provider.
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Iterable, Optional, cast
4
+ from collections.abc import Iterable
5
+ from typing import TYPE_CHECKING, cast
5
6
 
6
7
  from fastapi import FastAPI
7
8
 
@@ -14,9 +15,7 @@ if TYPE_CHECKING:
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- def _maybe_register_default_providers(
18
- register_defaults: bool, adapters: Optional[Iterable[object]]
19
- ):
18
+ def _maybe_register_default_providers(register_defaults: bool, adapters: Iterable[object] | None):
20
19
  reg = get_provider_registry()
21
20
  if register_defaults:
22
21
  # Try Stripe by default; silently skip if not configured
@@ -34,9 +33,7 @@ def _maybe_register_default_providers(
34
33
  pass
35
34
  if adapters:
36
35
  for a in adapters:
37
- reg.register(
38
- cast("ProviderAdapter", a)
39
- ) # must implement ProviderAdapter protocol
36
+ reg.register(cast("ProviderAdapter", a)) # must implement ProviderAdapter protocol
40
37
 
41
38
 
42
39
  def add_payments(
@@ -44,9 +41,8 @@ def add_payments(
44
41
  *,
45
42
  prefix: str = "/payments",
46
43
  register_default_providers: bool = True,
47
- adapters: Optional[Iterable[object]] = None,
48
- include_in_docs: bool
49
- | None = None, # None = keep your env-based default visibility
44
+ adapters: Iterable[object] | None = None,
45
+ include_in_docs: bool | None = None, # None = keep your env-based default visibility
50
46
  ) -> None:
51
47
  """
52
48
  One-call payments installer.
@@ -60,9 +56,7 @@ def add_payments(
60
56
  for r in build_payments_routers(prefix=prefix):
61
57
  app.include_router(
62
58
  r,
63
- include_in_schema=True
64
- if include_in_docs is None
65
- else bool(include_in_docs),
59
+ include_in_schema=True if include_in_docs is None else bool(include_in_docs),
66
60
  )
67
61
 
68
62
  # Store the startup function to be called by lifespan if needed
@@ -27,7 +27,7 @@ from .security import (
27
27
  RequireService,
28
28
  RequireUser,
29
29
  )
30
- from .settings import AuthSettings, get_auth_settings, JWTSettings, OIDCProvider
30
+ from .settings import AuthSettings, JWTSettings, OIDCProvider, get_auth_settings
31
31
 
32
32
  if TYPE_CHECKING:
33
33
  from .add import add_auth_users as add_auth_users