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,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
4
-
5
3
  from starlette.requests import Request
6
4
 
7
5
  from svc_infra.app.env import IS_PROD
@@ -16,9 +14,7 @@ def _is_local_host(host: str) -> bool:
16
14
 
17
15
  def _is_https(request: Request) -> bool:
18
16
  proto = (
19
- (request.headers.get("x-forwarded-proto") or request.url.scheme or "")
20
- .split(",")[0]
21
- .strip()
17
+ (request.headers.get("x-forwarded-proto") or request.url.scheme or "").split(",")[0].strip()
22
18
  )
23
19
  return proto.lower() == "https"
24
20
 
@@ -27,15 +23,13 @@ def compute_cookie_params(request: Request, *, name: str) -> dict:
27
23
  st = get_auth_settings()
28
24
  cfg_domain = (getattr(st, "session_cookie_domain", "") or "").strip()
29
25
 
30
- domain: Optional[str] = None
26
+ domain: str | None = None
31
27
  if cfg_domain and not _is_local_host(cfg_domain):
32
28
  domain = cfg_domain
33
29
 
34
30
  explicit_secure = getattr(st, "session_cookie_secure", None)
35
31
  secure = (
36
- bool(explicit_secure)
37
- if explicit_secure is not None
38
- else (_is_https(request) or IS_PROD)
32
+ bool(explicit_secure) if explicit_secure is not None else (_is_https(request) or IS_PROD)
39
33
  )
40
34
 
41
35
  samesite = str(getattr(st, "session_cookie_samesite", "lax")).lower()
@@ -135,9 +135,7 @@ def setup_oauth_authentication(
135
135
  if not providers:
136
136
  return
137
137
 
138
- redirect_url = (
139
- post_login_redirect or getattr(settings_obj, "post_login_redirect", None) or "/"
140
- )
138
+ redirect_url = post_login_redirect or getattr(settings_obj, "post_login_redirect", None) or "/"
141
139
  oauth_router_instance = oauth_router_with_backend(
142
140
  user_model=user_model,
143
141
  auth_backend=auth_backend,
@@ -253,7 +251,7 @@ def add_auth_users(
253
251
  (
254
252
  fapi,
255
253
  auth_backend,
256
- auth_router,
254
+ _auth_router,
257
255
  users_router,
258
256
  get_strategy,
259
257
  register_router,
@@ -268,9 +266,7 @@ def add_auth_users(
268
266
  )
269
267
 
270
268
  # Make the boot-time strategy and model available to resolvers
271
- set_auth_state(
272
- user_model=user_model, get_strategy=get_strategy, auth_prefix=auth_prefix
273
- )
269
+ set_auth_state(user_model=user_model, get_strategy=get_strategy, auth_prefix=auth_prefix)
274
270
 
275
271
  settings_obj = get_auth_settings()
276
272
  policy = auth_policy or DefaultAuthPolicy(settings_obj)
@@ -287,7 +283,7 @@ def add_auth_users(
287
283
  dev_default="dev-only-session-jwt-secret-not-for-production",
288
284
  )
289
285
  same_site_lit = cast(
290
- Literal["lax", "strict", "none"],
286
+ "Literal['lax', 'strict', 'none']",
291
287
  str(getattr(settings_obj, "session_cookie_samesite", "lax")).lower(),
292
288
  )
293
289
  app.add_middleware(
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
- from datetime import datetime, timezone
4
+ from datetime import UTC, datetime
5
5
  from typing import Any
6
6
 
7
7
  from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
@@ -42,9 +42,7 @@ async def login_client_gaurd(request: Request):
42
42
  client_id_raw = form.get("client_id")
43
43
  client_secret_raw = form.get("client_secret")
44
44
  client_id = client_id_raw.strip() if isinstance(client_id_raw, str) else ""
45
- client_secret = (
46
- client_secret_raw.strip() if isinstance(client_secret_raw, str) else ""
47
- )
45
+ client_secret = client_secret_raw.strip() if isinstance(client_secret_raw, str) else ""
48
46
  if not client_id or not client_secret:
49
47
  raise HTTPException(
50
48
  status_code=status.HTTP_401_UNAUTHORIZED,
@@ -54,10 +52,7 @@ async def login_client_gaurd(request: Request):
54
52
  # validate against configured clients
55
53
  ok = False
56
54
  for pc in getattr(st, "password_clients", []) or []:
57
- if (
58
- pc.client_id == client_id
59
- and pc.client_secret.get_secret_value() == client_secret
60
- ):
55
+ if pc.client_id == client_id and pc.client_secret.get_secret_value() == client_secret:
61
56
  ok = True
62
57
  break
63
58
 
@@ -102,11 +97,7 @@ def auth_session_router(
102
97
  try:
103
98
  status_lo = await get_lockout_status(session, user_id=None, ip_hash=ip_hash)
104
99
  if status_lo.locked and status_lo.next_allowed_at:
105
- retry = int(
106
- (
107
- status_lo.next_allowed_at - datetime.now(timezone.utc)
108
- ).total_seconds()
109
- )
100
+ retry = int((status_lo.next_allowed_at - datetime.now(UTC)).total_seconds())
110
101
  raise HTTPException(
111
102
  status_code=429,
112
103
  detail="account_locked",
@@ -120,9 +111,7 @@ def auth_session_router(
120
111
  if not user:
121
112
  _, _ = _pwd.verify_and_update(password, _DUMMY_BCRYPT)
122
113
  try:
123
- await record_attempt(
124
- session, user_id=None, ip_hash=ip_hash, success=False
125
- )
114
+ await record_attempt(session, user_id=None, ip_hash=ip_hash, success=False)
126
115
  except Exception:
127
116
  pass
128
117
  raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
@@ -131,9 +120,7 @@ def auth_session_router(
131
120
  if not getattr(user, "is_active", True):
132
121
  raise HTTPException(401, "account_disabled")
133
122
 
134
- hashed = getattr(user, "hashed_password", None) or getattr(
135
- user, "password_hash", None
136
- )
123
+ hashed = getattr(user, "hashed_password", None) or getattr(user, "password_hash", None)
137
124
  if not hashed:
138
125
  try:
139
126
  await record_attempt(
@@ -152,11 +139,7 @@ def auth_session_router(
152
139
  session, user_id=getattr(user, "id", None), ip_hash=ip_hash
153
140
  )
154
141
  if status_user.locked and status_user.next_allowed_at:
155
- retry = int(
156
- (
157
- status_user.next_allowed_at - datetime.now(timezone.utc)
158
- ).total_seconds()
159
- )
142
+ retry = int((status_user.next_allowed_at - datetime.now(UTC)).total_seconds())
160
143
  raise HTTPException(
161
144
  status_code=429,
162
145
  detail="account_locked",
@@ -189,7 +172,7 @@ def auth_session_router(
189
172
  except Exception:
190
173
  pass
191
174
 
192
- if getattr(user, "is_verified") is False:
175
+ if user.is_verified is False:
193
176
  raise HTTPException(400, "LOGIN_USER_NOT_VERIFIED")
194
177
 
195
178
  # 3) MFA policy check (user flag, tenant/global, etc.)
@@ -204,7 +187,7 @@ def auth_session_router(
204
187
 
205
188
  # 4) record last_login for password logins that do NOT require MFA
206
189
  try:
207
- user.last_login = datetime.now(timezone.utc)
190
+ user.last_login = datetime.now(UTC)
208
191
  await user_manager.user_db.update(user, {"last_login": user.last_login})
209
192
  except Exception:
210
193
  # don’t block login if this write fails
@@ -1,12 +1,9 @@
1
1
  from datetime import datetime
2
- from typing import Optional
3
2
 
4
3
  from pydantic import BaseModel
5
4
 
6
5
  # --- Email OTP store (replace with Redis in prod) ---
7
- EMAIL_OTP_STORE: dict[
8
- str, dict
9
- ] = {} # key = uid (or jti), value={hash,exp,attempts,next_send}
6
+ EMAIL_OTP_STORE: dict[str, dict] = {} # key = uid (or jti), value={hash,exp,attempts,next_send}
10
7
 
11
8
 
12
9
  class StartSetupOut(BaseModel):
@@ -56,9 +53,9 @@ class MFAProof(BaseModel):
56
53
 
57
54
 
58
55
  class DisableAccountIn(BaseModel):
59
- reason: Optional[str] = None
60
- mfa: Optional[MFAProof] = None
56
+ reason: str | None = None
57
+ mfa: MFAProof | None = None
61
58
 
62
59
 
63
60
  class DeleteAccountIn(BaseModel):
64
- mfa: Optional[MFAProof] = None
61
+ mfa: MFAProof | None = None
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timezone
1
+ from datetime import UTC, datetime
2
2
 
3
3
  from svc_infra.api.fastapi.auth.settings import get_auth_settings
4
4
  from svc_infra.app.env import require_secret
@@ -29,9 +29,9 @@ def get_mfa_pre_jwt_writer():
29
29
  async def write(self, user):
30
30
  from fastapi_users.jwt import generate_jwt
31
31
 
32
- now = datetime.now(timezone.utc)
32
+ now = datetime.now(UTC)
33
33
  payload = {
34
- "sub": str(getattr(user, "id")),
34
+ "sub": str(user.id),
35
35
  "aud": ["fastapi-users:mfa"],
36
36
  "iat": int(now.timestamp()),
37
37
  "exp": int(now.timestamp()) + self.lifetime,
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime, timezone
3
+ from datetime import UTC, datetime
4
4
  from typing import Any, cast
5
5
 
6
6
  import pyotp
@@ -80,7 +80,7 @@ def mfa_router(
80
80
  raise HTTPException(401, "Invalid token")
81
81
 
82
82
  # IMPORTANT: rehydrate into *your* session
83
- db_user = await cast(Any, session).get(user_model, user.id)
83
+ db_user = await cast("Any", session).get(user_model, user.id)
84
84
  if not db_user:
85
85
  raise HTTPException(401, "Invalid token")
86
86
 
@@ -114,9 +114,7 @@ def mfa_router(
114
114
  # )).scalar_one()
115
115
  # assert fresh_secret == secret
116
116
 
117
- return StartSetupOut(
118
- otpauth_url=uri, secret=secret, qr_svg=_qr_svg_from_uri(uri)
119
- )
117
+ return StartSetupOut(otpauth_url=uri, secret=secret, qr_svg=_qr_svg_from_uri(uri))
120
118
 
121
119
  @u.post(
122
120
  MFA_CONFIRM_PATH,
@@ -144,7 +142,7 @@ def mfa_router(
144
142
 
145
143
  user.mfa_recovery = [_hash(c) for c in codes]
146
144
  user.mfa_enabled = True
147
- user.mfa_confirmed_at = datetime.now(timezone.utc)
145
+ user.mfa_confirmed_at = datetime.now(UTC)
148
146
  await session.commit()
149
147
 
150
148
  return RecoveryCodesOut(codes=codes)
@@ -201,7 +199,7 @@ def mfa_router(
201
199
  raise HTTPException(401, "Invalid pre-auth token")
202
200
 
203
201
  # 2) load user
204
- user = await cast(Any, session).get(user_model, uid)
202
+ user = await cast("Any", session).get(user_model, uid)
205
203
  if not user:
206
204
  raise HTTPException(401, "Invalid pre-auth token")
207
205
 
@@ -209,9 +207,7 @@ def mfa_router(
209
207
  if not getattr(user, "is_active", True):
210
208
  raise HTTPException(401, "account_disabled")
211
209
 
212
- if (not getattr(user, "mfa_enabled", False)) or (
213
- not getattr(user, "mfa_secret", None)
214
- ):
210
+ if (not getattr(user, "mfa_enabled", False)) or (not getattr(user, "mfa_secret", None)):
215
211
  raise HTTPException(401, "MFA not enabled")
216
212
 
217
213
  # 3) verify TOTP or fallback
@@ -247,15 +243,13 @@ def mfa_router(
247
243
  raise HTTPException(400, "Invalid code")
248
244
 
249
245
  # NEW: set last_login on successful MFA
250
- user.last_login = datetime.now(timezone.utc)
246
+ user.last_login = datetime.now(UTC)
251
247
  await session.commit()
252
248
 
253
249
  # 4) mint normal JWT and set cookie
254
250
  token = await strategy.write_token(user)
255
251
  resp = JSONResponse({"access_token": token, "token_type": "bearer"})
256
- cp = compute_cookie_params(
257
- request, name=st.auth_cookie_name
258
- ) # <-- pass Request here
252
+ cp = compute_cookie_params(request, name=st.auth_cookie_name) # <-- pass Request here
259
253
  resp.set_cookie(**cp, value=token)
260
254
  return resp
261
255
 
@@ -278,7 +272,7 @@ def mfa_router(
278
272
  raise HTTPException(401, "Invalid pre-auth token")
279
273
 
280
274
  # 1b) Load user to get their email
281
- user = await cast(Any, session).get(user_model, uid)
275
+ user = await cast("Any", session).get(user_model, uid)
282
276
  if not user or not getattr(user, "email", None):
283
277
  # (optionally also check user.mfa_enabled here)
284
278
  raise HTTPException(401, "Invalid pre-auth token")
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from fastapi import Body, Depends, HTTPException, Query
4
2
 
5
3
  from svc_infra.api.fastapi.auth.security import Identity
@@ -12,9 +10,9 @@ def RequireMFAIfEnabled(body_field: str = "mfa"):
12
10
  async def _dep(
13
11
  p: Identity,
14
12
  sess: SqlSessionDep,
15
- mfa: Optional[MFAProof] = Body(None, embed=True, alias=body_field),
16
- mfa_code: Optional[str] = Query(None, alias="mfa_code"),
17
- mfa_pre_token: Optional[str] = Query(None, alias="mfa_pre_token"),
13
+ mfa: MFAProof | None = Body(None, embed=True, alias=body_field),
14
+ mfa_code: str | None = Query(None, alias="mfa_code"),
15
+ mfa_pre_token: str | None = Query(None, alias="mfa_pre_token"),
18
16
  ):
19
17
  proof = mfa or (
20
18
  MFAProof(code=mfa_code, pre_token=mfa_pre_token)
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import hashlib
3
3
  import os
4
+ from datetime import UTC
4
5
 
5
6
  import pyotp
6
7
 
@@ -38,6 +39,6 @@ def _hash(s: str) -> str:
38
39
 
39
40
 
40
41
  def _now_utc_ts() -> int:
41
- from datetime import datetime, timezone
42
+ from datetime import datetime
42
43
 
43
- return int(datetime.now(timezone.utc).timestamp())
44
+ return int(datetime.now(UTC).timestamp())
@@ -73,18 +73,11 @@ async def verify_mfa_for_user(
73
73
  now = _now_utc_ts()
74
74
  if rec:
75
75
  attempts_left = rec.get("attempts_left")
76
- if (
77
- now <= rec["exp"]
78
- and attempts_left
79
- and attempts_left > 0
80
- and rec["hash"] == dig
81
- ):
76
+ if now <= rec["exp"] and attempts_left and attempts_left > 0 and rec["hash"] == dig:
82
77
  EMAIL_OTP_STORE.pop(uid, None) # burn on success
83
78
  return MFAResult(ok=True, method="email", attempts_left=None)
84
79
  # decrement on failure
85
80
  rec["attempts_left"] = max(0, (attempts_left or 0) - 1)
86
- return MFAResult(
87
- ok=False, method="email", attempts_left=rec["attempts_left"]
88
- )
81
+ return MFAResult(ok=False, method="email", attempts_left=rec["attempts_left"])
89
82
 
90
83
  return MFAResult(ok=False, method="none", attempts_left=None)
@@ -1,7 +1,7 @@
1
- from typing import Any, Dict
1
+ from typing import Any
2
2
 
3
3
 
4
- def providers_from_settings(settings: Any) -> Dict[str, Dict[str, Any]]:
4
+ def providers_from_settings(settings: Any) -> dict[str, dict[str, Any]]:
5
5
  """
6
6
  Returns a registry of providers:
7
7
  {
@@ -20,7 +20,7 @@ def providers_from_settings(settings: Any) -> Dict[str, Dict[str, Any]]:
20
20
  }
21
21
  }
22
22
  """
23
- reg: Dict[str, Dict[str, Any]] = {}
23
+ reg: dict[str, dict[str, Any]] = {}
24
24
 
25
25
  # Google (OIDC)
26
26
  if getattr(settings, "google_client_id", None) and getattr(
@@ -64,9 +64,7 @@ def providers_from_settings(settings: Any) -> Dict[str, Dict[str, Any]]:
64
64
  }
65
65
 
66
66
  # LinkedIn (non-OIDC)
67
- if getattr(settings, "li_client_id", None) and getattr(
68
- settings, "li_client_secret", None
69
- ):
67
+ if getattr(settings, "li_client_id", None) and getattr(settings, "li_client_secret", None):
70
68
  reg["linkedin"] = {
71
69
  "kind": "linkedin",
72
70
  "authorize_url": "https://www.linkedin.com/oauth/v2/authorization",
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime, timedelta, timezone
4
- from typing import Any, List, Optional, cast
3
+ from datetime import UTC, datetime, timedelta
4
+ from typing import Any, cast
5
5
  from uuid import UUID
6
6
 
7
7
  from fastapi import HTTPException, Query
@@ -23,21 +23,21 @@ from svc_infra.db.sql.apikey import get_apikey_model
23
23
 
24
24
  class ApiKeyCreateIn(BaseModel):
25
25
  name: str
26
- user_id: Optional[str] = None
27
- scopes: List[str] = Field(default_factory=list)
28
- ttl_hours: Optional[int] = 24 * 365 # default 1y
26
+ user_id: str | None = None
27
+ scopes: list[str] = Field(default_factory=list)
28
+ ttl_hours: int | None = 24 * 365 # default 1y
29
29
 
30
30
 
31
31
  class ApiKeyOut(BaseModel):
32
32
  id: str
33
33
  name: str
34
- user_id: Optional[str]
35
- key: Optional[str] = None
34
+ user_id: str | None
35
+ key: str | None = None
36
36
  key_prefix: str
37
- scopes: List[str]
37
+ scopes: list[str]
38
38
  active: bool
39
- expires_at: Optional[datetime]
40
- last_used_at: Optional[datetime]
39
+ expires_at: datetime | None
40
+ last_used_at: datetime | None
41
41
 
42
42
 
43
43
  def _to_uuid(val):
@@ -56,7 +56,7 @@ def apikey_router():
56
56
  description="Create a new API key. The plaintext key is shown only once, at creation time.",
57
57
  )
58
58
  async def create_key(sess: SqlSessionDep, payload: ApiKeyCreateIn, p: Identity):
59
- caller_id: UUID = getattr(p.user, "id")
59
+ caller_id: UUID = p.user.id
60
60
  owner_id: UUID = _to_uuid(payload.user_id) if payload.user_id else caller_id
61
61
 
62
62
  if owner_id != caller_id and not getattr(p.user, "is_superuser", False):
@@ -64,9 +64,7 @@ def apikey_router():
64
64
 
65
65
  plaintext, prefix, hashed = ApiKey.make_secret() # type: ignore[attr-defined]
66
66
  expires = (
67
- (datetime.now(timezone.utc) + timedelta(hours=payload.ttl_hours))
68
- if payload.ttl_hours
69
- else None
67
+ (datetime.now(UTC) + timedelta(hours=payload.ttl_hours)) if payload.ttl_hours else None
70
68
  )
71
69
 
72
70
  row = ApiKey(
@@ -124,11 +122,11 @@ def apikey_router():
124
122
  description="Revoke an API key",
125
123
  )
126
124
  async def revoke_key(key_id: str, sess: SqlSessionDep, p: Identity):
127
- row = await cast(Any, sess).get(ApiKey, key_id)
125
+ row = await cast("Any", sess).get(ApiKey, key_id)
128
126
  if not row:
129
127
  raise HTTPException(404, "not_found")
130
128
 
131
- caller_id: UUID = getattr(p.user, "id")
129
+ caller_id: UUID = p.user.id
132
130
  if not (getattr(p.user, "is_superuser", False) or row.user_id == caller_id):
133
131
  raise HTTPException(403, "forbidden")
134
132
 
@@ -148,11 +146,11 @@ def apikey_router():
148
146
  p: Identity,
149
147
  force: bool = Query(False, description="Allow deleting an active key if True"),
150
148
  ):
151
- row = await cast(Any, sess).get(ApiKey, key_id)
149
+ row = await cast("Any", sess).get(ApiKey, key_id)
152
150
  if not row:
153
151
  return # 204
154
152
 
155
- caller_id: UUID = getattr(p.user, "id")
153
+ caller_id: UUID = p.user.id
156
154
  if not (getattr(p.user, "is_superuser", False) or row.user_id == caller_id):
157
155
  raise HTTPException(403, "forbidden")
158
156