svc-infra 0.1.595__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 (256) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/models.py +133 -42
  3. svc_infra/apf_payments/provider/aiydan.py +121 -47
  4. svc_infra/apf_payments/provider/base.py +30 -9
  5. svc_infra/apf_payments/provider/stripe.py +156 -62
  6. svc_infra/apf_payments/schemas.py +18 -9
  7. svc_infra/apf_payments/service.py +98 -41
  8. svc_infra/apf_payments/settings.py +5 -1
  9. svc_infra/api/__init__.py +61 -0
  10. svc_infra/api/fastapi/__init__.py +15 -0
  11. svc_infra/api/fastapi/admin/__init__.py +3 -0
  12. svc_infra/api/fastapi/admin/add.py +245 -0
  13. svc_infra/api/fastapi/apf_payments/router.py +128 -70
  14. svc_infra/api/fastapi/apf_payments/setup.py +13 -6
  15. svc_infra/api/fastapi/auth/__init__.py +65 -0
  16. svc_infra/api/fastapi/auth/_cookies.py +6 -2
  17. svc_infra/api/fastapi/auth/add.py +17 -14
  18. svc_infra/api/fastapi/auth/gaurd.py +45 -16
  19. svc_infra/api/fastapi/auth/mfa/models.py +3 -1
  20. svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
  21. svc_infra/api/fastapi/auth/mfa/router.py +15 -8
  22. svc_infra/api/fastapi/auth/mfa/security.py +1 -2
  23. svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
  24. svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
  25. svc_infra/api/fastapi/auth/policy.py +0 -1
  26. svc_infra/api/fastapi/auth/providers.py +3 -1
  27. svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
  28. svc_infra/api/fastapi/auth/routers/oauth_router.py +146 -52
  29. svc_infra/api/fastapi/auth/routers/session_router.py +6 -2
  30. svc_infra/api/fastapi/auth/security.py +31 -10
  31. svc_infra/api/fastapi/auth/sender.py +8 -1
  32. svc_infra/api/fastapi/auth/state.py +3 -1
  33. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  34. svc_infra/api/fastapi/billing/router.py +73 -0
  35. svc_infra/api/fastapi/billing/setup.py +19 -0
  36. svc_infra/api/fastapi/cache/add.py +9 -5
  37. svc_infra/api/fastapi/db/__init__.py +5 -1
  38. svc_infra/api/fastapi/db/http.py +3 -1
  39. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  40. svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
  41. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
  42. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  43. svc_infra/api/fastapi/db/sql/add.py +71 -26
  44. svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
  45. svc_infra/api/fastapi/db/sql/health.py +3 -1
  46. svc_infra/api/fastapi/db/sql/session.py +18 -0
  47. svc_infra/api/fastapi/db/sql/users.py +18 -6
  48. svc_infra/api/fastapi/dependencies/ratelimit.py +78 -14
  49. svc_infra/api/fastapi/docs/add.py +173 -0
  50. svc_infra/api/fastapi/docs/landing.py +4 -2
  51. svc_infra/api/fastapi/docs/scoped.py +62 -15
  52. svc_infra/api/fastapi/dual/__init__.py +12 -2
  53. svc_infra/api/fastapi/dual/dualize.py +1 -1
  54. svc_infra/api/fastapi/dual/protected.py +126 -4
  55. svc_infra/api/fastapi/dual/public.py +25 -0
  56. svc_infra/api/fastapi/dual/router.py +40 -13
  57. svc_infra/api/fastapi/dx.py +33 -2
  58. svc_infra/api/fastapi/ease.py +10 -2
  59. svc_infra/api/fastapi/http/concurrency.py +2 -1
  60. svc_infra/api/fastapi/http/conditional.py +3 -1
  61. svc_infra/api/fastapi/middleware/debug.py +4 -1
  62. svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
  63. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
  64. svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
  65. svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
  66. svc_infra/api/fastapi/middleware/idempotency.py +197 -70
  67. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  68. svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
  69. svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
  70. svc_infra/api/fastapi/middleware/ratelimit_store.py +43 -10
  71. svc_infra/api/fastapi/middleware/request_id.py +27 -11
  72. svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
  73. svc_infra/api/fastapi/middleware/timeout.py +177 -0
  74. svc_infra/api/fastapi/openapi/apply.py +5 -3
  75. svc_infra/api/fastapi/openapi/conventions.py +9 -2
  76. svc_infra/api/fastapi/openapi/mutators.py +165 -20
  77. svc_infra/api/fastapi/openapi/pipeline.py +1 -1
  78. svc_infra/api/fastapi/openapi/security.py +3 -1
  79. svc_infra/api/fastapi/ops/add.py +75 -0
  80. svc_infra/api/fastapi/pagination.py +47 -20
  81. svc_infra/api/fastapi/routers/__init__.py +43 -15
  82. svc_infra/api/fastapi/routers/ping.py +1 -0
  83. svc_infra/api/fastapi/setup.py +188 -57
  84. svc_infra/api/fastapi/tenancy/add.py +19 -0
  85. svc_infra/api/fastapi/tenancy/context.py +112 -0
  86. svc_infra/api/fastapi/versioned.py +101 -0
  87. svc_infra/app/README.md +5 -5
  88. svc_infra/app/__init__.py +3 -1
  89. svc_infra/app/env.py +69 -1
  90. svc_infra/app/logging/add.py +9 -2
  91. svc_infra/app/logging/formats.py +12 -5
  92. svc_infra/billing/__init__.py +23 -0
  93. svc_infra/billing/async_service.py +147 -0
  94. svc_infra/billing/jobs.py +241 -0
  95. svc_infra/billing/models.py +177 -0
  96. svc_infra/billing/quotas.py +103 -0
  97. svc_infra/billing/schemas.py +36 -0
  98. svc_infra/billing/service.py +123 -0
  99. svc_infra/bundled_docs/README.md +5 -0
  100. svc_infra/bundled_docs/__init__.py +1 -0
  101. svc_infra/bundled_docs/getting-started.md +6 -0
  102. svc_infra/cache/__init__.py +9 -0
  103. svc_infra/cache/add.py +170 -0
  104. svc_infra/cache/backend.py +7 -6
  105. svc_infra/cache/decorators.py +81 -15
  106. svc_infra/cache/demo.py +2 -2
  107. svc_infra/cache/keys.py +24 -4
  108. svc_infra/cache/recache.py +26 -14
  109. svc_infra/cache/resources.py +14 -5
  110. svc_infra/cache/tags.py +19 -44
  111. svc_infra/cache/utils.py +3 -1
  112. svc_infra/cli/__init__.py +52 -8
  113. svc_infra/cli/__main__.py +4 -0
  114. svc_infra/cli/cmds/__init__.py +39 -2
  115. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
  116. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
  117. svc_infra/cli/cmds/db/ops_cmds.py +270 -0
  118. svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
  119. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
  120. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  121. svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
  122. svc_infra/cli/cmds/dx/__init__.py +12 -0
  123. svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
  124. svc_infra/cli/cmds/health/__init__.py +179 -0
  125. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  126. svc_infra/cli/cmds/help.py +4 -0
  127. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  128. svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
  129. svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
  130. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  131. svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
  132. svc_infra/cli/foundation/runner.py +6 -2
  133. svc_infra/data/add.py +61 -0
  134. svc_infra/data/backup.py +58 -0
  135. svc_infra/data/erasure.py +45 -0
  136. svc_infra/data/fixtures.py +42 -0
  137. svc_infra/data/retention.py +61 -0
  138. svc_infra/db/__init__.py +15 -0
  139. svc_infra/db/crud_schema.py +9 -9
  140. svc_infra/db/inbox.py +67 -0
  141. svc_infra/db/nosql/__init__.py +3 -0
  142. svc_infra/db/nosql/core.py +30 -9
  143. svc_infra/db/nosql/indexes.py +3 -1
  144. svc_infra/db/nosql/management.py +1 -1
  145. svc_infra/db/nosql/mongo/README.md +13 -13
  146. svc_infra/db/nosql/mongo/client.py +19 -2
  147. svc_infra/db/nosql/mongo/settings.py +6 -2
  148. svc_infra/db/nosql/repository.py +35 -15
  149. svc_infra/db/nosql/resource.py +20 -3
  150. svc_infra/db/nosql/scaffold.py +9 -3
  151. svc_infra/db/nosql/service.py +3 -1
  152. svc_infra/db/nosql/types.py +6 -2
  153. svc_infra/db/ops.py +384 -0
  154. svc_infra/db/outbox.py +108 -0
  155. svc_infra/db/sql/apikey.py +37 -9
  156. svc_infra/db/sql/authref.py +9 -3
  157. svc_infra/db/sql/constants.py +12 -8
  158. svc_infra/db/sql/core.py +2 -2
  159. svc_infra/db/sql/management.py +11 -8
  160. svc_infra/db/sql/repository.py +99 -26
  161. svc_infra/db/sql/resource.py +5 -0
  162. svc_infra/db/sql/scaffold.py +6 -2
  163. svc_infra/db/sql/service.py +15 -5
  164. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  165. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  166. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  167. svc_infra/db/sql/tenant.py +88 -0
  168. svc_infra/db/sql/uniq_hooks.py +9 -3
  169. svc_infra/db/sql/utils.py +138 -51
  170. svc_infra/db/sql/versioning.py +14 -0
  171. svc_infra/deploy/__init__.py +538 -0
  172. svc_infra/documents/__init__.py +100 -0
  173. svc_infra/documents/add.py +264 -0
  174. svc_infra/documents/ease.py +233 -0
  175. svc_infra/documents/models.py +114 -0
  176. svc_infra/documents/storage.py +264 -0
  177. svc_infra/dx/add.py +65 -0
  178. svc_infra/dx/changelog.py +74 -0
  179. svc_infra/dx/checks.py +68 -0
  180. svc_infra/exceptions.py +141 -0
  181. svc_infra/health/__init__.py +864 -0
  182. svc_infra/http/__init__.py +13 -0
  183. svc_infra/http/client.py +105 -0
  184. svc_infra/jobs/builtins/outbox_processor.py +40 -0
  185. svc_infra/jobs/builtins/webhook_delivery.py +95 -0
  186. svc_infra/jobs/easy.py +33 -0
  187. svc_infra/jobs/loader.py +50 -0
  188. svc_infra/jobs/queue.py +116 -0
  189. svc_infra/jobs/redis_queue.py +256 -0
  190. svc_infra/jobs/runner.py +79 -0
  191. svc_infra/jobs/scheduler.py +53 -0
  192. svc_infra/jobs/worker.py +40 -0
  193. svc_infra/loaders/__init__.py +186 -0
  194. svc_infra/loaders/base.py +142 -0
  195. svc_infra/loaders/github.py +311 -0
  196. svc_infra/loaders/models.py +147 -0
  197. svc_infra/loaders/url.py +235 -0
  198. svc_infra/logging/__init__.py +374 -0
  199. svc_infra/mcp/svc_infra_mcp.py +91 -33
  200. svc_infra/obs/README.md +2 -0
  201. svc_infra/obs/add.py +65 -9
  202. svc_infra/obs/cloud_dash.py +2 -1
  203. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  204. svc_infra/obs/metrics/__init__.py +3 -4
  205. svc_infra/obs/metrics/asgi.py +13 -7
  206. svc_infra/obs/metrics/http.py +9 -5
  207. svc_infra/obs/metrics/sqlalchemy.py +13 -9
  208. svc_infra/obs/metrics.py +6 -5
  209. svc_infra/obs/settings.py +6 -2
  210. svc_infra/security/add.py +217 -0
  211. svc_infra/security/audit.py +92 -10
  212. svc_infra/security/audit_service.py +4 -3
  213. svc_infra/security/headers.py +15 -2
  214. svc_infra/security/hibp.py +14 -4
  215. svc_infra/security/jwt_rotation.py +74 -22
  216. svc_infra/security/lockout.py +11 -5
  217. svc_infra/security/models.py +54 -12
  218. svc_infra/security/oauth_models.py +73 -0
  219. svc_infra/security/org_invites.py +5 -3
  220. svc_infra/security/passwords.py +3 -1
  221. svc_infra/security/permissions.py +25 -2
  222. svc_infra/security/session.py +1 -1
  223. svc_infra/security/signed_cookies.py +21 -1
  224. svc_infra/storage/__init__.py +93 -0
  225. svc_infra/storage/add.py +253 -0
  226. svc_infra/storage/backends/__init__.py +11 -0
  227. svc_infra/storage/backends/local.py +339 -0
  228. svc_infra/storage/backends/memory.py +216 -0
  229. svc_infra/storage/backends/s3.py +353 -0
  230. svc_infra/storage/base.py +239 -0
  231. svc_infra/storage/easy.py +185 -0
  232. svc_infra/storage/settings.py +195 -0
  233. svc_infra/testing/__init__.py +685 -0
  234. svc_infra/utils.py +7 -3
  235. svc_infra/webhooks/__init__.py +69 -0
  236. svc_infra/webhooks/add.py +339 -0
  237. svc_infra/webhooks/encryption.py +115 -0
  238. svc_infra/webhooks/fastapi.py +39 -0
  239. svc_infra/webhooks/router.py +55 -0
  240. svc_infra/webhooks/service.py +70 -0
  241. svc_infra/webhooks/signing.py +34 -0
  242. svc_infra/websocket/__init__.py +79 -0
  243. svc_infra/websocket/add.py +140 -0
  244. svc_infra/websocket/client.py +282 -0
  245. svc_infra/websocket/config.py +69 -0
  246. svc_infra/websocket/easy.py +76 -0
  247. svc_infra/websocket/exceptions.py +61 -0
  248. svc_infra/websocket/manager.py +344 -0
  249. svc_infra/websocket/models.py +49 -0
  250. svc_infra-0.1.706.dist-info/LICENSE +21 -0
  251. svc_infra-0.1.706.dist-info/METADATA +356 -0
  252. svc_infra-0.1.706.dist-info/RECORD +357 -0
  253. svc_infra-0.1.595.dist-info/METADATA +0 -80
  254. svc_infra-0.1.595.dist-info/RECORD +0 -253
  255. {svc_infra-0.1.595.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
  256. {svc_infra-0.1.595.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  from datetime import date, datetime, timezone
5
- from typing import Any, Optional, Sequence, Tuple
5
+ from typing import Any, Literal, Optional, Sequence, Tuple, cast
6
6
 
7
7
  from svc_infra.apf_payments.schemas import (
8
8
  BalanceAmount,
@@ -43,9 +43,9 @@ from svc_infra.apf_payments.settings import get_payments_settings
43
43
  from .base import ProviderAdapter
44
44
 
45
45
  try: # pragma: no cover - optional dependency
46
- import aiydan # type: ignore
46
+ import aiydan
47
47
  except Exception: # pragma: no cover - handled at runtime
48
- aiydan = None # type: ignore
48
+ aiydan = None
49
49
 
50
50
 
51
51
  async def _maybe_await(result: Any) -> Any:
@@ -62,24 +62,27 @@ def _coerce_id(data: dict[str, Any], *candidates: str) -> str:
62
62
  raise RuntimeError(f"Aiydan payload missing id fields: {candidates}")
63
63
 
64
64
 
65
- def _ensure_utc_isoformat(value: Any) -> Optional[str]:
65
+ def _ensure_utc_isoformat(value: Any) -> str | None:
66
66
  if value is None:
67
67
  return None
68
68
  if isinstance(value, str):
69
69
  return value
70
70
  if isinstance(value, datetime):
71
- if value.tzinfo is None:
72
- value = value.replace(tzinfo=timezone.utc)
73
- return value.astimezone(timezone.utc).isoformat()
71
+ dt: datetime = value
72
+ if dt.tzinfo is None:
73
+ dt = dt.replace(tzinfo=timezone.utc)
74
+ return dt.astimezone(timezone.utc).isoformat()
74
75
  if isinstance(value, date):
75
- return datetime(value.year, value.month, value.day, tzinfo=timezone.utc).isoformat()
76
+ return datetime(
77
+ value.year, value.month, value.day, tzinfo=timezone.utc
78
+ ).isoformat()
76
79
  try:
77
80
  parsed = datetime.fromisoformat(str(value))
78
81
  if parsed.tzinfo is None:
79
82
  parsed = parsed.replace(tzinfo=timezone.utc)
80
83
  return parsed.astimezone(timezone.utc).isoformat()
81
84
  except Exception:
82
- return str(value)
85
+ return cast(str, str(value)) # Cast needed since value is Any
83
86
 
84
87
 
85
88
  def _customer_to_out(data: dict[str, Any]) -> CustomerOut:
@@ -113,7 +116,9 @@ def _payment_method_to_out(data: dict[str, Any]) -> PaymentMethodOut:
113
116
  return PaymentMethodOut(
114
117
  id=method_id,
115
118
  provider="aiydan",
116
- provider_customer_id=str(data.get("provider_customer_id") or data.get("customer_id") or ""),
119
+ provider_customer_id=str(
120
+ data.get("provider_customer_id") or data.get("customer_id") or ""
121
+ ),
117
122
  provider_method_id=method_id,
118
123
  brand=card.get("brand") or data.get("brand"),
119
124
  last4=card.get("last4") or data.get("last4"),
@@ -194,7 +199,9 @@ def _invoice_to_out(data: dict[str, Any]) -> InvoiceOut:
194
199
  id=invoice_id,
195
200
  provider="aiydan",
196
201
  provider_invoice_id=invoice_id,
197
- provider_customer_id=str(data.get("provider_customer_id") or data.get("customer_id") or ""),
202
+ provider_customer_id=str(
203
+ data.get("provider_customer_id") or data.get("customer_id") or ""
204
+ ),
198
205
  status=str(data.get("status", "")),
199
206
  amount_due=int(data.get("amount_due", data.get("amount") or 0) or 0),
200
207
  currency=str(data.get("currency", "")).upper(),
@@ -208,12 +215,15 @@ def _invoice_line_item_to_out(data: dict[str, Any]) -> InvoiceLineItemOut:
208
215
  price = data.get("price") or {}
209
216
  if not isinstance(price, dict):
210
217
  price = {"id": getattr(price, "id", None)}
218
+ quantity = int(data.get("quantity", 0) or 0)
219
+ unit_amount = int(data.get("unit_amount", 0) or 0)
220
+ amount = int(data.get("amount", unit_amount * quantity) or 0)
211
221
  return InvoiceLineItemOut(
212
222
  id=line_id,
213
223
  description=data.get("description"),
214
224
  currency=str(data.get("currency", price.get("currency", ""))).upper(),
215
- quantity=int(data.get("quantity", 0) or 0),
216
- unit_amount=int(data.get("unit_amount", data.get("amount", 0) or 0)),
225
+ quantity=quantity,
226
+ amount=amount,
217
227
  provider_price_id=price.get("id"),
218
228
  )
219
229
 
@@ -225,7 +235,9 @@ def _refund_to_out(data: dict[str, Any]) -> RefundOut:
225
235
  provider="aiydan",
226
236
  provider_refund_id=refund_id,
227
237
  provider_payment_intent_id=str(
228
- data.get("provider_payment_intent_id") or data.get("payment_intent_id") or ""
238
+ data.get("provider_payment_intent_id")
239
+ or data.get("payment_intent_id")
240
+ or ""
229
241
  ),
230
242
  amount=int(data.get("amount", 0) or 0),
231
243
  currency=str(data.get("currency", "")).upper(),
@@ -268,17 +280,25 @@ def _payout_to_out(data: dict[str, Any]) -> PayoutOut:
268
280
 
269
281
 
270
282
  def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
283
+ action_raw = data.get("action")
284
+ action: Literal["increment", "set"] | None = None
285
+ if action_raw in ("increment", "set"):
286
+ action = cast(Literal["increment", "set"], action_raw)
271
287
  return UsageRecordOut(
272
288
  id=str(data.get("id")),
273
289
  quantity=int(data.get("quantity", 0) or 0),
274
290
  timestamp=data.get("timestamp"),
275
291
  subscription_item=(
276
- str(data.get("subscription_item")) if data.get("subscription_item") else None
292
+ str(data.get("subscription_item"))
293
+ if data.get("subscription_item")
294
+ else None
277
295
  ),
278
296
  provider_price_id=(
279
- str(data.get("provider_price_id")) if data.get("provider_price_id") else None
297
+ str(data.get("provider_price_id"))
298
+ if data.get("provider_price_id")
299
+ else None
280
300
  ),
281
- action=(str(data.get("action")) if data.get("action") else None),
301
+ action=action,
282
302
  )
283
303
 
284
304
 
@@ -297,7 +317,8 @@ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
297
317
  return out
298
318
  if isinstance(side, dict):
299
319
  return [
300
- {"currency": str(cur).upper(), "amount": int(amt or 0)} for cur, amt in side.items()
320
+ {"currency": str(cur).upper(), "amount": int(amt or 0)}
321
+ for cur, amt in side.items()
301
322
  ]
302
323
  return []
303
324
 
@@ -315,26 +336,28 @@ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
315
336
 
316
337
  def _ensure_sequence(result: Any) -> Sequence[dict[str, Any]]:
317
338
  if isinstance(result, Sequence):
318
- return result # type: ignore[arg-type]
339
+ return result
319
340
  if isinstance(result, dict):
320
341
  items = result.get("items")
321
342
  if isinstance(items, Sequence):
322
- return items # type: ignore[arg-type]
343
+ return items
323
344
  raise RuntimeError("Expected sequence payload from Aiydan client")
324
345
 
325
346
 
326
- def _ensure_list_response(result: Any) -> Tuple[Sequence[dict[str, Any]], Optional[str]]:
347
+ def _ensure_list_response(
348
+ result: Any,
349
+ ) -> Tuple[Sequence[dict[str, Any]], Optional[str]]:
327
350
  if isinstance(result, tuple) and len(result) == 2:
328
351
  items, cursor = result
329
352
  if isinstance(items, Sequence) or items is None:
330
- return (items or []), cursor # type: ignore[arg-type]
353
+ return (items or []), cursor
331
354
  if isinstance(result, dict):
332
355
  items = result.get("items")
333
356
  cursor = result.get("next_cursor") or result.get("cursor")
334
357
  if isinstance(items, Sequence):
335
358
  return items, cursor
336
359
  if isinstance(result, Sequence):
337
- return result, None # type: ignore[arg-type]
360
+ return result, None
338
361
  raise RuntimeError("Expected iterable response from Aiydan client")
339
362
 
340
363
 
@@ -347,7 +370,9 @@ class AiydanAdapter(ProviderAdapter):
347
370
  if client is not None:
348
371
  self._client = client
349
372
  self._webhook_secret = (
350
- cfg.webhook_secret.get_secret_value() if cfg and cfg.webhook_secret else None
373
+ cfg.webhook_secret.get_secret_value()
374
+ if cfg and cfg.webhook_secret
375
+ else None
351
376
  )
352
377
  return
353
378
  if cfg is None:
@@ -367,32 +392,44 @@ class AiydanAdapter(ProviderAdapter):
367
392
  if cfg.base_url:
368
393
  kwargs["base_url"] = cfg.base_url
369
394
  self._client = client_class(**kwargs)
370
- self._webhook_secret = cfg.webhook_secret.get_secret_value() if cfg.webhook_secret else None
395
+ self._webhook_secret = (
396
+ cfg.webhook_secret.get_secret_value() if cfg.webhook_secret else None
397
+ )
371
398
 
372
399
  async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
373
400
  payload = data.model_dump(exclude_none=True)
374
401
  result = await _maybe_await(self._client.ensure_customer(payload))
375
402
  return _customer_to_out(result)
376
403
 
377
- async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
404
+ async def attach_payment_method(
405
+ self, data: PaymentMethodAttachIn
406
+ ) -> PaymentMethodOut:
378
407
  payload = data.model_dump(exclude_none=True)
379
408
  result = await _maybe_await(self._client.attach_payment_method(payload))
380
409
  return _payment_method_to_out(result)
381
410
 
382
- async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
383
- result = await _maybe_await(self._client.list_payment_methods(provider_customer_id))
411
+ async def list_payment_methods(
412
+ self, provider_customer_id: str
413
+ ) -> list[PaymentMethodOut]:
414
+ result = await _maybe_await(
415
+ self._client.list_payment_methods(provider_customer_id)
416
+ )
384
417
  methods = _ensure_sequence(result)
385
418
  return [_payment_method_to_out(method) for method in methods]
386
419
 
387
420
  async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
388
- result = await _maybe_await(self._client.detach_payment_method(provider_method_id))
421
+ result = await _maybe_await(
422
+ self._client.detach_payment_method(provider_method_id)
423
+ )
389
424
  return _payment_method_to_out(result)
390
425
 
391
426
  async def set_default_payment_method(
392
427
  self, provider_customer_id: str, provider_method_id: str
393
428
  ) -> PaymentMethodOut:
394
429
  result = await _maybe_await(
395
- self._client.set_default_payment_method(provider_customer_id, provider_method_id)
430
+ self._client.set_default_payment_method(
431
+ provider_customer_id, provider_method_id
432
+ )
396
433
  )
397
434
  return _payment_method_to_out(result)
398
435
 
@@ -404,7 +441,9 @@ class AiydanAdapter(ProviderAdapter):
404
441
  self, provider_method_id: str, data: PaymentMethodUpdateIn
405
442
  ) -> PaymentMethodOut:
406
443
  payload = data.model_dump(exclude_none=True)
407
- result = await _maybe_await(self._client.update_payment_method(provider_method_id, payload))
444
+ result = await _maybe_await(
445
+ self._client.update_payment_method(provider_method_id, payload)
446
+ )
408
447
  return _payment_method_to_out(result)
409
448
 
410
449
  async def create_product(self, data: ProductCreateIn) -> ProductOut:
@@ -425,9 +464,13 @@ class AiydanAdapter(ProviderAdapter):
425
464
  items, next_cursor = _ensure_list_response(result)
426
465
  return [_product_to_out(item) for item in items], next_cursor
427
466
 
428
- async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
467
+ async def update_product(
468
+ self, provider_product_id: str, data: ProductUpdateIn
469
+ ) -> ProductOut:
429
470
  payload = data.model_dump(exclude_none=True)
430
- result = await _maybe_await(self._client.update_product(provider_product_id, payload))
471
+ result = await _maybe_await(
472
+ self._client.update_product(provider_product_id, payload)
473
+ )
431
474
  return _product_to_out(result)
432
475
 
433
476
  async def create_price(self, data: PriceCreateIn) -> PriceOut:
@@ -458,9 +501,13 @@ class AiydanAdapter(ProviderAdapter):
458
501
  items, next_cursor = _ensure_list_response(result)
459
502
  return [_price_to_out(item) for item in items], next_cursor
460
503
 
461
- async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
504
+ async def update_price(
505
+ self, provider_price_id: str, data: PriceUpdateIn
506
+ ) -> PriceOut:
462
507
  payload = data.model_dump(exclude_none=True)
463
- result = await _maybe_await(self._client.update_price(provider_price_id, payload))
508
+ result = await _maybe_await(
509
+ self._client.update_price(provider_price_id, payload)
510
+ )
464
511
  return _price_to_out(result)
465
512
 
466
513
  async def create_subscription(self, data: SubscriptionCreateIn) -> SubscriptionOut:
@@ -486,7 +533,9 @@ class AiydanAdapter(ProviderAdapter):
486
533
  return _subscription_to_out(result)
487
534
 
488
535
  async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
489
- result = await _maybe_await(self._client.get_subscription(provider_subscription_id))
536
+ result = await _maybe_await(
537
+ self._client.get_subscription(provider_subscription_id)
538
+ )
490
539
  return _subscription_to_out(result)
491
540
 
492
541
  async def list_subscriptions(
@@ -581,7 +630,9 @@ class AiydanAdapter(ProviderAdapter):
581
630
  items, next_cursor = _ensure_list_response(result)
582
631
  return [_invoice_line_item_to_out(item) for item in items], next_cursor
583
632
 
584
- async def create_intent(self, data: IntentCreateIn, *, user_id: str | None) -> IntentOut:
633
+ async def create_intent(
634
+ self, data: IntentCreateIn, *, user_id: str | None
635
+ ) -> IntentOut:
585
636
  payload = data.model_dump(exclude_none=True)
586
637
  if user_id is not None:
587
638
  payload["user_id"] = user_id
@@ -598,15 +649,21 @@ class AiydanAdapter(ProviderAdapter):
598
649
 
599
650
  async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
600
651
  payload = data.model_dump(exclude_none=True)
601
- result = await _maybe_await(self._client.refund_intent(provider_intent_id, payload))
652
+ result = await _maybe_await(
653
+ self._client.refund_intent(provider_intent_id, payload)
654
+ )
602
655
  return _intent_to_out(result)
603
656
 
604
657
  async def hydrate_intent(self, provider_intent_id: str) -> IntentOut:
605
658
  result = await _maybe_await(self._client.get_intent(provider_intent_id))
606
659
  return _intent_to_out(result)
607
660
 
608
- async def capture_intent(self, provider_intent_id: str, *, amount: int | None) -> IntentOut:
609
- result = await _maybe_await(self._client.capture_intent(provider_intent_id, amount=amount))
661
+ async def capture_intent(
662
+ self, provider_intent_id: str, *, amount: int | None
663
+ ) -> IntentOut:
664
+ result = await _maybe_await(
665
+ self._client.capture_intent(provider_intent_id, amount=amount)
666
+ )
610
667
  return _intent_to_out(result)
611
668
 
612
669
  async def list_intents(
@@ -666,7 +723,9 @@ class AiydanAdapter(ProviderAdapter):
666
723
  result = await _maybe_await(self._client.get_dispute(provider_dispute_id))
667
724
  return _dispute_to_out(result)
668
725
 
669
- async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
726
+ async def submit_dispute_evidence(
727
+ self, provider_dispute_id: str, evidence: dict
728
+ ) -> DisputeOut:
670
729
  result = await _maybe_await(
671
730
  self._client.submit_dispute_evidence(provider_dispute_id, evidence)
672
731
  )
@@ -683,7 +742,9 @@ class AiydanAdapter(ProviderAdapter):
683
742
  async def list_payouts(
684
743
  self, *, limit: int, cursor: str | None
685
744
  ) -> tuple[list[PayoutOut], str | None]:
686
- result = await _maybe_await(self._client.list_payouts(limit=limit, cursor=cursor))
745
+ result = await _maybe_await(
746
+ self._client.list_payouts(limit=limit, cursor=cursor)
747
+ )
687
748
  items, next_cursor = _ensure_list_response(result)
688
749
  return [_payout_to_out(item) for item in items], next_cursor
689
750
 
@@ -743,8 +804,12 @@ class AiydanAdapter(ProviderAdapter):
743
804
  next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
744
805
  )
745
806
 
746
- async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
747
- result = await _maybe_await(self._client.confirm_setup_intent(provider_setup_intent_id))
807
+ async def confirm_setup_intent(
808
+ self, provider_setup_intent_id: str
809
+ ) -> SetupIntentOut:
810
+ result = await _maybe_await(
811
+ self._client.confirm_setup_intent(provider_setup_intent_id)
812
+ )
748
813
  return SetupIntentOut(
749
814
  id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
750
815
  provider="aiydan",
@@ -757,7 +822,9 @@ class AiydanAdapter(ProviderAdapter):
757
822
  )
758
823
 
759
824
  async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
760
- result = await _maybe_await(self._client.get_setup_intent(provider_setup_intent_id))
825
+ result = await _maybe_await(
826
+ self._client.get_setup_intent(provider_setup_intent_id)
827
+ )
761
828
  return SetupIntentOut(
762
829
  id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
763
830
  provider="aiydan",
@@ -771,13 +838,20 @@ class AiydanAdapter(ProviderAdapter):
771
838
 
772
839
  async def resume_intent_after_action(self, provider_intent_id: str) -> IntentOut:
773
840
  if hasattr(self._client, "resume_intent_after_action"):
774
- result = await _maybe_await(self._client.resume_intent_after_action(provider_intent_id))
841
+ result = await _maybe_await(
842
+ self._client.resume_intent_after_action(provider_intent_id)
843
+ )
775
844
  else:
776
845
  result = await _maybe_await(self._client.get_intent(provider_intent_id))
777
846
  return _intent_to_out(result)
778
847
 
779
848
  async def list_customers(
780
- self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
849
+ self,
850
+ *,
851
+ provider: str | None,
852
+ user_id: str | None,
853
+ limit: int,
854
+ cursor: str | None,
781
855
  ) -> tuple[list[CustomerOut], str | None]:
782
856
  result = await _maybe_await(
783
857
  self._client.list_customers(
@@ -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 ---