svc-infra 0.1.595__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 (274) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/models.py +68 -38
  3. svc_infra/apf_payments/provider/__init__.py +2 -2
  4. svc_infra/apf_payments/provider/aiydan.py +39 -23
  5. svc_infra/apf_payments/provider/base.py +8 -3
  6. svc_infra/apf_payments/provider/registry.py +3 -5
  7. svc_infra/apf_payments/provider/stripe.py +74 -52
  8. svc_infra/apf_payments/schemas.py +84 -83
  9. svc_infra/apf_payments/service.py +27 -16
  10. svc_infra/apf_payments/settings.py +12 -11
  11. svc_infra/api/__init__.py +61 -0
  12. svc_infra/api/fastapi/__init__.py +34 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +240 -0
  15. svc_infra/api/fastapi/apf_payments/router.py +94 -73
  16. svc_infra/api/fastapi/apf_payments/setup.py +10 -9
  17. svc_infra/api/fastapi/auth/__init__.py +65 -0
  18. svc_infra/api/fastapi/auth/_cookies.py +1 -3
  19. svc_infra/api/fastapi/auth/add.py +14 -15
  20. svc_infra/api/fastapi/auth/gaurd.py +32 -20
  21. svc_infra/api/fastapi/auth/mfa/models.py +3 -4
  22. svc_infra/api/fastapi/auth/mfa/pre_auth.py +13 -9
  23. svc_infra/api/fastapi/auth/mfa/router.py +9 -8
  24. svc_infra/api/fastapi/auth/mfa/security.py +4 -7
  25. svc_infra/api/fastapi/auth/mfa/utils.py +5 -3
  26. svc_infra/api/fastapi/auth/policy.py +0 -1
  27. svc_infra/api/fastapi/auth/providers.py +3 -3
  28. svc_infra/api/fastapi/auth/routers/apikey_router.py +19 -21
  29. svc_infra/api/fastapi/auth/routers/oauth_router.py +98 -52
  30. svc_infra/api/fastapi/auth/routers/session_router.py +6 -5
  31. svc_infra/api/fastapi/auth/security.py +25 -15
  32. svc_infra/api/fastapi/auth/sender.py +5 -0
  33. svc_infra/api/fastapi/auth/settings.py +18 -19
  34. svc_infra/api/fastapi/auth/state.py +5 -4
  35. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  36. svc_infra/api/fastapi/billing/router.py +71 -0
  37. svc_infra/api/fastapi/billing/setup.py +19 -0
  38. svc_infra/api/fastapi/cache/add.py +9 -5
  39. svc_infra/api/fastapi/db/__init__.py +5 -1
  40. svc_infra/api/fastapi/db/http.py +10 -9
  41. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  42. svc_infra/api/fastapi/db/nosql/mongo/add.py +35 -30
  43. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +39 -21
  44. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  45. svc_infra/api/fastapi/db/sql/add.py +62 -25
  46. svc_infra/api/fastapi/db/sql/crud_router.py +205 -30
  47. svc_infra/api/fastapi/db/sql/session.py +19 -2
  48. svc_infra/api/fastapi/db/sql/users.py +18 -9
  49. svc_infra/api/fastapi/dependencies/ratelimit.py +76 -14
  50. svc_infra/api/fastapi/docs/add.py +163 -0
  51. svc_infra/api/fastapi/docs/landing.py +6 -6
  52. svc_infra/api/fastapi/docs/scoped.py +75 -36
  53. svc_infra/api/fastapi/dual/__init__.py +12 -2
  54. svc_infra/api/fastapi/dual/dualize.py +2 -2
  55. svc_infra/api/fastapi/dual/protected.py +123 -10
  56. svc_infra/api/fastapi/dual/public.py +25 -0
  57. svc_infra/api/fastapi/dual/router.py +18 -8
  58. svc_infra/api/fastapi/dx.py +33 -2
  59. svc_infra/api/fastapi/ease.py +59 -7
  60. svc_infra/api/fastapi/http/concurrency.py +2 -1
  61. svc_infra/api/fastapi/http/conditional.py +2 -2
  62. svc_infra/api/fastapi/middleware/debug.py +4 -1
  63. svc_infra/api/fastapi/middleware/errors/exceptions.py +2 -5
  64. svc_infra/api/fastapi/middleware/errors/handlers.py +50 -10
  65. svc_infra/api/fastapi/middleware/graceful_shutdown.py +95 -0
  66. svc_infra/api/fastapi/middleware/idempotency.py +190 -68
  67. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  68. svc_infra/api/fastapi/middleware/optimistic_lock.py +39 -0
  69. svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
  70. svc_infra/api/fastapi/middleware/ratelimit_store.py +45 -13
  71. svc_infra/api/fastapi/middleware/request_id.py +24 -10
  72. svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
  73. svc_infra/api/fastapi/middleware/timeout.py +176 -0
  74. svc_infra/api/fastapi/object_router.py +1060 -0
  75. svc_infra/api/fastapi/openapi/apply.py +4 -3
  76. svc_infra/api/fastapi/openapi/conventions.py +13 -6
  77. svc_infra/api/fastapi/openapi/mutators.py +144 -17
  78. svc_infra/api/fastapi/openapi/pipeline.py +2 -2
  79. svc_infra/api/fastapi/openapi/responses.py +4 -6
  80. svc_infra/api/fastapi/openapi/security.py +1 -1
  81. svc_infra/api/fastapi/ops/add.py +73 -0
  82. svc_infra/api/fastapi/pagination.py +47 -32
  83. svc_infra/api/fastapi/routers/__init__.py +16 -10
  84. svc_infra/api/fastapi/routers/ping.py +1 -0
  85. svc_infra/api/fastapi/setup.py +167 -54
  86. svc_infra/api/fastapi/tenancy/add.py +20 -0
  87. svc_infra/api/fastapi/tenancy/context.py +113 -0
  88. svc_infra/api/fastapi/versioned.py +102 -0
  89. svc_infra/app/README.md +5 -5
  90. svc_infra/app/__init__.py +3 -1
  91. svc_infra/app/env.py +70 -4
  92. svc_infra/app/logging/add.py +10 -2
  93. svc_infra/app/logging/filter.py +1 -1
  94. svc_infra/app/logging/formats.py +13 -5
  95. svc_infra/app/root.py +3 -3
  96. svc_infra/billing/__init__.py +40 -0
  97. svc_infra/billing/async_service.py +167 -0
  98. svc_infra/billing/jobs.py +231 -0
  99. svc_infra/billing/models.py +146 -0
  100. svc_infra/billing/quotas.py +101 -0
  101. svc_infra/billing/schemas.py +34 -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 +21 -5
  106. svc_infra/cache/add.py +167 -0
  107. svc_infra/cache/backend.py +9 -7
  108. svc_infra/cache/decorators.py +75 -20
  109. svc_infra/cache/demo.py +2 -2
  110. svc_infra/cache/keys.py +26 -6
  111. svc_infra/cache/recache.py +26 -27
  112. svc_infra/cache/resources.py +6 -5
  113. svc_infra/cache/tags.py +19 -44
  114. svc_infra/cache/ttl.py +2 -3
  115. svc_infra/cache/utils.py +4 -3
  116. svc_infra/cli/__init__.py +44 -8
  117. svc_infra/cli/__main__.py +4 -0
  118. svc_infra/cli/cmds/__init__.py +39 -2
  119. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +18 -14
  120. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +9 -10
  121. svc_infra/cli/cmds/db/ops_cmds.py +267 -0
  122. svc_infra/cli/cmds/db/sql/alembic_cmds.py +97 -29
  123. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  124. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +13 -13
  125. svc_infra/cli/cmds/docs/docs_cmds.py +139 -0
  126. svc_infra/cli/cmds/dx/__init__.py +12 -0
  127. svc_infra/cli/cmds/dx/dx_cmds.py +110 -0
  128. svc_infra/cli/cmds/health/__init__.py +179 -0
  129. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  130. svc_infra/cli/cmds/help.py +4 -0
  131. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  132. svc_infra/cli/cmds/jobs/jobs_cmds.py +42 -0
  133. svc_infra/cli/cmds/obs/obs_cmds.py +31 -13
  134. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  135. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  136. svc_infra/cli/foundation/runner.py +4 -5
  137. svc_infra/cli/foundation/typer_bootstrap.py +1 -2
  138. svc_infra/data/__init__.py +83 -0
  139. svc_infra/data/add.py +61 -0
  140. svc_infra/data/backup.py +56 -0
  141. svc_infra/data/erasure.py +46 -0
  142. svc_infra/data/fixtures.py +42 -0
  143. svc_infra/data/retention.py +56 -0
  144. svc_infra/db/__init__.py +15 -0
  145. svc_infra/db/crud_schema.py +14 -13
  146. svc_infra/db/inbox.py +67 -0
  147. svc_infra/db/nosql/__init__.py +2 -0
  148. svc_infra/db/nosql/constants.py +1 -1
  149. svc_infra/db/nosql/core.py +19 -5
  150. svc_infra/db/nosql/indexes.py +12 -9
  151. svc_infra/db/nosql/management.py +4 -4
  152. svc_infra/db/nosql/mongo/README.md +13 -13
  153. svc_infra/db/nosql/mongo/client.py +21 -4
  154. svc_infra/db/nosql/mongo/settings.py +1 -1
  155. svc_infra/db/nosql/repository.py +46 -27
  156. svc_infra/db/nosql/resource.py +28 -16
  157. svc_infra/db/nosql/scaffold.py +14 -12
  158. svc_infra/db/nosql/service.py +2 -1
  159. svc_infra/db/nosql/service_with_hooks.py +4 -3
  160. svc_infra/db/nosql/utils.py +4 -4
  161. svc_infra/db/ops.py +380 -0
  162. svc_infra/db/outbox.py +105 -0
  163. svc_infra/db/sql/apikey.py +34 -15
  164. svc_infra/db/sql/authref.py +8 -6
  165. svc_infra/db/sql/constants.py +5 -1
  166. svc_infra/db/sql/core.py +13 -13
  167. svc_infra/db/sql/management.py +5 -6
  168. svc_infra/db/sql/repository.py +92 -26
  169. svc_infra/db/sql/resource.py +18 -12
  170. svc_infra/db/sql/scaffold.py +11 -11
  171. svc_infra/db/sql/service.py +2 -1
  172. svc_infra/db/sql/service_with_hooks.py +4 -3
  173. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  174. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  175. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  176. svc_infra/db/sql/tenant.py +80 -0
  177. svc_infra/db/sql/uniq.py +8 -7
  178. svc_infra/db/sql/uniq_hooks.py +12 -11
  179. svc_infra/db/sql/utils.py +105 -47
  180. svc_infra/db/sql/versioning.py +14 -0
  181. svc_infra/db/utils.py +3 -3
  182. svc_infra/deploy/__init__.py +531 -0
  183. svc_infra/documents/__init__.py +100 -0
  184. svc_infra/documents/add.py +263 -0
  185. svc_infra/documents/ease.py +233 -0
  186. svc_infra/documents/models.py +114 -0
  187. svc_infra/documents/storage.py +262 -0
  188. svc_infra/dx/__init__.py +58 -0
  189. svc_infra/dx/add.py +63 -0
  190. svc_infra/dx/changelog.py +74 -0
  191. svc_infra/dx/checks.py +68 -0
  192. svc_infra/exceptions.py +141 -0
  193. svc_infra/health/__init__.py +863 -0
  194. svc_infra/http/__init__.py +13 -0
  195. svc_infra/http/client.py +101 -0
  196. svc_infra/jobs/__init__.py +79 -0
  197. svc_infra/jobs/builtins/outbox_processor.py +38 -0
  198. svc_infra/jobs/builtins/webhook_delivery.py +93 -0
  199. svc_infra/jobs/easy.py +33 -0
  200. svc_infra/jobs/loader.py +49 -0
  201. svc_infra/jobs/queue.py +106 -0
  202. svc_infra/jobs/redis_queue.py +242 -0
  203. svc_infra/jobs/runner.py +75 -0
  204. svc_infra/jobs/scheduler.py +53 -0
  205. svc_infra/jobs/worker.py +40 -0
  206. svc_infra/loaders/__init__.py +186 -0
  207. svc_infra/loaders/base.py +143 -0
  208. svc_infra/loaders/github.py +309 -0
  209. svc_infra/loaders/models.py +147 -0
  210. svc_infra/loaders/url.py +229 -0
  211. svc_infra/logging/__init__.py +375 -0
  212. svc_infra/mcp/__init__.py +82 -0
  213. svc_infra/mcp/svc_infra_mcp.py +91 -33
  214. svc_infra/obs/README.md +2 -0
  215. svc_infra/obs/add.py +68 -11
  216. svc_infra/obs/cloud_dash.py +2 -1
  217. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  218. svc_infra/obs/metrics/__init__.py +6 -7
  219. svc_infra/obs/metrics/asgi.py +8 -7
  220. svc_infra/obs/metrics/base.py +13 -13
  221. svc_infra/obs/metrics/http.py +3 -3
  222. svc_infra/obs/metrics/sqlalchemy.py +14 -13
  223. svc_infra/obs/metrics.py +9 -8
  224. svc_infra/resilience/__init__.py +44 -0
  225. svc_infra/resilience/circuit_breaker.py +328 -0
  226. svc_infra/resilience/retry.py +289 -0
  227. svc_infra/security/__init__.py +167 -0
  228. svc_infra/security/add.py +213 -0
  229. svc_infra/security/audit.py +97 -18
  230. svc_infra/security/audit_service.py +10 -9
  231. svc_infra/security/headers.py +15 -2
  232. svc_infra/security/hibp.py +14 -7
  233. svc_infra/security/jwt_rotation.py +78 -29
  234. svc_infra/security/lockout.py +23 -16
  235. svc_infra/security/models.py +77 -44
  236. svc_infra/security/oauth_models.py +73 -0
  237. svc_infra/security/org_invites.py +12 -12
  238. svc_infra/security/passwords.py +3 -3
  239. svc_infra/security/permissions.py +31 -7
  240. svc_infra/security/session.py +7 -8
  241. svc_infra/security/signed_cookies.py +26 -6
  242. svc_infra/storage/__init__.py +93 -0
  243. svc_infra/storage/add.py +250 -0
  244. svc_infra/storage/backends/__init__.py +11 -0
  245. svc_infra/storage/backends/local.py +331 -0
  246. svc_infra/storage/backends/memory.py +213 -0
  247. svc_infra/storage/backends/s3.py +334 -0
  248. svc_infra/storage/base.py +239 -0
  249. svc_infra/storage/easy.py +181 -0
  250. svc_infra/storage/settings.py +193 -0
  251. svc_infra/testing/__init__.py +682 -0
  252. svc_infra/utils.py +170 -5
  253. svc_infra/webhooks/__init__.py +69 -0
  254. svc_infra/webhooks/add.py +327 -0
  255. svc_infra/webhooks/encryption.py +115 -0
  256. svc_infra/webhooks/fastapi.py +37 -0
  257. svc_infra/webhooks/router.py +55 -0
  258. svc_infra/webhooks/service.py +69 -0
  259. svc_infra/webhooks/signing.py +34 -0
  260. svc_infra/websocket/__init__.py +79 -0
  261. svc_infra/websocket/add.py +139 -0
  262. svc_infra/websocket/client.py +283 -0
  263. svc_infra/websocket/config.py +57 -0
  264. svc_infra/websocket/easy.py +76 -0
  265. svc_infra/websocket/exceptions.py +61 -0
  266. svc_infra/websocket/manager.py +343 -0
  267. svc_infra/websocket/models.py +49 -0
  268. svc_infra-1.1.0.dist-info/LICENSE +21 -0
  269. svc_infra-1.1.0.dist-info/METADATA +362 -0
  270. svc_infra-1.1.0.dist-info/RECORD +364 -0
  271. svc_infra-0.1.595.dist-info/METADATA +0 -80
  272. svc_infra-0.1.595.dist-info/RECORD +0 -253
  273. {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
  274. {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
svc_infra/__init__.py CHANGED
@@ -1,3 +1,59 @@
1
- from . import api, app
1
+ """svc-infra: Service Infrastructure Toolkit.
2
2
 
3
- __all__ = ["app", "api"]
3
+ A comprehensive backend infrastructure library providing:
4
+ - API framework (FastAPI scaffolding, dual routers, auth)
5
+ - Database (SQL/Mongo, migrations, repositories)
6
+ - Caching (Redis, decorators)
7
+ - Jobs (background tasks, queues)
8
+ - Webhooks (delivery, subscriptions)
9
+ - Billing (Stripe/Adyen integration)
10
+ - Observability (logging, metrics)
11
+
12
+ Example:
13
+ from svc_infra.api.fastapi import easy_service_app
14
+ from svc_infra.api.fastapi.auth import add_auth_users
15
+
16
+ app = easy_service_app(name="MyAPI")
17
+ add_auth_users(app)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ # Core modules (lazy import pattern for optional dependencies)
23
+ from . import api, app, cache, db, jobs, webhooks
24
+
25
+ # Base exception
26
+ from .exceptions import SvcInfraError
27
+
28
+ # Content Loaders
29
+ from .loaders import (
30
+ BaseLoader,
31
+ GitHubLoader,
32
+ LoadedContent,
33
+ URLLoader,
34
+ load_github,
35
+ load_github_sync,
36
+ load_url,
37
+ load_url_sync,
38
+ )
39
+
40
+ __all__ = [
41
+ # Core modules
42
+ "api",
43
+ "app",
44
+ "cache",
45
+ "db",
46
+ "jobs",
47
+ "webhooks",
48
+ # Base exception
49
+ "SvcInfraError",
50
+ # Loaders
51
+ "BaseLoader",
52
+ "GitHubLoader",
53
+ "LoadedContent",
54
+ "URLLoader",
55
+ "load_github",
56
+ "load_github_sync",
57
+ "load_url",
58
+ "load_url_sync",
59
+ ]
@@ -1,9 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Optional
5
4
 
6
- from sqlalchemy import JSON, Boolean, DateTime, Index, Numeric, String, UniqueConstraint, text
5
+ from sqlalchemy import (
6
+ JSON,
7
+ Boolean,
8
+ DateTime,
9
+ Index,
10
+ Numeric,
11
+ String,
12
+ UniqueConstraint,
13
+ text,
14
+ )
7
15
  from sqlalchemy.orm import Mapped, mapped_column
8
16
 
9
17
  from svc_infra.db.sql.authref import user_fk_constraint, user_id_type
@@ -21,7 +29,7 @@ class PayCustomer(ModelBase):
21
29
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
22
30
 
23
31
  # Always typed to match the actual auth PK; FK is enforced at table level
24
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
32
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
25
33
 
26
34
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
27
35
  provider_customer_id: Mapped[str] = mapped_column(
@@ -29,7 +37,9 @@ class PayCustomer(ModelBase):
29
37
  )
30
38
 
31
39
  created_at: Mapped[datetime] = mapped_column(
32
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
40
+ DateTime(timezone=True),
41
+ server_default=text("CURRENT_TIMESTAMP"),
42
+ nullable=False,
33
43
  )
34
44
 
35
45
  __table_args__ = (
@@ -46,7 +56,7 @@ class PayIntent(ModelBase):
46
56
 
47
57
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
48
58
 
49
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
59
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
50
60
 
51
61
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
52
62
  provider_intent_id: Mapped[str] = mapped_column(
@@ -55,11 +65,13 @@ class PayIntent(ModelBase):
55
65
  amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
56
66
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
57
67
  status: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
58
- client_secret: Mapped[Optional[str]] = mapped_column(String(255))
68
+ client_secret: Mapped[str | None] = mapped_column(String(255))
59
69
  created_at: Mapped[datetime] = mapped_column(
60
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
70
+ DateTime(timezone=True),
71
+ server_default=text("CURRENT_TIMESTAMP"),
72
+ nullable=False,
61
73
  )
62
- confirmed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
74
+ confirmed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
63
75
  captured: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
64
76
 
65
77
  __table_args__ = (
@@ -82,7 +94,9 @@ class PayEvent(ModelBase):
82
94
  )
83
95
  type: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
84
96
  received_at: Mapped[datetime] = mapped_column(
85
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
97
+ DateTime(timezone=True),
98
+ server_default=text("CURRENT_TIMESTAMP"),
99
+ nullable=False,
86
100
  )
87
101
  payload_json: Mapped[dict] = mapped_column(JSON, nullable=False) # compact JSON string
88
102
 
@@ -102,8 +116,8 @@ class LedgerEntry(ModelBase):
102
116
  )
103
117
 
104
118
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
105
- provider_ref: Mapped[Optional[str]] = mapped_column(String(128), index=True)
106
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
119
+ provider_ref: Mapped[str | None] = mapped_column(String(128), index=True)
120
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
107
121
  amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
108
122
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
109
123
  kind: Mapped[str] = mapped_column(String(24), nullable=False) # payment|refund|fee|payout...
@@ -130,19 +144,21 @@ class PayPaymentMethod(ModelBase):
130
144
 
131
145
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
132
146
 
133
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
147
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
134
148
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
135
149
  provider_customer_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
136
150
  provider_method_id: Mapped[str] = mapped_column(
137
151
  String(128), unique=True, index=True, nullable=False
138
152
  )
139
- brand: Mapped[Optional[str]] = mapped_column(String(32))
140
- last4: Mapped[Optional[str]] = mapped_column(String(8))
141
- exp_month: Mapped[Optional[int]] = mapped_column(Numeric(2, 0))
142
- exp_year: Mapped[Optional[int]] = mapped_column(Numeric(4, 0))
153
+ brand: Mapped[str | None] = mapped_column(String(32))
154
+ last4: Mapped[str | None] = mapped_column(String(8))
155
+ exp_month: Mapped[int | None] = mapped_column(Numeric(2, 0))
156
+ exp_year: Mapped[int | None] = mapped_column(Numeric(4, 0))
143
157
  is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
144
158
  created_at: Mapped[datetime] = mapped_column(
145
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
159
+ DateTime(timezone=True),
160
+ server_default=text("CURRENT_TIMESTAMP"),
161
+ nullable=False,
146
162
  )
147
163
 
148
164
  __table_args__ = (
@@ -170,7 +186,9 @@ class PayProduct(ModelBase):
170
186
  name: Mapped[str] = mapped_column(String(128), nullable=False)
171
187
  active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
172
188
  created_at: Mapped[datetime] = mapped_column(
173
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
189
+ DateTime(timezone=True),
190
+ server_default=text("CURRENT_TIMESTAMP"),
191
+ nullable=False,
174
192
  )
175
193
 
176
194
 
@@ -188,11 +206,13 @@ class PayPrice(ModelBase):
188
206
  provider_product_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
189
207
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
190
208
  unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
191
- interval: Mapped[Optional[str]] = mapped_column(String(16)) # month|year|week|day
192
- trial_days: Mapped[Optional[int]] = mapped_column(Numeric(5, 0))
209
+ interval: Mapped[str | None] = mapped_column(String(16)) # month|year|week|day
210
+ trial_days: Mapped[int | None] = mapped_column(Numeric(5, 0))
193
211
  active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
194
212
  created_at: Mapped[datetime] = mapped_column(
195
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
213
+ DateTime(timezone=True),
214
+ server_default=text("CURRENT_TIMESTAMP"),
215
+ nullable=False,
196
216
  )
197
217
 
198
218
 
@@ -203,7 +223,7 @@ class PaySubscription(ModelBase):
203
223
 
204
224
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
205
225
 
206
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
226
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
207
227
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
208
228
  provider_customer_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
209
229
  provider_subscription_id: Mapped[str] = mapped_column(
@@ -215,9 +235,11 @@ class PaySubscription(ModelBase):
215
235
  ) # active|trialing|canceled|past_due|incomplete
216
236
  quantity: Mapped[int] = mapped_column(Numeric(10, 0), default=1, nullable=False)
217
237
  cancel_at_period_end: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
218
- current_period_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
238
+ current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
219
239
  created_at: Mapped[datetime] = mapped_column(
220
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
240
+ DateTime(timezone=True),
241
+ server_default=text("CURRENT_TIMESTAMP"),
242
+ nullable=False,
221
243
  )
222
244
 
223
245
  __table_args__ = (
@@ -238,7 +260,7 @@ class PayInvoice(ModelBase):
238
260
 
239
261
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
240
262
 
241
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
263
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
242
264
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
243
265
  provider_invoice_id: Mapped[str] = mapped_column(
244
266
  String(128), unique=True, index=True, nullable=False
@@ -249,10 +271,12 @@ class PayInvoice(ModelBase):
249
271
  ) # draft|open|paid|void|uncollectible
250
272
  amount_due: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
251
273
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
252
- hosted_invoice_url: Mapped[Optional[str]] = mapped_column(String(255))
253
- pdf_url: Mapped[Optional[str]] = mapped_column(String(255))
274
+ hosted_invoice_url: Mapped[str | None] = mapped_column(String(255))
275
+ pdf_url: Mapped[str | None] = mapped_column(String(255))
254
276
  created_at: Mapped[datetime] = mapped_column(
255
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
277
+ DateTime(timezone=True),
278
+ server_default=text("CURRENT_TIMESTAMP"),
279
+ nullable=False,
256
280
  )
257
281
 
258
282
  __table_args__ = (
@@ -273,7 +297,7 @@ class PaySetupIntent(ModelBase):
273
297
 
274
298
  tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
275
299
 
276
- user_id: Mapped[Optional[str]] = mapped_column(user_id_type(), index=True, nullable=True)
300
+ user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
277
301
  provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
278
302
  provider_setup_intent_id: Mapped[str] = mapped_column(
279
303
  String(128), unique=True, index=True, nullable=False
@@ -281,9 +305,11 @@ class PaySetupIntent(ModelBase):
281
305
  status: Mapped[str] = mapped_column(
282
306
  String(32), index=True, nullable=False
283
307
  ) # requires_action|succeeded|canceled|processing
284
- client_secret: Mapped[Optional[str]] = mapped_column(String(255))
308
+ client_secret: Mapped[str | None] = mapped_column(String(255))
285
309
  created_at: Mapped[datetime] = mapped_column(
286
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
310
+ DateTime(timezone=True),
311
+ server_default=text("CURRENT_TIMESTAMP"),
312
+ nullable=False,
287
313
  )
288
314
 
289
315
  __table_args__ = (
@@ -303,16 +329,18 @@ class PayDispute(ModelBase):
303
329
  provider_dispute_id: Mapped[str] = mapped_column(
304
330
  String(128), unique=True, index=True, nullable=False
305
331
  )
306
- provider_charge_id: Mapped[Optional[str]] = mapped_column(String(128), index=True)
332
+ provider_charge_id: Mapped[str | None] = mapped_column(String(128), index=True)
307
333
  amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
308
334
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
309
- reason: Mapped[Optional[str]] = mapped_column(String(64))
335
+ reason: Mapped[str | None] = mapped_column(String(64))
310
336
  status: Mapped[str] = mapped_column(
311
337
  String(32), index=True, nullable=False
312
338
  ) # needs_response|under_review|won|lost|warning_closed
313
- evidence_due_by: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
339
+ evidence_due_by: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
314
340
  created_at: Mapped[datetime] = mapped_column(
315
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
341
+ DateTime(timezone=True),
342
+ server_default=text("CURRENT_TIMESTAMP"),
343
+ nullable=False,
316
344
  )
317
345
 
318
346
 
@@ -332,8 +360,10 @@ class PayPayout(ModelBase):
332
360
  status: Mapped[str] = mapped_column(
333
361
  String(32), index=True, nullable=False
334
362
  ) # pending|in_transit|paid|canceled|failed
335
- arrival_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
336
- type: Mapped[Optional[str]] = mapped_column(String(32)) # bank_account|card|...
363
+ arrival_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
364
+ type: Mapped[str | None] = mapped_column(String(32)) # bank_account|card|...
337
365
  created_at: Mapped[datetime] = mapped_column(
338
- DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
366
+ DateTime(timezone=True),
367
+ server_default=text("CURRENT_TIMESTAMP"),
368
+ nullable=False,
339
369
  )
@@ -1,4 +1,4 @@
1
- from .aiydan import AiydanAdapter # noqa: F401
2
- from .stripe import StripeAdapter # noqa: F401
1
+ from .aiydan import AiydanAdapter
2
+ from .stripe import StripeAdapter
3
3
 
4
4
  __all__ = ["AiydanAdapter", "StripeAdapter"]
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
- from datetime import date, datetime, timezone
5
- from typing import Any, Optional, Sequence, Tuple
4
+ from collections.abc import Sequence
5
+ from datetime import UTC, date, datetime
6
+ from typing import Any, Literal, cast
6
7
 
7
8
  from svc_infra.apf_payments.schemas import (
8
9
  BalanceAmount,
@@ -43,9 +44,9 @@ from svc_infra.apf_payments.settings import get_payments_settings
43
44
  from .base import ProviderAdapter
44
45
 
45
46
  try: # pragma: no cover - optional dependency
46
- import aiydan # type: ignore
47
+ import aiydan
47
48
  except Exception: # pragma: no cover - handled at runtime
48
- aiydan = None # type: ignore
49
+ aiydan = None
49
50
 
50
51
 
51
52
  async def _maybe_await(result: Any) -> Any:
@@ -62,24 +63,25 @@ def _coerce_id(data: dict[str, Any], *candidates: str) -> str:
62
63
  raise RuntimeError(f"Aiydan payload missing id fields: {candidates}")
63
64
 
64
65
 
65
- def _ensure_utc_isoformat(value: Any) -> Optional[str]:
66
+ def _ensure_utc_isoformat(value: Any) -> str | None:
66
67
  if value is None:
67
68
  return None
68
69
  if isinstance(value, str):
69
70
  return value
70
71
  if isinstance(value, datetime):
71
- if value.tzinfo is None:
72
- value = value.replace(tzinfo=timezone.utc)
73
- return value.astimezone(timezone.utc).isoformat()
72
+ dt: datetime = value
73
+ if dt.tzinfo is None:
74
+ dt = dt.replace(tzinfo=UTC)
75
+ return dt.astimezone(UTC).isoformat()
74
76
  if isinstance(value, date):
75
- return datetime(value.year, value.month, value.day, tzinfo=timezone.utc).isoformat()
77
+ return datetime(value.year, value.month, value.day, tzinfo=UTC).isoformat()
76
78
  try:
77
79
  parsed = datetime.fromisoformat(str(value))
78
80
  if parsed.tzinfo is None:
79
- parsed = parsed.replace(tzinfo=timezone.utc)
80
- return parsed.astimezone(timezone.utc).isoformat()
81
+ parsed = parsed.replace(tzinfo=UTC)
82
+ return parsed.astimezone(UTC).isoformat()
81
83
  except Exception:
82
- return str(value)
84
+ return cast("str", str(value)) # Cast needed since value is Any
83
85
 
84
86
 
85
87
  def _customer_to_out(data: dict[str, Any]) -> CustomerOut:
@@ -208,12 +210,15 @@ def _invoice_line_item_to_out(data: dict[str, Any]) -> InvoiceLineItemOut:
208
210
  price = data.get("price") or {}
209
211
  if not isinstance(price, dict):
210
212
  price = {"id": getattr(price, "id", None)}
213
+ quantity = int(data.get("quantity", 0) or 0)
214
+ unit_amount = int(data.get("unit_amount", 0) or 0)
215
+ amount = int(data.get("amount", unit_amount * quantity) or 0)
211
216
  return InvoiceLineItemOut(
212
217
  id=line_id,
213
218
  description=data.get("description"),
214
219
  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)),
220
+ quantity=quantity,
221
+ amount=amount,
217
222
  provider_price_id=price.get("id"),
218
223
  )
219
224
 
@@ -268,6 +273,10 @@ def _payout_to_out(data: dict[str, Any]) -> PayoutOut:
268
273
 
269
274
 
270
275
  def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
276
+ action_raw = data.get("action")
277
+ action: Literal["increment", "set"] | None = None
278
+ if action_raw in ("increment", "set"):
279
+ action = cast("Literal['increment', 'set']", action_raw)
271
280
  return UsageRecordOut(
272
281
  id=str(data.get("id")),
273
282
  quantity=int(data.get("quantity", 0) or 0),
@@ -278,7 +287,7 @@ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
278
287
  provider_price_id=(
279
288
  str(data.get("provider_price_id")) if data.get("provider_price_id") else None
280
289
  ),
281
- action=(str(data.get("action")) if data.get("action") else None),
290
+ action=action,
282
291
  )
283
292
 
284
293
 
@@ -315,33 +324,35 @@ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
315
324
 
316
325
  def _ensure_sequence(result: Any) -> Sequence[dict[str, Any]]:
317
326
  if isinstance(result, Sequence):
318
- return result # type: ignore[arg-type]
327
+ return result
319
328
  if isinstance(result, dict):
320
329
  items = result.get("items")
321
330
  if isinstance(items, Sequence):
322
- return items # type: ignore[arg-type]
331
+ return items
323
332
  raise RuntimeError("Expected sequence payload from Aiydan client")
324
333
 
325
334
 
326
- def _ensure_list_response(result: Any) -> Tuple[Sequence[dict[str, Any]], Optional[str]]:
335
+ def _ensure_list_response(
336
+ result: Any,
337
+ ) -> tuple[Sequence[dict[str, Any]], str | None]:
327
338
  if isinstance(result, tuple) and len(result) == 2:
328
339
  items, cursor = result
329
340
  if isinstance(items, Sequence) or items is None:
330
- return (items or []), cursor # type: ignore[arg-type]
341
+ return (items or []), cursor
331
342
  if isinstance(result, dict):
332
343
  items = result.get("items")
333
344
  cursor = result.get("next_cursor") or result.get("cursor")
334
345
  if isinstance(items, Sequence):
335
346
  return items, cursor
336
347
  if isinstance(result, Sequence):
337
- return result, None # type: ignore[arg-type]
348
+ return result, None
338
349
  raise RuntimeError("Expected iterable response from Aiydan client")
339
350
 
340
351
 
341
352
  class AiydanAdapter(ProviderAdapter):
342
353
  name = "aiydan"
343
354
 
344
- def __init__(self, *, client: Optional[Any] = None):
355
+ def __init__(self, *, client: Any | None = None):
345
356
  settings = get_payments_settings()
346
357
  cfg = settings.aiydan
347
358
  if client is not None:
@@ -777,7 +788,12 @@ class AiydanAdapter(ProviderAdapter):
777
788
  return _intent_to_out(result)
778
789
 
779
790
  async def list_customers(
780
- self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
791
+ self,
792
+ *,
793
+ provider: str | None,
794
+ user_id: str | None,
795
+ limit: int,
796
+ cursor: str | None,
781
797
  ) -> tuple[list[CustomerOut], str | None]:
782
798
  result = await _maybe_await(
783
799
  self._client.list_customers(
@@ -790,7 +806,7 @@ class AiydanAdapter(ProviderAdapter):
790
806
  items, next_cursor = _ensure_list_response(result)
791
807
  return [_customer_to_out(item) for item in items], next_cursor
792
808
 
793
- async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
809
+ async def get_customer(self, provider_customer_id: str) -> CustomerOut | None:
794
810
  result = await _maybe_await(self._client.get_customer(provider_customer_id))
795
811
  if result is None:
796
812
  return None
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Optional, Protocol
3
+ from typing import Any, Protocol
4
4
 
5
5
  from ..schemas import (
6
6
  BalanceSnapshotOut,
@@ -186,12 +186,17 @@ class ProviderAdapter(Protocol):
186
186
 
187
187
  # --- Customers ---
188
188
  async def list_customers(
189
- self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
189
+ self,
190
+ *,
191
+ provider: str | None,
192
+ user_id: str | None,
193
+ limit: int,
194
+ cursor: str | None,
190
195
  ) -> tuple[list[CustomerOut], str | None]:
191
196
  """Optional: if not implemented, the service will list from local DB."""
192
197
  pass
193
198
 
194
- async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
199
+ async def get_customer(self, provider_customer_id: str) -> CustomerOut | None:
195
200
  pass
196
201
 
197
202
  # --- Products / Prices ---
@@ -1,19 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Dict, Optional
4
-
5
3
  from ..settings import get_payments_settings
6
4
  from .base import ProviderAdapter
7
5
 
8
6
 
9
7
  class ProviderRegistry:
10
8
  def __init__(self):
11
- self._adapters: Dict[str, ProviderAdapter] = {}
9
+ self._adapters: dict[str, ProviderAdapter] = {}
12
10
 
13
11
  def register(self, adapter: ProviderAdapter):
14
12
  self._adapters[adapter.name] = adapter
15
13
 
16
- def get(self, name: Optional[str] = None) -> ProviderAdapter:
14
+ def get(self, name: str | None = None) -> ProviderAdapter:
17
15
  settings = get_payments_settings()
18
16
  key = (name or settings.default_provider).lower()
19
17
  if key not in self._adapters:
@@ -21,7 +19,7 @@ class ProviderRegistry:
21
19
  return self._adapters[key]
22
20
 
23
21
 
24
- _REGISTRY: Optional[ProviderRegistry] = None
22
+ _REGISTRY: ProviderRegistry | None = None
25
23
 
26
24
 
27
25
  def get_provider_registry() -> ProviderRegistry: