svc-infra 0.1.589__py3-none-any.whl → 0.1.706__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 (260) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/README.md +732 -0
  3. svc_infra/apf_payments/models.py +133 -42
  4. svc_infra/apf_payments/provider/__init__.py +4 -0
  5. svc_infra/apf_payments/provider/aiydan.py +871 -0
  6. svc_infra/apf_payments/provider/base.py +30 -9
  7. svc_infra/apf_payments/provider/stripe.py +156 -62
  8. svc_infra/apf_payments/schemas.py +19 -10
  9. svc_infra/apf_payments/service.py +211 -68
  10. svc_infra/apf_payments/settings.py +27 -3
  11. svc_infra/api/__init__.py +61 -0
  12. svc_infra/api/fastapi/__init__.py +15 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +245 -0
  15. svc_infra/api/fastapi/apf_payments/router.py +145 -46
  16. svc_infra/api/fastapi/apf_payments/setup.py +26 -8
  17. svc_infra/api/fastapi/auth/__init__.py +65 -0
  18. svc_infra/api/fastapi/auth/_cookies.py +6 -2
  19. svc_infra/api/fastapi/auth/add.py +27 -14
  20. svc_infra/api/fastapi/auth/gaurd.py +104 -13
  21. svc_infra/api/fastapi/auth/mfa/models.py +3 -1
  22. svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
  23. svc_infra/api/fastapi/auth/mfa/router.py +15 -8
  24. svc_infra/api/fastapi/auth/mfa/security.py +1 -2
  25. svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
  26. svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
  27. svc_infra/api/fastapi/auth/policy.py +0 -1
  28. svc_infra/api/fastapi/auth/providers.py +3 -1
  29. svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
  30. svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
  31. svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
  32. svc_infra/api/fastapi/auth/security.py +31 -10
  33. svc_infra/api/fastapi/auth/sender.py +8 -1
  34. svc_infra/api/fastapi/auth/settings.py +2 -0
  35. svc_infra/api/fastapi/auth/state.py +3 -1
  36. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  37. svc_infra/api/fastapi/billing/router.py +73 -0
  38. svc_infra/api/fastapi/billing/setup.py +19 -0
  39. svc_infra/api/fastapi/cache/add.py +9 -5
  40. svc_infra/api/fastapi/db/__init__.py +5 -1
  41. svc_infra/api/fastapi/db/http.py +3 -1
  42. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  43. svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
  44. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
  45. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  46. svc_infra/api/fastapi/db/sql/add.py +71 -26
  47. svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
  48. svc_infra/api/fastapi/db/sql/health.py +3 -1
  49. svc_infra/api/fastapi/db/sql/session.py +18 -0
  50. svc_infra/api/fastapi/db/sql/users.py +29 -5
  51. svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
  52. svc_infra/api/fastapi/docs/add.py +173 -0
  53. svc_infra/api/fastapi/docs/landing.py +4 -2
  54. svc_infra/api/fastapi/docs/scoped.py +62 -15
  55. svc_infra/api/fastapi/dual/__init__.py +12 -2
  56. svc_infra/api/fastapi/dual/dualize.py +1 -1
  57. svc_infra/api/fastapi/dual/protected.py +126 -4
  58. svc_infra/api/fastapi/dual/public.py +25 -0
  59. svc_infra/api/fastapi/dual/router.py +40 -13
  60. svc_infra/api/fastapi/dx.py +33 -2
  61. svc_infra/api/fastapi/ease.py +10 -2
  62. svc_infra/api/fastapi/http/concurrency.py +2 -1
  63. svc_infra/api/fastapi/http/conditional.py +3 -1
  64. svc_infra/api/fastapi/middleware/debug.py +4 -1
  65. svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
  66. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
  67. svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
  68. svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
  69. svc_infra/api/fastapi/middleware/idempotency.py +197 -70
  70. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  71. svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
  72. svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
  73. svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
  74. svc_infra/api/fastapi/middleware/request_id.py +27 -11
  75. svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  76. svc_infra/api/fastapi/middleware/timeout.py +177 -0
  77. svc_infra/api/fastapi/openapi/apply.py +5 -3
  78. svc_infra/api/fastapi/openapi/conventions.py +9 -2
  79. svc_infra/api/fastapi/openapi/mutators.py +165 -20
  80. svc_infra/api/fastapi/openapi/pipeline.py +1 -1
  81. svc_infra/api/fastapi/openapi/security.py +3 -1
  82. svc_infra/api/fastapi/ops/add.py +75 -0
  83. svc_infra/api/fastapi/pagination.py +47 -20
  84. svc_infra/api/fastapi/routers/__init__.py +43 -15
  85. svc_infra/api/fastapi/routers/ping.py +1 -0
  86. svc_infra/api/fastapi/setup.py +188 -56
  87. svc_infra/api/fastapi/tenancy/add.py +19 -0
  88. svc_infra/api/fastapi/tenancy/context.py +112 -0
  89. svc_infra/api/fastapi/versioned.py +101 -0
  90. svc_infra/app/README.md +5 -5
  91. svc_infra/app/__init__.py +3 -1
  92. svc_infra/app/env.py +69 -1
  93. svc_infra/app/logging/add.py +9 -2
  94. svc_infra/app/logging/formats.py +12 -5
  95. svc_infra/billing/__init__.py +23 -0
  96. svc_infra/billing/async_service.py +147 -0
  97. svc_infra/billing/jobs.py +241 -0
  98. svc_infra/billing/models.py +177 -0
  99. svc_infra/billing/quotas.py +103 -0
  100. svc_infra/billing/schemas.py +36 -0
  101. svc_infra/billing/service.py +123 -0
  102. svc_infra/bundled_docs/README.md +5 -0
  103. svc_infra/bundled_docs/__init__.py +1 -0
  104. svc_infra/bundled_docs/getting-started.md +6 -0
  105. svc_infra/cache/__init__.py +9 -0
  106. svc_infra/cache/add.py +170 -0
  107. svc_infra/cache/backend.py +7 -6
  108. svc_infra/cache/decorators.py +81 -15
  109. svc_infra/cache/demo.py +2 -2
  110. svc_infra/cache/keys.py +24 -4
  111. svc_infra/cache/recache.py +26 -14
  112. svc_infra/cache/resources.py +14 -5
  113. svc_infra/cache/tags.py +19 -44
  114. svc_infra/cache/utils.py +3 -1
  115. svc_infra/cli/__init__.py +52 -8
  116. svc_infra/cli/__main__.py +4 -0
  117. svc_infra/cli/cmds/__init__.py +39 -2
  118. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
  119. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
  120. svc_infra/cli/cmds/db/ops_cmds.py +270 -0
  121. svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
  122. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
  123. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  124. svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
  125. svc_infra/cli/cmds/dx/__init__.py +12 -0
  126. svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
  127. svc_infra/cli/cmds/health/__init__.py +179 -0
  128. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  129. svc_infra/cli/cmds/help.py +4 -0
  130. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  131. svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
  132. svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
  133. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  134. svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
  135. svc_infra/cli/foundation/runner.py +6 -2
  136. svc_infra/data/add.py +61 -0
  137. svc_infra/data/backup.py +58 -0
  138. svc_infra/data/erasure.py +45 -0
  139. svc_infra/data/fixtures.py +42 -0
  140. svc_infra/data/retention.py +61 -0
  141. svc_infra/db/__init__.py +15 -0
  142. svc_infra/db/crud_schema.py +9 -9
  143. svc_infra/db/inbox.py +67 -0
  144. svc_infra/db/nosql/__init__.py +3 -0
  145. svc_infra/db/nosql/core.py +30 -9
  146. svc_infra/db/nosql/indexes.py +3 -1
  147. svc_infra/db/nosql/management.py +1 -1
  148. svc_infra/db/nosql/mongo/README.md +13 -13
  149. svc_infra/db/nosql/mongo/client.py +19 -2
  150. svc_infra/db/nosql/mongo/settings.py +6 -2
  151. svc_infra/db/nosql/repository.py +35 -15
  152. svc_infra/db/nosql/resource.py +20 -3
  153. svc_infra/db/nosql/scaffold.py +9 -3
  154. svc_infra/db/nosql/service.py +3 -1
  155. svc_infra/db/nosql/types.py +6 -2
  156. svc_infra/db/ops.py +384 -0
  157. svc_infra/db/outbox.py +108 -0
  158. svc_infra/db/sql/apikey.py +37 -9
  159. svc_infra/db/sql/authref.py +9 -3
  160. svc_infra/db/sql/constants.py +12 -8
  161. svc_infra/db/sql/core.py +2 -2
  162. svc_infra/db/sql/management.py +11 -8
  163. svc_infra/db/sql/repository.py +99 -26
  164. svc_infra/db/sql/resource.py +5 -0
  165. svc_infra/db/sql/scaffold.py +6 -2
  166. svc_infra/db/sql/service.py +15 -5
  167. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  168. svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
  169. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  170. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  171. svc_infra/db/sql/tenant.py +88 -0
  172. svc_infra/db/sql/uniq_hooks.py +9 -3
  173. svc_infra/db/sql/utils.py +138 -51
  174. svc_infra/db/sql/versioning.py +14 -0
  175. svc_infra/deploy/__init__.py +538 -0
  176. svc_infra/documents/__init__.py +100 -0
  177. svc_infra/documents/add.py +264 -0
  178. svc_infra/documents/ease.py +233 -0
  179. svc_infra/documents/models.py +114 -0
  180. svc_infra/documents/storage.py +264 -0
  181. svc_infra/dx/add.py +65 -0
  182. svc_infra/dx/changelog.py +74 -0
  183. svc_infra/dx/checks.py +68 -0
  184. svc_infra/exceptions.py +141 -0
  185. svc_infra/health/__init__.py +864 -0
  186. svc_infra/http/__init__.py +13 -0
  187. svc_infra/http/client.py +105 -0
  188. svc_infra/jobs/builtins/outbox_processor.py +40 -0
  189. svc_infra/jobs/builtins/webhook_delivery.py +95 -0
  190. svc_infra/jobs/easy.py +33 -0
  191. svc_infra/jobs/loader.py +50 -0
  192. svc_infra/jobs/queue.py +116 -0
  193. svc_infra/jobs/redis_queue.py +256 -0
  194. svc_infra/jobs/runner.py +79 -0
  195. svc_infra/jobs/scheduler.py +53 -0
  196. svc_infra/jobs/worker.py +40 -0
  197. svc_infra/loaders/__init__.py +186 -0
  198. svc_infra/loaders/base.py +142 -0
  199. svc_infra/loaders/github.py +311 -0
  200. svc_infra/loaders/models.py +147 -0
  201. svc_infra/loaders/url.py +235 -0
  202. svc_infra/logging/__init__.py +374 -0
  203. svc_infra/mcp/svc_infra_mcp.py +91 -33
  204. svc_infra/obs/README.md +2 -0
  205. svc_infra/obs/add.py +65 -9
  206. svc_infra/obs/cloud_dash.py +2 -1
  207. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  208. svc_infra/obs/metrics/__init__.py +52 -0
  209. svc_infra/obs/metrics/asgi.py +13 -7
  210. svc_infra/obs/metrics/http.py +9 -5
  211. svc_infra/obs/metrics/sqlalchemy.py +13 -9
  212. svc_infra/obs/metrics.py +53 -0
  213. svc_infra/obs/settings.py +6 -2
  214. svc_infra/security/add.py +217 -0
  215. svc_infra/security/audit.py +212 -0
  216. svc_infra/security/audit_service.py +74 -0
  217. svc_infra/security/headers.py +52 -0
  218. svc_infra/security/hibp.py +101 -0
  219. svc_infra/security/jwt_rotation.py +105 -0
  220. svc_infra/security/lockout.py +102 -0
  221. svc_infra/security/models.py +287 -0
  222. svc_infra/security/oauth_models.py +73 -0
  223. svc_infra/security/org_invites.py +130 -0
  224. svc_infra/security/passwords.py +79 -0
  225. svc_infra/security/permissions.py +171 -0
  226. svc_infra/security/session.py +98 -0
  227. svc_infra/security/signed_cookies.py +100 -0
  228. svc_infra/storage/__init__.py +93 -0
  229. svc_infra/storage/add.py +253 -0
  230. svc_infra/storage/backends/__init__.py +11 -0
  231. svc_infra/storage/backends/local.py +339 -0
  232. svc_infra/storage/backends/memory.py +216 -0
  233. svc_infra/storage/backends/s3.py +353 -0
  234. svc_infra/storage/base.py +239 -0
  235. svc_infra/storage/easy.py +185 -0
  236. svc_infra/storage/settings.py +195 -0
  237. svc_infra/testing/__init__.py +685 -0
  238. svc_infra/utils.py +7 -3
  239. svc_infra/webhooks/__init__.py +69 -0
  240. svc_infra/webhooks/add.py +339 -0
  241. svc_infra/webhooks/encryption.py +115 -0
  242. svc_infra/webhooks/fastapi.py +39 -0
  243. svc_infra/webhooks/router.py +55 -0
  244. svc_infra/webhooks/service.py +70 -0
  245. svc_infra/webhooks/signing.py +34 -0
  246. svc_infra/websocket/__init__.py +79 -0
  247. svc_infra/websocket/add.py +140 -0
  248. svc_infra/websocket/client.py +282 -0
  249. svc_infra/websocket/config.py +69 -0
  250. svc_infra/websocket/easy.py +76 -0
  251. svc_infra/websocket/exceptions.py +61 -0
  252. svc_infra/websocket/manager.py +344 -0
  253. svc_infra/websocket/models.py +49 -0
  254. svc_infra-0.1.706.dist-info/LICENSE +21 -0
  255. svc_infra-0.1.706.dist-info/METADATA +356 -0
  256. svc_infra-0.1.706.dist-info/RECORD +357 -0
  257. svc_infra-0.1.589.dist-info/METADATA +0 -79
  258. svc_infra-0.1.589.dist-info/RECORD +0 -234
  259. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
  260. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
@@ -42,10 +42,14 @@ class ProviderAdapter(Protocol):
42
42
  async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
43
43
  pass
44
44
 
45
- async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
45
+ async def attach_payment_method(
46
+ self, data: PaymentMethodAttachIn
47
+ ) -> PaymentMethodOut:
46
48
  pass
47
49
 
48
- async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
50
+ async def list_payment_methods(
51
+ self, provider_customer_id: str
52
+ ) -> list[PaymentMethodOut]:
49
53
  pass
50
54
 
51
55
  async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
@@ -87,7 +91,9 @@ class ProviderAdapter(Protocol):
87
91
  async def pay_invoice(self, provider_invoice_id: str) -> InvoiceOut:
88
92
  pass
89
93
 
90
- async def create_intent(self, data: IntentCreateIn, *, user_id: str | None) -> IntentOut:
94
+ async def create_intent(
95
+ self, data: IntentCreateIn, *, user_id: str | None
96
+ ) -> IntentOut:
91
97
  pass
92
98
 
93
99
  async def confirm_intent(self, provider_intent_id: str) -> IntentOut:
@@ -107,7 +113,9 @@ class ProviderAdapter(Protocol):
107
113
  ) -> dict[str, Any]:
108
114
  pass
109
115
 
110
- async def capture_intent(self, provider_intent_id: str, *, amount: int | None) -> IntentOut:
116
+ async def capture_intent(
117
+ self, provider_intent_id: str, *, amount: int | None
118
+ ) -> IntentOut:
111
119
  pass
112
120
 
113
121
  async def list_intents(
@@ -150,7 +158,9 @@ class ProviderAdapter(Protocol):
150
158
  async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
151
159
  pass
152
160
 
153
- async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
161
+ async def confirm_setup_intent(
162
+ self, provider_setup_intent_id: str
163
+ ) -> SetupIntentOut:
154
164
  pass
155
165
 
156
166
  async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
@@ -169,7 +179,9 @@ class ProviderAdapter(Protocol):
169
179
  async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
170
180
  pass
171
181
 
172
- async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
182
+ async def submit_dispute_evidence(
183
+ self, provider_dispute_id: str, evidence: dict
184
+ ) -> DisputeOut:
173
185
  pass
174
186
 
175
187
  # --- Balance & Payouts ---
@@ -186,7 +198,12 @@ class ProviderAdapter(Protocol):
186
198
 
187
199
  # --- Customers ---
188
200
  async def list_customers(
189
- self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
201
+ self,
202
+ *,
203
+ provider: str | None,
204
+ user_id: str | None,
205
+ limit: int,
206
+ cursor: str | None,
190
207
  ) -> tuple[list[CustomerOut], str | None]:
191
208
  """Optional: if not implemented, the service will list from local DB."""
192
209
  pass
@@ -203,7 +220,9 @@ class ProviderAdapter(Protocol):
203
220
  ) -> tuple[list[ProductOut], str | None]:
204
221
  pass
205
222
 
206
- async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
223
+ async def update_product(
224
+ self, provider_product_id: str, data: ProductUpdateIn
225
+ ) -> ProductOut:
207
226
  pass
208
227
 
209
228
  async def get_price(self, provider_price_id: str) -> PriceOut:
@@ -219,7 +238,9 @@ class ProviderAdapter(Protocol):
219
238
  ) -> tuple[list[PriceOut], str | None]:
220
239
  pass
221
240
 
222
- async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
241
+ async def update_price(
242
+ self, provider_price_id: str, data: PriceUpdateIn
243
+ ) -> PriceOut:
223
244
  pass
224
245
 
225
246
  # --- Subscriptions ---
@@ -44,7 +44,7 @@ from .base import ProviderAdapter
44
44
  try:
45
45
  import stripe
46
46
  except Exception: # pragma: no cover
47
- stripe = None # type: ignore
47
+ stripe = None # type: ignore[assignment]
48
48
 
49
49
 
50
50
  async def _acall(fn, /, *args, **kwargs):
@@ -60,7 +60,9 @@ def _pi_to_out(pi) -> IntentOut:
60
60
  amount=int(pi.amount),
61
61
  currency=str(pi.currency).upper(),
62
62
  client_secret=getattr(pi, "client_secret", None),
63
- next_action=NextAction(type=getattr(getattr(pi, "next_action", None), "type", None)),
63
+ next_action=NextAction(
64
+ type=getattr(getattr(pi, "next_action", None), "type", None)
65
+ ),
64
66
  )
65
67
 
66
68
 
@@ -134,7 +136,9 @@ def _sub_to_out(s) -> SubscriptionOut:
134
136
  quantity=int(qty or 0),
135
137
  cancel_at_period_end=bool(s.cancel_at_period_end),
136
138
  current_period_end=(
137
- str(s.current_period_end) if getattr(s, "current_period_end", None) else None
139
+ str(s.current_period_end)
140
+ if getattr(s, "current_period_end", None)
141
+ else None
138
142
  ),
139
143
  )
140
144
 
@@ -163,7 +167,9 @@ def _dispute_to_out(d) -> DisputeOut:
163
167
  reason=getattr(d, "reason", None),
164
168
  status=d.status,
165
169
  evidence_due_by=(
166
- str(d.evidence_details.get("due_by")) if getattr(d, "evidence_details", None) else None
170
+ str(d.evidence_details.get("due_by"))
171
+ if getattr(d, "evidence_details", None)
172
+ else None
167
173
  ),
168
174
  created_at=str(d.created) if getattr(d, "created", None) else None,
169
175
  )
@@ -193,7 +199,9 @@ class StripeAdapter(ProviderAdapter):
193
199
  raise RuntimeError("stripe SDK is not installed. pip install stripe")
194
200
  stripe.api_key = st.stripe.secret_key.get_secret_value()
195
201
  self._wh_secret = (
196
- st.stripe.webhook_secret.get_secret_value() if st.stripe.webhook_secret else None
202
+ st.stripe.webhook_secret.get_secret_value()
203
+ if st.stripe.webhook_secret
204
+ else None
197
205
  )
198
206
 
199
207
  # -------- Customers --------
@@ -235,9 +243,14 @@ class StripeAdapter(ProviderAdapter):
235
243
  )
236
244
 
237
245
  async def list_customers(
238
- self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
246
+ self,
247
+ *,
248
+ provider: str | None,
249
+ user_id: str | None,
250
+ limit: int,
251
+ cursor: str | None,
239
252
  ) -> tuple[list[CustomerOut], str | None]:
240
- params = {"limit": int(limit)}
253
+ params: dict[str, Any] = {"limit": int(limit)}
241
254
  if cursor:
242
255
  params["starting_after"] = cursor
243
256
  # Stripe has no direct filter for our custom user_id; many teams store mapping in DB.
@@ -253,11 +266,15 @@ class StripeAdapter(ProviderAdapter):
253
266
  )
254
267
  for c in res.data
255
268
  ]
256
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
269
+ next_cursor = (
270
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
271
+ )
257
272
  return items, next_cursor
258
273
 
259
274
  # -------- Payment Methods --------
260
- async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
275
+ async def attach_payment_method(
276
+ self, data: PaymentMethodAttachIn
277
+ ) -> PaymentMethodOut:
261
278
  pm = await _acall(
262
279
  stripe.PaymentMethod.attach,
263
280
  data.payment_method_token,
@@ -271,23 +288,35 @@ class StripeAdapter(ProviderAdapter):
271
288
  invoice_settings={"default_payment_method": pm.id},
272
289
  )
273
290
  is_default = (
274
- getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
291
+ getattr(
292
+ getattr(cust, "invoice_settings", None),
293
+ "default_payment_method",
294
+ None,
295
+ )
275
296
  == pm.id
276
297
  )
277
298
  else:
278
299
  cust = await _acall(stripe.Customer.retrieve, data.customer_provider_id)
279
300
  is_default = (
280
- getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
301
+ getattr(
302
+ getattr(cust, "invoice_settings", None),
303
+ "default_payment_method",
304
+ None,
305
+ )
281
306
  == pm.id
282
307
  )
283
308
  return _pm_to_out(pm, is_default=is_default)
284
309
 
285
- async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
310
+ async def list_payment_methods(
311
+ self, provider_customer_id: str
312
+ ) -> list[PaymentMethodOut]:
286
313
  cust = await _acall(stripe.Customer.retrieve, provider_customer_id)
287
314
  default_pm = getattr(
288
315
  getattr(cust, "invoice_settings", None), "default_payment_method", None
289
316
  )
290
- res = await _acall(stripe.PaymentMethod.list, customer=provider_customer_id, type="card")
317
+ res = await _acall(
318
+ stripe.PaymentMethod.list, customer=provider_customer_id, type="card"
319
+ )
291
320
  return [_pm_to_out(pm, is_default=(pm.id == default_pm)) for pm in res.data]
292
321
 
293
322
  async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
@@ -312,7 +341,9 @@ class StripeAdapter(ProviderAdapter):
312
341
  )
313
342
  pm = await _acall(stripe.PaymentMethod.retrieve, provider_method_id)
314
343
  is_default = (
315
- getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
344
+ getattr(
345
+ getattr(cust, "invoice_settings", None), "default_payment_method", None
346
+ )
316
347
  == pm.id
317
348
  )
318
349
  return _pm_to_out(pm, is_default=is_default)
@@ -356,7 +387,9 @@ class StripeAdapter(ProviderAdapter):
356
387
 
357
388
  # -------- Products / Prices --------
358
389
  async def create_product(self, data: ProductCreateIn) -> ProductOut:
359
- p = await _acall(stripe.Product.create, name=data.name, active=bool(data.active))
390
+ p = await _acall(
391
+ stripe.Product.create, name=data.name, active=bool(data.active)
392
+ )
360
393
  return _product_to_out(p)
361
394
 
362
395
  async def get_product(self, provider_product_id: str) -> ProductOut:
@@ -366,17 +399,21 @@ class StripeAdapter(ProviderAdapter):
366
399
  async def list_products(
367
400
  self, *, active: bool | None, limit: int, cursor: str | None
368
401
  ) -> tuple[list[ProductOut], str | None]:
369
- params = {"limit": int(limit)}
402
+ params: dict[str, Any] = {"limit": int(limit)}
370
403
  if active is not None:
371
404
  params["active"] = bool(active)
372
405
  if cursor:
373
406
  params["starting_after"] = cursor
374
407
  res = await _acall(stripe.Product.list, **params)
375
408
  items = [_product_to_out(p) for p in res.data]
376
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
409
+ next_cursor = (
410
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
411
+ )
377
412
  return items, next_cursor
378
413
 
379
- async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
414
+ async def update_product(
415
+ self, provider_product_id: str, data: ProductUpdateIn
416
+ ) -> ProductOut:
380
417
  update: dict[str, Any] = {}
381
418
  if data.name is not None:
382
419
  update["name"] = data.name
@@ -415,7 +452,7 @@ class StripeAdapter(ProviderAdapter):
415
452
  limit: int,
416
453
  cursor: str | None,
417
454
  ) -> tuple[list[PriceOut], str | None]:
418
- params = {"limit": int(limit)}
455
+ params: dict[str, Any] = {"limit": int(limit)}
419
456
  if provider_product_id:
420
457
  params["product"] = provider_product_id
421
458
  if active is not None:
@@ -424,10 +461,14 @@ class StripeAdapter(ProviderAdapter):
424
461
  params["starting_after"] = cursor
425
462
  res = await _acall(stripe.Price.list, **params)
426
463
  items = [_price_to_out(p) for p in res.data]
427
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
464
+ next_cursor = (
465
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
466
+ )
428
467
  return items, next_cursor
429
468
 
430
- async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
469
+ async def update_price(
470
+ self, provider_price_id: str, data: PriceUpdateIn
471
+ ) -> PriceOut:
431
472
  # Stripe allows toggling `active` and updating metadata, but not amount/currency/product.
432
473
  update: dict[str, Any] = {}
433
474
  if data.active is not None:
@@ -454,7 +495,9 @@ class StripeAdapter(ProviderAdapter):
454
495
  async def update_subscription(
455
496
  self, provider_subscription_id: str, data: SubscriptionUpdateIn
456
497
  ) -> SubscriptionOut:
457
- s = await _acall(stripe.Subscription.retrieve, provider_subscription_id, expand=["items"])
498
+ s = await _acall(
499
+ stripe.Subscription.retrieve, provider_subscription_id, expand=["items"]
500
+ )
458
501
  items = s.items.data
459
502
  update_kwargs: dict[str, Any] = {"proration_behavior": data.proration_behavior}
460
503
  # update first item (simple plan model)
@@ -468,21 +511,27 @@ class StripeAdapter(ProviderAdapter):
468
511
  update_kwargs["items"] = [item_update]
469
512
  if data.cancel_at_period_end is not None:
470
513
  update_kwargs["cancel_at_period_end"] = bool(data.cancel_at_period_end)
471
- s2 = await _acall(stripe.Subscription.modify, provider_subscription_id, **update_kwargs)
514
+ s2 = await _acall(
515
+ stripe.Subscription.modify, provider_subscription_id, **update_kwargs
516
+ )
472
517
  return _sub_to_out(s2)
473
518
 
474
519
  async def cancel_subscription(
475
520
  self, provider_subscription_id: str, at_period_end: bool = True
476
521
  ) -> SubscriptionOut:
477
522
  s = await _acall(
478
- stripe.Subscription.cancel if not at_period_end else stripe.Subscription.modify,
523
+ stripe.Subscription.cancel
524
+ if not at_period_end
525
+ else stripe.Subscription.modify,
479
526
  provider_subscription_id,
480
527
  **({} if not at_period_end else {"cancel_at_period_end": True}),
481
528
  )
482
529
  return _sub_to_out(s)
483
530
 
484
531
  async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
485
- s = await _acall(stripe.Subscription.retrieve, provider_subscription_id, expand=["items"])
532
+ s = await _acall(
533
+ stripe.Subscription.retrieve, provider_subscription_id, expand=["items"]
534
+ )
486
535
  return _sub_to_out(s)
487
536
 
488
537
  async def list_subscriptions(
@@ -493,7 +542,7 @@ class StripeAdapter(ProviderAdapter):
493
542
  limit: int,
494
543
  cursor: str | None,
495
544
  ) -> tuple[list[SubscriptionOut], str | None]:
496
- params = {"limit": int(limit)}
545
+ params: dict[str, Any] = {"limit": int(limit)}
497
546
  if customer_provider_id:
498
547
  params["customer"] = customer_provider_id
499
548
  if status:
@@ -502,7 +551,9 @@ class StripeAdapter(ProviderAdapter):
502
551
  params["starting_after"] = cursor
503
552
  res = await _acall(stripe.Subscription.list, **params)
504
553
  items = [_sub_to_out(s) for s in res.data]
505
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
554
+ next_cursor = (
555
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
556
+ )
506
557
  return items, next_cursor
507
558
 
508
559
  # -------- Invoices --------
@@ -542,7 +593,8 @@ class StripeAdapter(ProviderAdapter):
542
593
  else:
543
594
  kwargs["unit_amount"] = int(data.unit_amount)
544
595
  await _acall(
545
- stripe.InvoiceItem.create, **{k: v for k, v in kwargs.items() if v is not None}
596
+ stripe.InvoiceItem.create,
597
+ **{k: v for k, v in kwargs.items() if v is not None},
546
598
  )
547
599
  inv = await _acall(stripe.Invoice.retrieve, provider_invoice_id)
548
600
  return _inv_to_out(inv)
@@ -555,7 +607,7 @@ class StripeAdapter(ProviderAdapter):
555
607
  limit: int,
556
608
  cursor: str | None,
557
609
  ) -> tuple[list[InvoiceOut], str | None]:
558
- params = {"limit": int(limit)}
610
+ params: dict[str, Any] = {"limit": int(limit)}
559
611
  if customer_provider_id:
560
612
  params["customer"] = customer_provider_id
561
613
  if status:
@@ -564,7 +616,9 @@ class StripeAdapter(ProviderAdapter):
564
616
  params["starting_after"] = cursor
565
617
  res = await _acall(stripe.Invoice.list, **params)
566
618
  items = [_inv_to_out(inv) for inv in res.data]
567
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
619
+ next_cursor = (
620
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
621
+ )
568
622
  return items, next_cursor
569
623
 
570
624
  async def get_invoice(self, provider_invoice_id: str) -> InvoiceOut:
@@ -577,13 +631,13 @@ class StripeAdapter(ProviderAdapter):
577
631
  params = {"customer": customer_provider_id}
578
632
  if subscription_id:
579
633
  params["subscription"] = subscription_id
580
- inv = await _acall(stripe.Invoice.upcoming, **params)
634
+ inv = await _acall(stripe.Invoice.upcoming, **params) # type: ignore[attr-defined]
581
635
  return _inv_to_out(inv)
582
636
 
583
637
  async def list_invoice_line_items(
584
638
  self, provider_invoice_id: str, *, limit: int, cursor: str | None
585
639
  ) -> tuple[list[InvoiceLineItemOut], str | None]:
586
- params = {"limit": int(limit)}
640
+ params: dict[str, Any] = {"limit": int(limit)}
587
641
  if cursor:
588
642
  params["starting_after"] = cursor
589
643
  res = await _acall(stripe.Invoice.list_lines, provider_invoice_id, **params)
@@ -602,22 +656,29 @@ class StripeAdapter(ProviderAdapter):
602
656
  provider_price_id=price_id,
603
657
  )
604
658
  )
605
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
659
+ next_cursor = (
660
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
661
+ )
606
662
  return items, next_cursor
607
663
 
608
664
  # -------- Intents --------
609
- async def create_intent(self, data: IntentCreateIn, *, user_id: str | None) -> IntentOut:
665
+ async def create_intent(
666
+ self, data: IntentCreateIn, *, user_id: str | None
667
+ ) -> IntentOut:
610
668
  kwargs: dict[str, Any] = dict(
611
669
  amount=int(data.amount),
612
670
  currency=data.currency.lower(),
613
671
  description=data.description or None,
614
672
  capture_method="manual" if data.capture_method == "manual" else "automatic",
615
- automatic_payment_methods={"enabled": True} if not data.payment_method_types else None,
673
+ automatic_payment_methods={"enabled": True}
674
+ if not data.payment_method_types
675
+ else None,
616
676
  )
617
677
  if data.payment_method_types:
618
678
  kwargs["payment_method_types"] = data.payment_method_types
619
679
  pi = await _acall(
620
- stripe.PaymentIntent.create, **{k: v for k, v in kwargs.items() if v is not None}
680
+ stripe.PaymentIntent.create,
681
+ **{k: v for k, v in kwargs.items() if v is not None},
621
682
  )
622
683
  return _pi_to_out(pi)
623
684
 
@@ -648,7 +709,9 @@ class StripeAdapter(ProviderAdapter):
648
709
  pi = await _acall(stripe.PaymentIntent.retrieve, provider_intent_id)
649
710
  return _pi_to_out(pi)
650
711
 
651
- async def capture_intent(self, provider_intent_id: str, *, amount: int | None) -> IntentOut:
712
+ async def capture_intent(
713
+ self, provider_intent_id: str, *, amount: int | None
714
+ ) -> IntentOut:
652
715
  kwargs = {}
653
716
  if amount is not None:
654
717
  kwargs["amount_to_capture"] = int(amount)
@@ -663,7 +726,7 @@ class StripeAdapter(ProviderAdapter):
663
726
  limit: int,
664
727
  cursor: str | None,
665
728
  ) -> tuple[list[IntentOut], str | None]:
666
- params = {"limit": int(limit)}
729
+ params: dict[str, Any] = {"limit": int(limit)}
667
730
  if customer_provider_id:
668
731
  params["customer"] = customer_provider_id
669
732
  if status:
@@ -672,13 +735,16 @@ class StripeAdapter(ProviderAdapter):
672
735
  params["starting_after"] = cursor
673
736
  res = await _acall(stripe.PaymentIntent.list, **params)
674
737
  items = [_pi_to_out(pi) for pi in res.data]
675
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
738
+ next_cursor = (
739
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
740
+ )
676
741
  return items, next_cursor
677
742
 
678
743
  # ---- Setup Intents (off-session readiness) ----
679
744
  async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
680
745
  si = await _acall(
681
- stripe.SetupIntent.create, payment_method_types=data.payment_method_types or ["card"]
746
+ stripe.SetupIntent.create,
747
+ payment_method_types=data.payment_method_types or ["card"],
682
748
  )
683
749
  return SetupIntentOut(
684
750
  id=si.id,
@@ -686,10 +752,14 @@ class StripeAdapter(ProviderAdapter):
686
752
  provider_setup_intent_id=si.id,
687
753
  status=si.status,
688
754
  client_secret=getattr(si, "client_secret", None),
689
- next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
755
+ next_action=NextAction(
756
+ type=getattr(getattr(si, "next_action", None), "type", None)
757
+ ),
690
758
  )
691
759
 
692
- async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
760
+ async def confirm_setup_intent(
761
+ self, provider_setup_intent_id: str
762
+ ) -> SetupIntentOut:
693
763
  si = await _acall(stripe.SetupIntent.confirm, provider_setup_intent_id)
694
764
  return SetupIntentOut(
695
765
  id=si.id,
@@ -697,7 +767,9 @@ class StripeAdapter(ProviderAdapter):
697
767
  provider_setup_intent_id=si.id,
698
768
  status=si.status,
699
769
  client_secret=getattr(si, "client_secret", None),
700
- next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
770
+ next_action=NextAction(
771
+ type=getattr(getattr(si, "next_action", None), "type", None)
772
+ ),
701
773
  )
702
774
 
703
775
  async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
@@ -708,7 +780,9 @@ class StripeAdapter(ProviderAdapter):
708
780
  provider_setup_intent_id=si.id,
709
781
  status=si.status,
710
782
  client_secret=getattr(si, "client_secret", None),
711
- next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
783
+ next_action=NextAction(
784
+ type=getattr(getattr(si, "next_action", None), "type", None)
785
+ ),
712
786
  )
713
787
 
714
788
  # ---- 3DS/SCA resume ----
@@ -720,25 +794,29 @@ class StripeAdapter(ProviderAdapter):
720
794
  async def list_disputes(
721
795
  self, *, status: str | None, limit: int, cursor: str | None
722
796
  ) -> tuple[list[DisputeOut], str | None]:
723
- params = {"limit": int(limit)}
797
+ params: dict[str, Any] = {"limit": int(limit)}
724
798
  if status:
725
799
  params["status"] = status
726
800
  if cursor:
727
801
  params["starting_after"] = cursor
728
802
  res = await _acall(stripe.Dispute.list, **params)
729
803
  items = [_dispute_to_out(d) for d in res.data]
730
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
804
+ next_cursor = (
805
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
806
+ )
731
807
  return items, next_cursor
732
808
 
733
809
  async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
734
810
  d = await _acall(stripe.Dispute.retrieve, provider_dispute_id)
735
811
  return _dispute_to_out(d)
736
812
 
737
- async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
813
+ async def submit_dispute_evidence(
814
+ self, provider_dispute_id: str, evidence: dict
815
+ ) -> DisputeOut:
738
816
  d = await _acall(stripe.Dispute.modify, provider_dispute_id, evidence=evidence)
739
817
  # Some disputes require explicit submit call:
740
818
  try:
741
- d = await _acall(stripe.Dispute.submit, provider_dispute_id)
819
+ d = await _acall(stripe.Dispute.submit, provider_dispute_id) # type: ignore[attr-defined]
742
820
  except Exception:
743
821
  pass
744
822
  return _dispute_to_out(d)
@@ -750,7 +828,9 @@ class StripeAdapter(ProviderAdapter):
750
828
  def _bucket(entries):
751
829
  out = []
752
830
  for b in entries or []:
753
- out.append({"currency": str(b.currency).upper(), "amount": int(b.amount)})
831
+ out.append(
832
+ {"currency": str(b.currency).upper(), "amount": int(b.amount)}
833
+ )
754
834
  return out
755
835
 
756
836
  return BalanceSnapshotOut(
@@ -761,12 +841,14 @@ class StripeAdapter(ProviderAdapter):
761
841
  async def list_payouts(
762
842
  self, *, limit: int, cursor: str | None
763
843
  ) -> tuple[list[PayoutOut], str | None]:
764
- params = {"limit": int(limit)}
844
+ params: dict[str, Any] = {"limit": int(limit)}
765
845
  if cursor:
766
846
  params["starting_after"] = cursor
767
847
  res = await _acall(stripe.Payout.list, **params)
768
848
  items = [_payout_to_out(p) for p in res.data]
769
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
849
+ next_cursor = (
850
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
851
+ )
770
852
  return items, next_cursor
771
853
 
772
854
  async def get_payout(self, provider_payout_id: str) -> PayoutOut:
@@ -777,14 +859,16 @@ class StripeAdapter(ProviderAdapter):
777
859
  async def list_refunds(
778
860
  self, *, provider_payment_intent_id: str | None, limit: int, cursor: str | None
779
861
  ) -> tuple[list[RefundOut], str | None]:
780
- params = {"limit": int(limit)}
862
+ params: dict[str, Any] = {"limit": int(limit)}
781
863
  if provider_payment_intent_id:
782
864
  params["payment_intent"] = provider_payment_intent_id
783
865
  if cursor:
784
866
  params["starting_after"] = cursor
785
867
  res = await _acall(stripe.Refund.list, **params)
786
868
  items = [_refund_to_out(r) for r in res.data]
787
- next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
869
+ next_cursor = (
870
+ res.data[-1].id if getattr(res, "has_more", False) and res.data else None
871
+ )
788
872
  return items, next_cursor
789
873
 
790
874
  async def get_refund(self, provider_refund_id: str) -> RefundOut:
@@ -810,7 +894,7 @@ class StripeAdapter(ProviderAdapter):
810
894
  }
811
895
  if data.timestamp:
812
896
  body["timestamp"] = int(data.timestamp)
813
- rec = await _acall(stripe.UsageRecord.create, **body)
897
+ rec = await _acall(stripe.UsageRecord.create, **body) # type: ignore[attr-defined]
814
898
  return UsageRecordOut(
815
899
  id=rec.id,
816
900
  quantity=int(rec.quantity),
@@ -825,19 +909,25 @@ class StripeAdapter(ProviderAdapter):
825
909
  # Stripe exposes *summaries* per period. We surface them as list results.
826
910
  sub_item = f.subscription_item
827
911
  if not sub_item and f.provider_price_id:
828
- items = await _acall(stripe.SubscriptionItem.list, price=f.provider_price_id, limit=1)
912
+ items = await _acall(
913
+ stripe.SubscriptionItem.list, price=f.provider_price_id, limit=1
914
+ )
829
915
  sub_item = items.data[0].id if items.data else None
830
916
  if not sub_item:
831
917
  return [], None
832
- params = {"limit": int(f.limit or 50)}
918
+ params: dict[str, Any] = {"limit": int(f.limit or 50)}
833
919
  if f.cursor:
834
920
  params["starting_after"] = f.cursor
835
- res = await _acall(stripe.SubscriptionItem.list_usage_record_summaries, sub_item, **params)
836
- items: list[UsageRecordOut] = []
921
+ res = await _acall(
922
+ stripe.SubscriptionItem.list_usage_record_summaries, # type: ignore[attr-defined]
923
+ sub_item,
924
+ **params,
925
+ )
926
+ usage_records: list[UsageRecordOut] = []
837
927
  for s in res.data:
838
928
  # No record id in summaries—synthesize a stable id from period start.
839
929
  synthesized_id = f"{sub_item}:{getattr(s, 'period', {}).get('start')}"
840
- items.append(
930
+ usage_records.append(
841
931
  UsageRecordOut(
842
932
  id=synthesized_id,
843
933
  quantity=int(getattr(s, "total_usage", 0)),
@@ -848,15 +938,19 @@ class StripeAdapter(ProviderAdapter):
848
938
  )
849
939
  next_cursor = (
850
940
  res.data[-1].id
851
- if getattr(res, "has_more", False) and res.data and hasattr(res.data[-1], "id")
941
+ if getattr(res, "has_more", False)
942
+ and res.data
943
+ and hasattr(res.data[-1], "id")
852
944
  else None
853
945
  )
854
- return items, next_cursor
946
+ return usage_records, next_cursor
855
947
 
856
948
  async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
857
949
  # Stripe has no direct "retrieve usage record by id" API.
858
950
  # You can reconstruct via list summaries or store records locally when creating.
859
- raise NotImplementedError("Stripe does not support retrieving a single usage record by id")
951
+ raise NotImplementedError(
952
+ "Stripe does not support retrieving a single usage record by id"
953
+ )
860
954
 
861
955
  # -------- Webhooks --------
862
956
  async def verify_and_parse_webhook(