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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (227) hide show
  1. svc_infra/apf_payments/models.py +47 -108
  2. svc_infra/apf_payments/provider/__init__.py +2 -2
  3. svc_infra/apf_payments/provider/aiydan.py +42 -100
  4. svc_infra/apf_payments/provider/base.py +10 -26
  5. svc_infra/apf_payments/provider/registry.py +3 -5
  6. svc_infra/apf_payments/provider/stripe.py +63 -135
  7. svc_infra/apf_payments/schemas.py +82 -90
  8. svc_infra/apf_payments/service.py +40 -86
  9. svc_infra/apf_payments/settings.py +10 -13
  10. svc_infra/api/__init__.py +13 -13
  11. svc_infra/api/fastapi/__init__.py +19 -0
  12. svc_infra/api/fastapi/admin/add.py +13 -18
  13. svc_infra/api/fastapi/apf_payments/router.py +47 -84
  14. svc_infra/api/fastapi/apf_payments/setup.py +7 -13
  15. svc_infra/api/fastapi/auth/__init__.py +1 -1
  16. svc_infra/api/fastapi/auth/_cookies.py +3 -9
  17. svc_infra/api/fastapi/auth/add.py +4 -8
  18. svc_infra/api/fastapi/auth/gaurd.py +9 -26
  19. svc_infra/api/fastapi/auth/mfa/models.py +4 -7
  20. svc_infra/api/fastapi/auth/mfa/pre_auth.py +3 -3
  21. svc_infra/api/fastapi/auth/mfa/router.py +9 -15
  22. svc_infra/api/fastapi/auth/mfa/security.py +3 -5
  23. svc_infra/api/fastapi/auth/mfa/utils.py +3 -2
  24. svc_infra/api/fastapi/auth/mfa/verify.py +2 -9
  25. svc_infra/api/fastapi/auth/providers.py +4 -6
  26. svc_infra/api/fastapi/auth/routers/apikey_router.py +16 -18
  27. svc_infra/api/fastapi/auth/routers/oauth_router.py +37 -85
  28. svc_infra/api/fastapi/auth/routers/session_router.py +3 -6
  29. svc_infra/api/fastapi/auth/security.py +17 -28
  30. svc_infra/api/fastapi/auth/sender.py +1 -3
  31. svc_infra/api/fastapi/auth/settings.py +18 -19
  32. svc_infra/api/fastapi/auth/state.py +6 -7
  33. svc_infra/api/fastapi/auth/ws_security.py +2 -2
  34. svc_infra/api/fastapi/billing/router.py +6 -8
  35. svc_infra/api/fastapi/db/http.py +10 -11
  36. svc_infra/api/fastapi/db/nosql/mongo/add.py +5 -15
  37. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +14 -15
  38. svc_infra/api/fastapi/db/sql/add.py +6 -14
  39. svc_infra/api/fastapi/db/sql/crud_router.py +27 -40
  40. svc_infra/api/fastapi/db/sql/health.py +1 -3
  41. svc_infra/api/fastapi/db/sql/session.py +4 -5
  42. svc_infra/api/fastapi/db/sql/users.py +8 -11
  43. svc_infra/api/fastapi/dependencies/ratelimit.py +4 -6
  44. svc_infra/api/fastapi/docs/add.py +13 -23
  45. svc_infra/api/fastapi/docs/landing.py +6 -8
  46. svc_infra/api/fastapi/docs/scoped.py +34 -42
  47. svc_infra/api/fastapi/dual/dualize.py +1 -1
  48. svc_infra/api/fastapi/dual/protected.py +12 -21
  49. svc_infra/api/fastapi/dual/router.py +14 -31
  50. svc_infra/api/fastapi/ease.py +57 -13
  51. svc_infra/api/fastapi/http/conditional.py +3 -5
  52. svc_infra/api/fastapi/middleware/errors/catchall.py +2 -6
  53. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -4
  54. svc_infra/api/fastapi/middleware/errors/handlers.py +12 -18
  55. svc_infra/api/fastapi/middleware/graceful_shutdown.py +4 -13
  56. svc_infra/api/fastapi/middleware/idempotency.py +11 -16
  57. svc_infra/api/fastapi/middleware/idempotency_store.py +14 -14
  58. svc_infra/api/fastapi/middleware/optimistic_lock.py +5 -8
  59. svc_infra/api/fastapi/middleware/ratelimit.py +8 -8
  60. svc_infra/api/fastapi/middleware/ratelimit_store.py +7 -8
  61. svc_infra/api/fastapi/middleware/request_id.py +1 -3
  62. svc_infra/api/fastapi/middleware/timeout.py +9 -10
  63. svc_infra/api/fastapi/object_router.py +1060 -0
  64. svc_infra/api/fastapi/openapi/apply.py +5 -6
  65. svc_infra/api/fastapi/openapi/conventions.py +4 -4
  66. svc_infra/api/fastapi/openapi/mutators.py +13 -31
  67. svc_infra/api/fastapi/openapi/pipeline.py +2 -2
  68. svc_infra/api/fastapi/openapi/responses.py +4 -6
  69. svc_infra/api/fastapi/openapi/security.py +1 -3
  70. svc_infra/api/fastapi/ops/add.py +7 -9
  71. svc_infra/api/fastapi/pagination.py +25 -37
  72. svc_infra/api/fastapi/routers/__init__.py +16 -38
  73. svc_infra/api/fastapi/setup.py +13 -31
  74. svc_infra/api/fastapi/tenancy/add.py +3 -2
  75. svc_infra/api/fastapi/tenancy/context.py +8 -7
  76. svc_infra/api/fastapi/versioned.py +3 -2
  77. svc_infra/app/env.py +5 -7
  78. svc_infra/app/logging/add.py +2 -1
  79. svc_infra/app/logging/filter.py +1 -1
  80. svc_infra/app/logging/formats.py +3 -2
  81. svc_infra/app/root.py +3 -3
  82. svc_infra/billing/__init__.py +19 -2
  83. svc_infra/billing/async_service.py +27 -7
  84. svc_infra/billing/jobs.py +23 -33
  85. svc_infra/billing/models.py +21 -52
  86. svc_infra/billing/quotas.py +5 -7
  87. svc_infra/billing/schemas.py +4 -6
  88. svc_infra/cache/__init__.py +12 -5
  89. svc_infra/cache/add.py +6 -9
  90. svc_infra/cache/backend.py +6 -5
  91. svc_infra/cache/decorators.py +17 -28
  92. svc_infra/cache/keys.py +2 -2
  93. svc_infra/cache/recache.py +22 -35
  94. svc_infra/cache/resources.py +8 -16
  95. svc_infra/cache/ttl.py +2 -3
  96. svc_infra/cache/utils.py +5 -6
  97. svc_infra/cli/__init__.py +4 -12
  98. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +11 -10
  99. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +6 -9
  100. svc_infra/cli/cmds/db/ops_cmds.py +3 -6
  101. svc_infra/cli/cmds/db/sql/alembic_cmds.py +24 -41
  102. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +9 -17
  103. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +10 -10
  104. svc_infra/cli/cmds/docs/docs_cmds.py +7 -10
  105. svc_infra/cli/cmds/dx/dx_cmds.py +5 -11
  106. svc_infra/cli/cmds/jobs/jobs_cmds.py +2 -7
  107. svc_infra/cli/cmds/obs/obs_cmds.py +4 -7
  108. svc_infra/cli/cmds/sdk/sdk_cmds.py +5 -15
  109. svc_infra/cli/foundation/runner.py +6 -11
  110. svc_infra/cli/foundation/typer_bootstrap.py +1 -2
  111. svc_infra/data/__init__.py +83 -0
  112. svc_infra/data/add.py +5 -5
  113. svc_infra/data/backup.py +8 -10
  114. svc_infra/data/erasure.py +3 -2
  115. svc_infra/data/fixtures.py +3 -3
  116. svc_infra/data/retention.py +8 -13
  117. svc_infra/db/crud_schema.py +9 -8
  118. svc_infra/db/nosql/__init__.py +0 -1
  119. svc_infra/db/nosql/constants.py +1 -1
  120. svc_infra/db/nosql/core.py +7 -14
  121. svc_infra/db/nosql/indexes.py +11 -10
  122. svc_infra/db/nosql/management.py +3 -3
  123. svc_infra/db/nosql/mongo/client.py +3 -3
  124. svc_infra/db/nosql/mongo/settings.py +2 -6
  125. svc_infra/db/nosql/repository.py +27 -28
  126. svc_infra/db/nosql/resource.py +15 -20
  127. svc_infra/db/nosql/scaffold.py +13 -17
  128. svc_infra/db/nosql/service.py +3 -4
  129. svc_infra/db/nosql/service_with_hooks.py +4 -3
  130. svc_infra/db/nosql/types.py +2 -6
  131. svc_infra/db/nosql/utils.py +4 -4
  132. svc_infra/db/ops.py +14 -18
  133. svc_infra/db/outbox.py +15 -18
  134. svc_infra/db/sql/apikey.py +12 -21
  135. svc_infra/db/sql/authref.py +3 -7
  136. svc_infra/db/sql/constants.py +9 -9
  137. svc_infra/db/sql/core.py +11 -11
  138. svc_infra/db/sql/management.py +2 -6
  139. svc_infra/db/sql/repository.py +17 -24
  140. svc_infra/db/sql/resource.py +14 -13
  141. svc_infra/db/sql/scaffold.py +13 -17
  142. svc_infra/db/sql/service.py +7 -16
  143. svc_infra/db/sql/service_with_hooks.py +4 -3
  144. svc_infra/db/sql/tenant.py +6 -14
  145. svc_infra/db/sql/uniq.py +8 -7
  146. svc_infra/db/sql/uniq_hooks.py +14 -19
  147. svc_infra/db/sql/utils.py +24 -53
  148. svc_infra/db/utils.py +3 -3
  149. svc_infra/deploy/__init__.py +8 -15
  150. svc_infra/documents/add.py +7 -8
  151. svc_infra/documents/ease.py +8 -8
  152. svc_infra/documents/models.py +3 -3
  153. svc_infra/documents/storage.py +11 -13
  154. svc_infra/dx/__init__.py +58 -0
  155. svc_infra/dx/add.py +1 -3
  156. svc_infra/dx/changelog.py +2 -2
  157. svc_infra/dx/checks.py +1 -1
  158. svc_infra/health/__init__.py +15 -16
  159. svc_infra/http/client.py +10 -14
  160. svc_infra/jobs/__init__.py +79 -0
  161. svc_infra/jobs/builtins/outbox_processor.py +3 -5
  162. svc_infra/jobs/builtins/webhook_delivery.py +1 -3
  163. svc_infra/jobs/loader.py +4 -5
  164. svc_infra/jobs/queue.py +14 -24
  165. svc_infra/jobs/redis_queue.py +20 -34
  166. svc_infra/jobs/runner.py +7 -11
  167. svc_infra/jobs/scheduler.py +5 -5
  168. svc_infra/jobs/worker.py +1 -1
  169. svc_infra/loaders/base.py +5 -4
  170. svc_infra/loaders/github.py +1 -3
  171. svc_infra/loaders/url.py +3 -9
  172. svc_infra/logging/__init__.py +7 -6
  173. svc_infra/mcp/__init__.py +82 -0
  174. svc_infra/mcp/svc_infra_mcp.py +2 -2
  175. svc_infra/obs/add.py +4 -3
  176. svc_infra/obs/cloud_dash.py +1 -1
  177. svc_infra/obs/metrics/__init__.py +3 -3
  178. svc_infra/obs/metrics/asgi.py +9 -14
  179. svc_infra/obs/metrics/base.py +13 -13
  180. svc_infra/obs/metrics/http.py +5 -9
  181. svc_infra/obs/metrics/sqlalchemy.py +9 -12
  182. svc_infra/obs/metrics.py +3 -3
  183. svc_infra/obs/settings.py +2 -6
  184. svc_infra/resilience/__init__.py +44 -0
  185. svc_infra/resilience/circuit_breaker.py +328 -0
  186. svc_infra/resilience/retry.py +289 -0
  187. svc_infra/security/__init__.py +167 -0
  188. svc_infra/security/add.py +5 -9
  189. svc_infra/security/audit.py +14 -17
  190. svc_infra/security/audit_service.py +9 -9
  191. svc_infra/security/hibp.py +3 -6
  192. svc_infra/security/jwt_rotation.py +7 -10
  193. svc_infra/security/lockout.py +12 -11
  194. svc_infra/security/models.py +37 -46
  195. svc_infra/security/oauth_models.py +8 -8
  196. svc_infra/security/org_invites.py +11 -13
  197. svc_infra/security/passwords.py +4 -6
  198. svc_infra/security/permissions.py +8 -7
  199. svc_infra/security/session.py +6 -7
  200. svc_infra/security/signed_cookies.py +9 -9
  201. svc_infra/storage/add.py +5 -8
  202. svc_infra/storage/backends/local.py +13 -21
  203. svc_infra/storage/backends/memory.py +4 -7
  204. svc_infra/storage/backends/s3.py +17 -36
  205. svc_infra/storage/base.py +2 -2
  206. svc_infra/storage/easy.py +4 -8
  207. svc_infra/storage/settings.py +16 -18
  208. svc_infra/testing/__init__.py +36 -39
  209. svc_infra/utils.py +169 -8
  210. svc_infra/webhooks/__init__.py +1 -1
  211. svc_infra/webhooks/add.py +17 -29
  212. svc_infra/webhooks/encryption.py +2 -2
  213. svc_infra/webhooks/fastapi.py +2 -4
  214. svc_infra/webhooks/router.py +3 -3
  215. svc_infra/webhooks/service.py +5 -6
  216. svc_infra/webhooks/signing.py +5 -5
  217. svc_infra/websocket/add.py +2 -3
  218. svc_infra/websocket/client.py +3 -2
  219. svc_infra/websocket/config.py +6 -18
  220. svc_infra/websocket/manager.py +9 -10
  221. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/METADATA +11 -5
  222. svc_infra-1.1.0.dist-info/RECORD +364 -0
  223. svc_infra/billing/service.py +0 -123
  224. svc_infra-0.1.706.dist-info/RECORD +0 -357
  225. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/LICENSE +0 -0
  226. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
  227. {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Optional
5
4
 
6
5
  from sqlalchemy import JSON, DateTime, Index, Numeric, String, UniqueConstraint, text
7
6
  from sqlalchemy.orm import Mapped, mapped_column
@@ -15,9 +14,7 @@ class UsageEvent(ModelBase):
15
14
  __tablename__ = "billing_usage_events"
16
15
 
17
16
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
18
- tenant_id: Mapped[str] = mapped_column(
19
- String(TENANT_ID_LEN), index=True, nullable=False
20
- )
17
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
21
18
  metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
22
19
  amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
23
20
  at_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
@@ -30,9 +27,7 @@ class UsageEvent(ModelBase):
30
27
  )
31
28
 
32
29
  __table_args__ = (
33
- UniqueConstraint(
34
- "tenant_id", "metric", "idempotency_key", name="uq_usage_idem"
35
- ),
30
+ UniqueConstraint("tenant_id", "metric", "idempotency_key", name="uq_usage_idem"),
36
31
  Index("ix_usage_tenant_metric_ts", "tenant_id", "metric", "at_ts"),
37
32
  )
38
33
 
@@ -41,16 +36,10 @@ class UsageAggregate(ModelBase):
41
36
  __tablename__ = "billing_usage_aggregates"
42
37
 
43
38
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
44
- tenant_id: Mapped[str] = mapped_column(
45
- String(TENANT_ID_LEN), index=True, nullable=False
46
- )
39
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
47
40
  metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
48
- period_start: Mapped[datetime] = mapped_column(
49
- DateTime(timezone=True), nullable=False
50
- )
51
- granularity: Mapped[str] = mapped_column(
52
- String(8), nullable=False
53
- ) # hour|day|month
41
+ period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
42
+ granularity: Mapped[str] = mapped_column(String(8), nullable=False) # hour|day|month
54
43
  total: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
55
44
  updated_at: Mapped[datetime] = mapped_column(
56
45
  DateTime(timezone=True),
@@ -59,9 +48,7 @@ class UsageAggregate(ModelBase):
59
48
  )
60
49
 
61
50
  __table_args__ = (
62
- UniqueConstraint(
63
- "tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"
64
- ),
51
+ UniqueConstraint("tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"),
65
52
  )
66
53
 
67
54
 
@@ -69,11 +56,9 @@ class Plan(ModelBase):
69
56
  __tablename__ = "billing_plans"
70
57
 
71
58
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
72
- key: Mapped[str] = mapped_column(
73
- String(64), unique=True, index=True, nullable=False
74
- )
59
+ key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
75
60
  name: Mapped[str] = mapped_column(String(128), nullable=False)
76
- description: Mapped[Optional[str]] = mapped_column(String(255))
61
+ description: Mapped[str | None] = mapped_column(String(255))
77
62
  created_at: Mapped[datetime] = mapped_column(
78
63
  DateTime(timezone=True),
79
64
  server_default=text("CURRENT_TIMESTAMP"),
@@ -100,14 +85,10 @@ class Subscription(ModelBase):
100
85
  __tablename__ = "billing_subscriptions"
101
86
 
102
87
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
103
- tenant_id: Mapped[str] = mapped_column(
104
- String(TENANT_ID_LEN), index=True, nullable=False
105
- )
88
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
106
89
  plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
107
- effective_at: Mapped[datetime] = mapped_column(
108
- DateTime(timezone=True), nullable=False
109
- )
110
- ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
90
+ effective_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
91
+ ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
111
92
  created_at: Mapped[datetime] = mapped_column(
112
93
  DateTime(timezone=True),
113
94
  server_default=text("CURRENT_TIMESTAMP"),
@@ -119,17 +100,11 @@ class Price(ModelBase):
119
100
  __tablename__ = "billing_prices"
120
101
 
121
102
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
122
- key: Mapped[str] = mapped_column(
123
- String(64), unique=True, index=True, nullable=False
124
- )
103
+ key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
125
104
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
126
- unit_amount: Mapped[int] = mapped_column(
127
- Numeric(18, 0), nullable=False
128
- ) # minor units
129
- metric: Mapped[Optional[str]] = mapped_column(
130
- String(64)
131
- ) # null for fixed recurring
132
- recurring_interval: Mapped[Optional[str]] = mapped_column(String(8)) # month|year
105
+ unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
106
+ metric: Mapped[str | None] = mapped_column(String(64)) # null for fixed recurring
107
+ recurring_interval: Mapped[str | None] = mapped_column(String(8)) # month|year
133
108
  created_at: Mapped[datetime] = mapped_column(
134
109
  DateTime(timezone=True),
135
110
  server_default=text("CURRENT_TIMESTAMP"),
@@ -141,19 +116,13 @@ class Invoice(ModelBase):
141
116
  __tablename__ = "billing_invoices"
142
117
 
143
118
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
144
- tenant_id: Mapped[str] = mapped_column(
145
- String(TENANT_ID_LEN), index=True, nullable=False
146
- )
147
- period_start: Mapped[datetime] = mapped_column(
148
- DateTime(timezone=True), nullable=False
149
- )
150
- period_end: Mapped[datetime] = mapped_column(
151
- DateTime(timezone=True), nullable=False
152
- )
119
+ tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
120
+ period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
121
+ period_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
153
122
  status: Mapped[str] = mapped_column(String(16), index=True, nullable=False)
154
123
  total_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
155
124
  currency: Mapped[str] = mapped_column(String(8), nullable=False)
156
- provider_invoice_id: Mapped[Optional[str]] = mapped_column(String(128), index=True)
125
+ provider_invoice_id: Mapped[str | None] = mapped_column(String(128), index=True)
157
126
  created_at: Mapped[datetime] = mapped_column(
158
127
  DateTime(timezone=True),
159
128
  server_default=text("CURRENT_TIMESTAMP"),
@@ -166,8 +135,8 @@ class InvoiceLine(ModelBase):
166
135
 
167
136
  id: Mapped[str] = mapped_column(String(64), primary_key=True)
168
137
  invoice_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
169
- price_id: Mapped[Optional[str]] = mapped_column(String(64), index=True)
170
- metric: Mapped[Optional[str]] = mapped_column(String(64))
138
+ price_id: Mapped[str | None] = mapped_column(String(64), index=True)
139
+ metric: Mapped[str | None] = mapped_column(String(64))
171
140
  quantity: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
172
141
  amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
173
142
  created_at: Mapped[datetime] = mapped_column(
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime, timezone
4
- from typing import Annotated, Optional
3
+ from datetime import UTC, datetime
4
+ from typing import Annotated
5
5
 
6
6
  from fastapi import Depends, HTTPException, status
7
7
  from sqlalchemy import select
@@ -13,10 +13,8 @@ from svc_infra.api.fastapi.tenancy.context import TenantId
13
13
  from .models import PlanEntitlement, Subscription, UsageAggregate
14
14
 
15
15
 
16
- async def _current_subscription(
17
- session: AsyncSession, tenant_id: str
18
- ) -> Optional[Subscription]:
19
- now = datetime.now(tz=timezone.utc)
16
+ async def _current_subscription(session: AsyncSession, tenant_id: str) -> Subscription | None:
17
+ now = datetime.now(tz=UTC)
20
18
  row = (
21
19
  (
22
20
  await session.execute(
@@ -59,7 +57,7 @@ def require_quota(metric: str, *, window: str = "day", soft: bool = True):
59
57
  # no entitlement → unlimited
60
58
  return
61
59
  # compute current window start
62
- now = datetime.now(tz=timezone.utc)
60
+ now = datetime.now(tz=UTC)
63
61
  if window == "day":
64
62
  period_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
65
63
  granularity = "day"
@@ -1,17 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Annotated, Optional
4
+ from typing import Annotated
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
 
9
9
  class UsageIn(BaseModel):
10
10
  metric: str = Field(..., min_length=1, max_length=64)
11
- amount: Annotated[
12
- int, Field(ge=0, description="Non-negative amount for the metric")
13
- ]
14
- at: Optional[datetime] = Field(
11
+ amount: Annotated[int, Field(ge=0, description="Non-negative amount for the metric")]
12
+ at: datetime | None = Field(
15
13
  default=None,
16
14
  description="Event timestamp (UTC). Defaults to server time if omitted.",
17
15
  )
@@ -33,4 +31,4 @@ class UsageAggregateRow(BaseModel):
33
31
 
34
32
  class UsageAggregatesOut(BaseModel):
35
33
  items: list[UsageAggregateRow] = Field(default_factory=list)
36
- next_cursor: Optional[str] = None
34
+ next_cursor: str | None = None
@@ -11,16 +11,23 @@ from .add import add_cache
11
11
  from .backend import get_cache
12
12
 
13
13
  # Core decorators - main public API
14
- from .decorators import cached # alias for cache_read
15
- from .decorators import mutates # alias for cache_write
16
- from .decorators import cache_read, cache_write, init_cache, init_cache_async
14
+ from .decorators import (
15
+ cache_read,
16
+ cache_write,
17
+ cached, # alias for cache_read
18
+ init_cache,
19
+ init_cache_async,
20
+ mutates, # alias for cache_write
21
+ )
17
22
 
18
23
  # Recaching functionality for advanced use cases
19
24
  from .recache import RecachePlan, recache
20
25
 
21
26
  # Resource management for entity-based caching
22
- from .resources import entity # legacy alias
23
- from .resources import resource
27
+ from .resources import (
28
+ entity, # legacy alias
29
+ resource,
30
+ )
24
31
 
25
32
  __all__ = [
26
33
  # Primary decorators developers use
svc_infra/cache/add.py CHANGED
@@ -14,7 +14,8 @@ from __future__ import annotations
14
14
 
15
15
  import logging
16
16
  import os
17
- from typing import Any, Callable, Optional
17
+ from collections.abc import Callable
18
+ from typing import Any
18
19
 
19
20
  from svc_infra.cache.backend import DEFAULT_READINESS_TIMEOUT
20
21
  from svc_infra.cache.backend import get_cache as _get_cache
@@ -35,7 +36,7 @@ def _instance() -> Any:
35
36
 
36
37
 
37
38
  def _derive_settings(
38
- url: Optional[str], prefix: Optional[str], version: Optional[str]
39
+ url: str | None, prefix: str | None, version: str | None
39
40
  ) -> tuple[str, str, str]:
40
41
  """Derive cache settings from parameters or environment variables.
41
42
 
@@ -111,9 +112,7 @@ def add_cache(
111
112
  try:
112
113
  setattr(app.state, state_key, _instance())
113
114
  except Exception:
114
- logger.debug(
115
- "Unable to expose cache instance on app.state", exc_info=True
116
- )
115
+ logger.debug("Unable to expose cache instance on app.state", exc_info=True)
117
116
 
118
117
  async def _shutdown():
119
118
  try:
@@ -146,16 +145,14 @@ def add_cache(
146
145
  # Mark wired and expose state immediately if desired
147
146
  if hasattr(app, "state"):
148
147
  try:
149
- setattr(app.state, "_svc_cache_wired", True)
148
+ app.state._svc_cache_wired = True
150
149
  if expose_state and not hasattr(app.state, state_key):
151
150
  setattr(app.state, state_key, _instance())
152
151
  except Exception:
153
152
  pass
154
153
 
155
154
  if register_ok:
156
- logger.info(
157
- "Cache wired: url=%s namespace=%s", eff_url, f"{eff_prefix}:{eff_version}"
158
- )
155
+ logger.info("Cache wired: url=%s namespace=%s", eff_url, f"{eff_prefix}:{eff_version}")
159
156
  else:
160
157
  # If we cannot register handlers, at least initialize now
161
158
  try:
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Optional
5
4
 
6
5
  from cashews import cache as _cache
7
6
 
@@ -42,10 +41,10 @@ def _full_prefix() -> str:
42
41
 
43
42
 
44
43
  def setup_cache(
45
- url: Optional[str] = None,
44
+ url: str | None = None,
46
45
  *,
47
- prefix: Optional[str] = None,
48
- version: Optional[str] = None,
46
+ prefix: str | None = None,
47
+ version: str | None = None,
49
48
  ):
50
49
  """
51
50
  Configure Cashews and set a global key prefix for namespacing.
@@ -118,7 +117,9 @@ async def wait_ready(timeout: float = DEFAULT_READINESS_TIMEOUT) -> None:
118
117
  retrieved_value = await _cache.get(probe_key)
119
118
 
120
119
  if retrieved_value != PROBE_VALUE:
121
- error_msg = f"Cache readiness probe failed. Expected '{PROBE_VALUE}', got '{retrieved_value}'"
120
+ error_msg = (
121
+ f"Cache readiness probe failed. Expected '{PROBE_VALUE}', got '{retrieved_value}'"
122
+ )
122
123
  logger.error(error_msg)
123
124
  raise RuntimeError(error_msg)
124
125
 
@@ -9,7 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  import inspect
11
11
  import logging
12
- from typing import Any, Awaitable, Callable, Iterable, Optional, Union
12
+ from collections.abc import Awaitable, Callable, Iterable
13
+ from typing import Any
13
14
 
14
15
  from cashews import cache as _cache
15
16
 
@@ -64,11 +65,11 @@ async def init_cache_async(
64
65
 
65
66
  def cache_read(
66
67
  *,
67
- key: Union[str, tuple[str, ...]],
68
- ttl: Optional[int] = None,
69
- tags: Optional[Union[Iterable[str], Callable[..., Iterable[str]]]] = None,
70
- early_ttl: Optional[int] = None,
71
- refresh: Optional[bool] = None,
68
+ key: str | tuple[str, ...],
69
+ ttl: int | None = None,
70
+ tags: Iterable[str] | Callable[..., Iterable[str]] | None = None,
71
+ early_ttl: int | None = None,
72
+ refresh: bool | None = None,
72
73
  ):
73
74
  """
74
75
  Cache decorator for read operations with version-resilient key handling.
@@ -126,9 +127,7 @@ def cache_read(
126
127
  # Attempt 1: With prefix parameter (preferred)
127
128
  if namespace:
128
129
  try:
129
- wrapped = _cache.cache(
130
- ttl_val, template, prefix=namespace, **cache_kwargs
131
- )(func)
130
+ wrapped = _cache.cache(ttl_val, template, prefix=namespace, **cache_kwargs)(func)
132
131
  except TypeError as e:
133
132
  error_msgs.append(f"prefix parameter: {e}")
134
133
 
@@ -140,28 +139,22 @@ def cache_read(
140
139
  if namespace and not template.startswith(f"{namespace}:")
141
140
  else template
142
141
  )
143
- wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(
144
- func
145
- )
142
+ wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(func)
146
143
  except TypeError as e:
147
144
  error_msgs.append(f"embedded namespace: {e}")
148
145
 
149
146
  # Attempt 3: Minimal fallback
150
147
  if wrapped is None:
151
148
  try:
152
- key_with_namespace = (
153
- f"{namespace}:{template}" if namespace else template
154
- )
149
+ key_with_namespace = f"{namespace}:{template}" if namespace else template
155
150
  wrapped = _cache.cache(ttl_val, key_with_namespace)(func)
156
151
  except Exception as e:
157
152
  error_msgs.append(f"minimal fallback: {e}")
158
153
  logger.error(f"All cache decorator attempts failed: {error_msgs}")
159
- raise RuntimeError(
160
- f"Failed to apply cache decorator: {error_msgs[-1]}"
161
- ) from e
154
+ raise RuntimeError(f"Failed to apply cache decorator: {error_msgs[-1]}") from e
162
155
 
163
156
  # Attach key variants renderer for cache writers
164
- setattr(wrapped, "__svc_key_variants__", build_key_variants_renderer(template))
157
+ wrapped.__svc_key_variants__ = build_key_variants_renderer(template) # type: ignore[attr-defined]
165
158
 
166
159
  # If tags were provided as a callable, populate cashews tag sets manually.
167
160
  # This is best-effort and only affects invalidation-by-tag behavior.
@@ -191,19 +184,15 @@ def cache_read(
191
184
  except Exception:
192
185
  pass
193
186
  if tag_val:
194
- await _cache.set_add(
195
- tag_key_prefix + tag_val, full_key, expire=ttl_val
196
- )
187
+ await _cache.set_add(tag_key_prefix + tag_val, full_key, expire=ttl_val)
197
188
  except Exception:
198
189
  # Don't let best-effort tag mapping break cache reads.
199
190
  pass
200
191
 
201
192
  return result
202
193
 
203
- setattr(
204
- _wrapped_with_dynamic_tags,
205
- "__svc_key_variants__",
206
- getattr(wrapped, "__svc_key_variants__", None),
194
+ _wrapped_with_dynamic_tags.__svc_key_variants__ = getattr( # type: ignore[attr-defined]
195
+ wrapped, "__svc_key_variants__", None
207
196
  )
208
197
  return _wrapped_with_dynamic_tags
209
198
 
@@ -219,8 +208,8 @@ cached = cache_read
219
208
 
220
209
  def cache_write(
221
210
  *,
222
- tags: Union[Iterable[str], Callable[..., Iterable[str]]],
223
- recache: Optional[Iterable[RecacheSpec]] = None,
211
+ tags: Iterable[str] | Callable[..., Iterable[str]],
212
+ recache: Iterable[RecacheSpec] | None = None,
224
213
  recache_max_concurrency: int = 5,
225
214
  ):
226
215
  """
svc_infra/cache/keys.py CHANGED
@@ -6,7 +6,7 @@ with version-resilient handling and namespace support.
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Callable, Union
9
+ from collections.abc import Callable
10
10
 
11
11
  from svc_infra.cache.backend import alias as _alias
12
12
 
@@ -15,7 +15,7 @@ from .utils import validate_cache_key
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- def build_key_template(key: Union[str, tuple[str, ...]]) -> str:
18
+ def build_key_template(key: str | tuple[str, ...]) -> str:
19
19
  """Convert key to template string."""
20
20
  if isinstance(key, tuple):
21
21
  parts = [part for part in key if part]
@@ -7,9 +7,10 @@ including recache plans and execution strategies.
7
7
 
8
8
  import asyncio
9
9
  import logging
10
+ from collections.abc import Awaitable, Callable, Iterable
10
11
  from dataclasses import dataclass
11
12
  from inspect import Parameter, signature
12
- from typing import Any, Awaitable, Callable, Iterable, Optional, Union
13
+ from typing import Any
13
14
 
14
15
  from cashews import cache as _cache
15
16
 
@@ -34,19 +35,19 @@ class RecachePlan:
34
35
  """
35
36
 
36
37
  getter: Callable[..., Awaitable[Any]]
37
- include: Optional[Iterable[str]] = None
38
- rename: Optional[dict[str, str]] = None
39
- extra: Optional[dict[str, Any]] = None
40
- key: Optional[Union[str, tuple[str, ...]]] = None
38
+ include: Iterable[str] | None = None
39
+ rename: dict[str, str] | None = None
40
+ extra: dict[str, Any] | None = None
41
+ key: str | tuple[str, ...] | None = None
41
42
 
42
43
 
43
44
  def recache(
44
45
  getter: Callable[..., Awaitable[Any]],
45
46
  *,
46
- include: Optional[Iterable[str]] = None,
47
- rename: Optional[dict[str, str]] = None,
48
- extra: Optional[dict[str, Any]] = None,
49
- key: Optional[Union[str, tuple[str, ...]]] = None,
47
+ include: Iterable[str] | None = None,
48
+ rename: dict[str, str] | None = None,
49
+ extra: dict[str, Any] | None = None,
50
+ key: str | tuple[str, ...] | None = None,
50
51
  ) -> RecachePlan:
51
52
  """
52
53
  Create a recache plan for cache warming after invalidation.
@@ -61,21 +62,17 @@ def recache(
61
62
  Returns:
62
63
  RecachePlan instance
63
64
  """
64
- return RecachePlan(
65
- getter=getter, include=include, rename=rename, extra=extra, key=key
66
- )
65
+ return RecachePlan(getter=getter, include=include, rename=rename, extra=extra, key=key)
67
66
 
68
67
 
69
- RecacheSpec = Union[
70
- Callable[..., Awaitable[Any]],
71
- RecachePlan,
72
- tuple[Callable[..., Awaitable[Any]], Any], # Legacy format
73
- ]
68
+ RecacheSpec = (
69
+ Callable[..., Awaitable[Any]]
70
+ | RecachePlan
71
+ | tuple[Callable[..., Awaitable[Any]], Any] # Legacy format
72
+ )
74
73
 
75
74
 
76
- def generate_key_variants(
77
- template: Union[str, tuple[str, ...]], params: dict[str, Any]
78
- ) -> list[str]:
75
+ def generate_key_variants(template: str | tuple[str, ...], params: dict[str, Any]) -> list[str]:
79
76
  """
80
77
  Generate all possible cache key variants for deletion.
81
78
 
@@ -184,24 +181,18 @@ def build_getter_kwargs(
184
181
  continue
185
182
  try:
186
183
  if callable(source):
187
- legacy_call_kwargs[getter_param] = source(
188
- *mut_args, **mut_kwargs
189
- )
184
+ legacy_call_kwargs[getter_param] = source(*mut_args, **mut_kwargs)
190
185
  elif isinstance(source, str) and source in mut_kwargs:
191
186
  legacy_call_kwargs[getter_param] = mut_kwargs[source]
192
187
  except Exception as e:
193
- logger.warning(
194
- f"Recache parameter mapping failed for {getter_param}: {e}"
195
- )
188
+ logger.warning(f"Recache parameter mapping failed for {getter_param}: {e}")
196
189
 
197
190
  # Add direct parameter matches
198
191
  for param_name in getter_params.keys():
199
192
  if param_name not in legacy_call_kwargs and param_name in mut_kwargs:
200
193
  legacy_call_kwargs[param_name] = mut_kwargs[param_name]
201
194
 
202
- legacy_call_kwargs = {
203
- k: v for k, v in legacy_call_kwargs.items() if k in getter_params
204
- }
195
+ legacy_call_kwargs = {k: v for k, v in legacy_call_kwargs.items() if k in getter_params}
205
196
  return getter, legacy_call_kwargs
206
197
 
207
198
  # Handle simple getter function
@@ -232,9 +223,7 @@ async def execute_recache(
232
223
  try:
233
224
  await _cache.delete(key_variant)
234
225
  except Exception as e:
235
- logger.debug(
236
- f"Failed to delete cache key {key_variant}: {e}"
237
- )
226
+ logger.debug(f"Failed to delete cache key {key_variant}: {e}")
238
227
 
239
228
  # Execute the getter to warm the cache
240
229
  await getter(**call_kwargs)
@@ -243,6 +232,4 @@ async def execute_recache(
243
232
  logger.error(f"Recache operation failed: {e}")
244
233
 
245
234
  # Execute all recache operations concurrently
246
- await asyncio.gather(
247
- *[_run_single_recache(spec) for spec in specs], return_exceptions=True
248
- )
235
+ await asyncio.gather(*[_run_single_recache(spec) for spec in specs], return_exceptions=True)
@@ -8,7 +8,7 @@ with standardized key patterns and tag management.
8
8
  import asyncio
9
9
  import inspect
10
10
  import logging
11
- from typing import Callable, Optional, Tuple
11
+ from collections.abc import Callable
12
12
 
13
13
  from cashews import cache as _cache
14
14
 
@@ -41,8 +41,8 @@ class Resource:
41
41
  *,
42
42
  suffix: str,
43
43
  ttl: int,
44
- key_template: Optional[str] = None,
45
- tags_template: Optional[Tuple[str, ...]] = None,
44
+ key_template: str | None = None,
45
+ tags_template: tuple[str, ...] | None = None,
46
46
  lock: bool = True,
47
47
  ):
48
48
  """
@@ -63,9 +63,7 @@ class Resource:
63
63
 
64
64
  def _decorator(func: Callable):
65
65
  try:
66
- return _cache(ttl=ttl, key=key_template, tags=tags_template, lock=lock)(
67
- func
68
- )
66
+ return _cache(ttl=ttl, key=key_template, tags=tags_template, lock=lock)(func)
69
67
  except TypeError:
70
68
  # Fallback for older cashews versions
71
69
  return _cache(ttl=ttl, key=key_template, tags=tags_template)(func)
@@ -75,7 +73,7 @@ class Resource:
75
73
  def cache_write(
76
74
  self,
77
75
  *,
78
- recache: Optional[list[tuple[Callable, Callable]]] = None,
76
+ recache: list[tuple[Callable, Callable]] | None = None,
79
77
  recache_max_concurrency: int = 5,
80
78
  ):
81
79
  """
@@ -99,9 +97,7 @@ class Resource:
99
97
  """Delete all cache keys for a specific entity."""
100
98
  namespace = _alias() or ""
101
99
  namespace_prefix = (
102
- f"{namespace}:"
103
- if namespace and not namespace.endswith(":")
104
- else namespace
100
+ f"{namespace}:" if namespace and not namespace.endswith(":") else namespace
105
101
  )
106
102
 
107
103
  # Generate candidate keys to delete
@@ -137,9 +133,7 @@ class Resource:
137
133
  # Namespaced wildcard
138
134
  if namespace_prefix:
139
135
  await _maybe_await(
140
- delete_match(
141
- f"{namespace_prefix}{entity_name}:*:{entity_id}*"
142
- )
136
+ delete_match(f"{namespace_prefix}{entity_name}:*:{entity_id}*")
143
137
  )
144
138
  # Non-namespaced wildcard
145
139
  await _maybe_await(delete_match(f"{entity_name}:*:{entity_id}*"))
@@ -178,9 +172,7 @@ class Resource:
178
172
  # Tag invalidation
179
173
  invalidate_func = getattr(_cache, "invalidate", None)
180
174
  if callable(invalidate_func):
181
- await _maybe_await(
182
- invalidate_func(f"{self.name}:{entity_id}")
183
- )
175
+ await _maybe_await(invalidate_func(f"{self.name}:{entity_id}"))
184
176
 
185
177
  # Precise key deletion
186
178
  await _delete_entity_keys(self.name, str(entity_id))
svc_infra/cache/ttl.py CHANGED
@@ -6,7 +6,6 @@ via environment variables with sensible defaults.
6
6
  """
7
7
 
8
8
  import os
9
- from typing import Optional
10
9
 
11
10
 
12
11
  def _get_env_int(key: str, default: int) -> int:
@@ -36,7 +35,7 @@ TTL_SHORT: int = _get_env_int("CACHE_TTL_SHORT", 30) # 30 seconds
36
35
  TTL_LONG: int = _get_env_int("CACHE_TTL_LONG", 3600) # 1 hour
37
36
 
38
37
 
39
- def get_ttl(duration_type: str) -> Optional[int]:
38
+ def get_ttl(duration_type: str) -> int | None:
40
39
  """
41
40
  Get TTL value by duration type name.
42
41
 
@@ -60,7 +59,7 @@ def get_ttl(duration_type: str) -> Optional[int]:
60
59
  return ttl_map.get(duration_type.lower())
61
60
 
62
61
 
63
- def validate_ttl(ttl: Optional[int]) -> int:
62
+ def validate_ttl(ttl: int | None) -> int:
64
63
  """
65
64
  Validate and normalize a TTL value.
66
65