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,13 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Callable
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
 
5
6
  from fastapi import APIRouter
6
7
 
7
8
 
8
- def apply_default_security(
9
- router: APIRouter, *, default_security: list[dict] | None
10
- ) -> None:
9
+ def apply_default_security(router: APIRouter, *, default_security: list[dict] | None) -> None:
11
10
  if default_security is None:
12
11
  return
13
12
  original_add = router.add_api_route
@@ -19,7 +18,7 @@ def apply_default_security(
19
18
  kwargs["openapi_extra"] = ox
20
19
  return original_add(path, endpoint, **kwargs)
21
20
 
22
- setattr(router, "add_api_route", _wrapped_add_api_route)
21
+ router.add_api_route = _wrapped_add_api_route # type: ignore[method-assign]
23
22
 
24
23
 
25
24
  def apply_default_responses(router: APIRouter, defaults: dict[int, dict]) -> None:
@@ -40,4 +39,4 @@ def apply_default_responses(router: APIRouter, defaults: dict[int, dict]) -> Non
40
39
  kwargs["responses"] = responses
41
40
  return original_add(path, endpoint, **kwargs)
42
41
 
43
- setattr(router, "add_api_route", _wrapped_add_api_route)
42
+ router.add_api_route = _wrapped_add_api_route # type: ignore[method-assign]
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict
3
+ from typing import Any
4
4
 
5
5
  from fastapi import FastAPI
6
6
 
7
7
  from .mutators import conventions_mutator
8
8
  from .pipeline import apply_mutators
9
9
 
10
- PROBLEM_SCHEMA: Dict[str, Any] = {
10
+ PROBLEM_SCHEMA: dict[str, Any] = {
11
11
  "type": "object",
12
12
  "properties": {
13
13
  "type": {
@@ -49,7 +49,7 @@ PROBLEM_SCHEMA: Dict[str, Any] = {
49
49
  }
50
50
 
51
51
 
52
- def _problem_example(**kw: Any) -> Dict[str, Any]:
52
+ def _problem_example(**kw: Any) -> dict[str, Any]:
53
53
  base = {
54
54
  "type": "about:blank",
55
55
  "title": "Internal Server Error",
@@ -63,7 +63,7 @@ def _problem_example(**kw: Any) -> Dict[str, Any]:
63
63
  return base
64
64
 
65
65
 
66
- STANDARD_RESPONSES: Dict[str, Dict[str, Any]] = {
66
+ STANDARD_RESPONSES: dict[str, dict[str, Any]] = {
67
67
  "BadRequest": {
68
68
  "description": "The request is malformed or missing required fields",
69
69
  "content": {
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import Callable, Dict, Iterable, Iterator, Tuple
4
+ from collections.abc import Callable, Iterable, Iterator
5
5
 
6
6
  from ..auth.security import auth_login_path
7
7
  from .models import APIVersionSpec, ServiceInfo, VersionInfo
@@ -9,7 +9,7 @@ from .models import APIVersionSpec, ServiceInfo, VersionInfo
9
9
  _HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
10
10
 
11
11
 
12
- def _iter_ops(schema: dict) -> Iterator[Tuple[str, str, dict]]:
12
+ def _iter_ops(schema: dict) -> Iterator[tuple[str, str, dict]]:
13
13
  """Yield (path, method, op) for each operation object."""
14
14
  paths = schema.get("paths") or {}
15
15
  for path, methods in paths.items():
@@ -273,9 +273,7 @@ def normalize_problem_and_examples_mutator():
273
273
  if not isinstance(val, dict):
274
274
  return
275
275
  inst = val.get("instance")
276
- if isinstance(inst, str) and (
277
- inst.startswith("/") or inst.startswith("about:")
278
- ):
276
+ if isinstance(inst, str) and (inst.startswith("/") or inst.startswith("about:")):
279
277
  # make absolute to satisfy format: uri
280
278
  val["instance"] = ABSOLUTE_INSTANCE
281
279
 
@@ -481,7 +479,7 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
481
479
 
482
480
  # map existing tags by name and preserve their fields
483
481
  existing_list = schema.get("tags") or []
484
- existing_map: Dict[str, dict] = {}
482
+ existing_map: dict[str, dict] = {}
485
483
  for item in existing_list:
486
484
  if isinstance(item, dict) and "name" in item:
487
485
  existing_map[item["name"]] = dict(item)
@@ -498,9 +496,7 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
498
496
  existing_map[name]["description"] = default_desc.format(tag=name)
499
497
 
500
498
  if existing_map:
501
- schema["tags"] = sorted(
502
- existing_map.values(), key=lambda x: x.get("name", "")
503
- )
499
+ schema["tags"] = sorted(existing_map.values(), key=lambda x: x.get("name", ""))
504
500
 
505
501
  return schema
506
502
 
@@ -508,8 +504,8 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
508
504
 
509
505
 
510
506
  def attach_standard_responses_mutator(
511
- codes: Dict[int, str] | None = None,
512
- per_method: Dict[str, Iterable[int]] | None = None,
507
+ codes: dict[int, str] | None = None,
508
+ per_method: dict[str, Iterable[int]] | None = None,
513
509
  exclude_tags: set[str] | None = None,
514
510
  op_flag_disable: str = "x_disable_standard_responses",
515
511
  ):
@@ -657,9 +653,7 @@ def ensure_request_body_descriptions_mutator(
657
653
  if isinstance(rb, dict):
658
654
  desc = rb.get("description")
659
655
  if not isinstance(desc, str) or not desc.strip():
660
- rb["description"] = default_template.format(
661
- method=method.upper(), path=path
662
- )
656
+ rb["description"] = default_template.format(method=method.upper(), path=path)
663
657
  return schema
664
658
 
665
659
  return m
@@ -847,9 +841,7 @@ def inject_safe_examples_mutator():
847
841
  """
848
842
 
849
843
  def _has_examples(mt_obj: dict) -> bool:
850
- return isinstance(mt_obj, dict) and (
851
- "example" in mt_obj or "examples" in mt_obj
852
- )
844
+ return isinstance(mt_obj, dict) and ("example" in mt_obj or "examples" in mt_obj)
853
845
 
854
846
  def m(schema: dict) -> dict:
855
847
  schema = dict(schema)
@@ -1095,11 +1087,7 @@ def ensure_success_examples_mutator():
1095
1087
  if not (200 <= ic < 300) or ic == 204:
1096
1088
  continue
1097
1089
  mt_obj = (resp.get("content") or {}).get("application/json")
1098
- if (
1099
- not isinstance(mt_obj, dict)
1100
- or "example" in mt_obj
1101
- or "examples" in mt_obj
1102
- ):
1090
+ if not isinstance(mt_obj, dict) or "example" in mt_obj or "examples" in mt_obj:
1103
1091
  continue
1104
1092
  sch = mt_obj.get("schema") or {}
1105
1093
 
@@ -1199,9 +1187,7 @@ def ensure_problem_examples_mutator():
1199
1187
  if mt_obj is None:
1200
1188
  # Create a basic media type referencing Problem schema when appropriate
1201
1189
  if mt == "application/problem+json":
1202
- mt_obj = {
1203
- "schema": {"$ref": "#/components/schemas/Problem"}
1204
- }
1190
+ mt_obj = {"schema": {"$ref": "#/components/schemas/Problem"}}
1205
1191
  content[mt] = mt_obj
1206
1192
  else:
1207
1193
  continue
@@ -1495,12 +1481,8 @@ def attach_header_params_mutator():
1495
1481
  if 200 <= ic < 300:
1496
1482
  hdrs = resp.setdefault("headers", {})
1497
1483
  hdrs.setdefault("ETag", {"$ref": "#/components/headers/ETag"})
1498
- hdrs.setdefault(
1499
- "Last-Modified", {"$ref": "#/components/headers/LastModified"}
1500
- )
1501
- hdrs.setdefault(
1502
- "X-Request-Id", {"$ref": "#/components/headers/XRequestId"}
1503
- )
1484
+ hdrs.setdefault("Last-Modified", {"$ref": "#/components/headers/LastModified"})
1485
+ hdrs.setdefault("X-Request-Id", {"$ref": "#/components/headers/XRequestId"})
1504
1486
  hdrs.setdefault(
1505
1487
  "X-RateLimit-Limit",
1506
1488
  {"$ref": "#/components/headers/XRateLimitLimit"},
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable
3
+ from collections.abc import Callable
4
4
 
5
5
  from fastapi import FastAPI
6
6
  from fastapi.openapi.utils import get_openapi
@@ -23,4 +23,4 @@ def apply_mutators(app: FastAPI, *mutators):
23
23
  app.openapi_schema = schema
24
24
  return schema
25
25
 
26
- setattr(app, "openapi", patched)
26
+ app.openapi = patched # type: ignore[method-assign]
@@ -1,32 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Dict
4
-
5
3
 
6
4
  def ref(name: str) -> dict:
7
5
  return {"$ref": f"#/components/responses/{name}"}
8
6
 
9
7
 
10
- DEFAULT_PUBLIC: Dict[int, dict] = {
8
+ DEFAULT_PUBLIC: dict[int, dict] = {
11
9
  400: ref("BadRequest"),
12
10
  422: ref("ValidationError"),
13
11
  500: ref("ServerError"),
14
12
  }
15
- DEFAULT_USER: Dict[int, dict] = {
13
+ DEFAULT_USER: dict[int, dict] = {
16
14
  400: ref("BadRequest"),
17
15
  401: ref("Unauthorized"),
18
16
  403: ref("Forbidden"),
19
17
  422: ref("ValidationError"),
20
18
  500: ref("ServerError"),
21
19
  }
22
- DEFAULT_SERVICE: Dict[int, dict] = {
20
+ DEFAULT_SERVICE: dict[int, dict] = {
23
21
  400: ref("BadRequest"),
24
22
  401: ref("Unauthorized"),
25
23
  403: ref("Forbidden"),
26
24
  429: ref("TooManyRequests"),
27
25
  500: ref("ServerError"),
28
26
  }
29
- DEFAULT_PROTECTED: Dict[int, dict] = {
27
+ DEFAULT_PROTECTED: dict[int, dict] = {
30
28
  400: ref("BadRequest"),
31
29
  401: ref("Unauthorized"),
32
30
  403: ref("Forbidden"),
@@ -6,9 +6,7 @@ from .mutators import auth_mutator
6
6
  from .pipeline import apply_mutators
7
7
 
8
8
 
9
- def _normalize_security_list(
10
- sec: list | None, *, drop_schemes: set[str] | None = None
11
- ) -> list:
9
+ def _normalize_security_list(sec: list | None, *, drop_schemes: set[str] | None = None) -> list:
12
10
  if not sec:
13
11
  return []
14
12
  drop_schemes = drop_schemes or set()
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from typing import Callable
4
+ from collections.abc import Callable
5
5
 
6
6
  from fastapi import FastAPI, HTTPException, Request
7
7
  from starlette.responses import JSONResponse
@@ -16,21 +16,19 @@ def add_probes(
16
16
  """Mount basic liveness/readiness/startup probes under prefix."""
17
17
  from svc_infra.api.fastapi.dual.public import public_router
18
18
 
19
- router = public_router(
20
- prefix=prefix, tags=["ops"], include_in_schema=include_in_schema
21
- )
19
+ router = public_router(prefix=prefix, tags=["ops"], include_in_schema=include_in_schema)
22
20
 
23
21
  @router.get("/live")
24
- async def live() -> JSONResponse: # noqa: D401, ANN201
22
+ async def live() -> JSONResponse:
25
23
  return JSONResponse({"status": "ok"})
26
24
 
27
25
  @router.get("/ready")
28
- async def ready() -> JSONResponse: # noqa: D401, ANN201
26
+ async def ready() -> JSONResponse:
29
27
  # In the future, add checks (DB ping, cache ping) via DI hooks.
30
28
  return JSONResponse({"status": "ok"})
31
29
 
32
30
  @router.get("/startup")
33
- async def startup_probe() -> JSONResponse: # noqa: D401, ANN201
31
+ async def startup_probe() -> JSONResponse:
34
32
  return JSONResponse({"status": "ok"})
35
33
 
36
34
  app.include_router(router)
@@ -48,7 +46,7 @@ def add_maintenance_mode(
48
46
  """
49
47
 
50
48
  @app.middleware("http")
51
- async def _maintenance_gate(request: Request, call_next): # noqa: ANN001, ANN202
49
+ async def _maintenance_gate(request: Request, call_next):
52
50
  flag = str(os.getenv(env_var, "")).lower() in {"1", "true", "yes", "on"}
53
51
  if flag and request.method not in {"GET", "HEAD", "OPTIONS"}:
54
52
  path = request.scope.get("path", "")
@@ -65,7 +63,7 @@ def circuit_breaker_dependency(limit: int = 100, window_seconds: int = 60) -> Ca
65
63
  breaker. Here, we read an env var to simulate an open breaker.
66
64
  """
67
65
 
68
- async def _dep(_: Request) -> None: # noqa: D401, ANN202
66
+ async def _dep(_: Request) -> None:
69
67
  if str(os.getenv("CIRCUIT_OPEN", "")).lower() in {"1", "true", "yes", "on"}:
70
68
  raise HTTPException(status_code=503, detail="circuit open")
71
69
 
@@ -4,14 +4,10 @@ import base64
4
4
  import contextvars
5
5
  import json
6
6
  import logging
7
+ from collections.abc import Callable, Iterable, Sequence
7
8
  from typing import (
8
9
  Any,
9
- Callable,
10
10
  Generic,
11
- Iterable,
12
- List,
13
- Optional,
14
- Sequence,
15
11
  TypeVar,
16
12
  cast,
17
13
  )
@@ -26,7 +22,7 @@ T = TypeVar("T")
26
22
 
27
23
  # ---------- Core query models ----------
28
24
  class CursorParams(BaseModel):
29
- cursor: Optional[str] = None
25
+ cursor: str | None = None
30
26
  limit: int = 50
31
27
 
32
28
 
@@ -36,19 +32,19 @@ class PageParams(BaseModel):
36
32
 
37
33
 
38
34
  class FilterParams(BaseModel):
39
- q: Optional[str] = None
40
- sort: Optional[str] = None
41
- created_after: Optional[str] = None
42
- created_before: Optional[str] = None
43
- updated_after: Optional[str] = None
44
- updated_before: Optional[str] = None
35
+ q: str | None = None
36
+ sort: str | None = None
37
+ created_after: str | None = None
38
+ created_before: str | None = None
39
+ updated_after: str | None = None
40
+ updated_before: str | None = None
45
41
 
46
42
 
47
43
  # ---------- Envelope model ----------
48
44
  class Paginated(BaseModel, Generic[T]):
49
- items: List[T]
50
- next_cursor: Optional[str] = Field(None, description="Opaque cursor for next page")
51
- total: Optional[int] = Field(None, description="Total items (optional)")
45
+ items: list[T]
46
+ next_cursor: str | None = Field(None, description="Opaque cursor for next page")
47
+ total: int | None = Field(None, description="Total items (optional)")
52
48
 
53
49
 
54
50
  # ---------- Cursor helpers ----------
@@ -57,13 +53,13 @@ def _encode_cursor(payload: dict) -> str:
57
53
  return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
58
54
 
59
55
 
60
- def decode_cursor(token: Optional[str]) -> dict[Any, Any]:
56
+ def decode_cursor(token: str | None) -> dict[Any, Any]:
61
57
  """Public: decode an incoming cursor token for debugging/ops."""
62
58
  if not token:
63
59
  return {}
64
60
  s = token + "=" * (-len(token) % 4)
65
61
  raw = base64.urlsafe_b64decode(s.encode("ascii")).decode("utf-8")
66
- return cast(dict[Any, Any], json.loads(raw))
62
+ return cast("dict[Any, Any]", json.loads(raw))
67
63
 
68
64
 
69
65
  # ---------- Context ----------
@@ -97,10 +93,8 @@ class PaginationContext(Generic[T]):
97
93
  self.limit_override = limit_override
98
94
 
99
95
  @property
100
- def cursor(self) -> Optional[str]:
101
- return (
102
- (self.cursor_params or CursorParams()).cursor if self.allow_cursor else None
103
- )
96
+ def cursor(self) -> str | None:
97
+ return (self.cursor_params or CursorParams()).cursor if self.allow_cursor else None
104
98
 
105
99
  @property
106
100
  def limit(self) -> int:
@@ -113,16 +107,12 @@ class PaginationContext(Generic[T]):
113
107
  return 50
114
108
 
115
109
  @property
116
- def page(self) -> Optional[int]:
110
+ def page(self) -> int | None:
117
111
  return self.page_params.page if (self.allow_page and self.page_params) else None
118
112
 
119
113
  @property
120
- def page_size(self) -> Optional[int]:
121
- return (
122
- self.page_params.page_size
123
- if (self.allow_page and self.page_params)
124
- else None
125
- )
114
+ def page_size(self) -> int | None:
115
+ return self.page_params.page_size if (self.allow_page and self.page_params) else None
126
116
 
127
117
  @property
128
118
  def offset(self) -> int:
@@ -134,8 +124,8 @@ class PaginationContext(Generic[T]):
134
124
  self,
135
125
  items: list[T],
136
126
  *,
137
- next_cursor: Optional[str] = None,
138
- total: Optional[int] = None,
127
+ next_cursor: str | None = None,
128
+ total: int | None = None,
139
129
  ):
140
130
  if self.envelope:
141
131
  return Paginated[T](items=items, next_cursor=next_cursor, total=total)
@@ -143,15 +133,15 @@ class PaginationContext(Generic[T]):
143
133
 
144
134
  def next_cursor_from_last(
145
135
  self, items: Sequence[T], *, key: Callable[[T], str | int]
146
- ) -> Optional[str]:
136
+ ) -> str | None:
147
137
  if not items:
148
138
  return None
149
139
  last_key = key(items[-1])
150
140
  return _encode_cursor({"after": last_key})
151
141
 
152
142
 
153
- _pagination_ctx: contextvars.ContextVar[PaginationContext | None] = (
154
- contextvars.ContextVar("pagination_ctx", default=None)
143
+ _pagination_ctx: contextvars.ContextVar[PaginationContext | None] = contextvars.ContextVar(
144
+ "pagination_ctx", default=None
155
145
  )
156
146
 
157
147
 
@@ -171,9 +161,7 @@ def use_pagination() -> PaginationContext:
171
161
 
172
162
 
173
163
  # ---------- Utilities ----------
174
- def text_filter(
175
- items: Iterable[T], q: Optional[str], *getters: Callable[[T], str]
176
- ) -> list[T]:
164
+ def text_filter(items: Iterable[T], q: str | None, *getters: Callable[[T], str]) -> list[T]:
177
165
  if not q:
178
166
  return list(items)
179
167
  ql = q.lower()
@@ -195,7 +183,7 @@ def sort_by(
195
183
  key: Callable[[T], Any],
196
184
  desc: bool = False,
197
185
  ) -> list[T]:
198
- return sorted(list(items), key=key, reverse=desc)
186
+ return sorted(items, key=key, reverse=desc)
199
187
 
200
188
 
201
189
  def cursor_window(items, *, cursor, limit, key, descending: bool, offset: int = 0):
@@ -4,7 +4,7 @@ import importlib
4
4
  import logging
5
5
  import pkgutil
6
6
  from types import ModuleType
7
- from typing import Any, Optional
7
+ from typing import Any
8
8
 
9
9
  from fastapi import FastAPI
10
10
  from fastapi.routing import APIRoute
@@ -55,9 +55,7 @@ def _validate_base_package(base_package: str) -> ModuleType:
55
55
  try:
56
56
  package_module: ModuleType = importlib.import_module(base_package)
57
57
  except Exception as exc:
58
- raise RuntimeError(
59
- f"Could not import base_package '{base_package}': {exc}"
60
- ) from exc
58
+ raise RuntimeError(f"Could not import base_package '{base_package}': {exc}") from exc
61
59
 
62
60
  if not hasattr(package_module, "__path__"):
63
61
  raise RuntimeError(
@@ -67,21 +65,17 @@ def _validate_base_package(base_package: str) -> ModuleType:
67
65
  return package_module
68
66
 
69
67
 
70
- def _normalize_environment(environment: Optional[Environment | str]) -> Environment:
68
+ def _normalize_environment(environment: Environment | str | None) -> Environment:
71
69
  """Normalize the environment parameter."""
72
70
  return (
73
71
  CURRENT_ENVIRONMENT
74
72
  if environment is None
75
- else (
76
- Environment(environment)
77
- if not isinstance(environment, Environment)
78
- else environment
79
- )
73
+ else (Environment(environment) if not isinstance(environment, Environment) else environment)
80
74
  )
81
75
 
82
76
 
83
77
  def _should_force_include_in_schema(
84
- environment: Environment, force_include_in_schema: Optional[bool]
78
+ environment: Environment, force_include_in_schema: bool | None
85
79
  ) -> bool:
86
80
  """Determine if routers should be forced to include in schema."""
87
81
  if force_include_in_schema is None:
@@ -99,12 +93,9 @@ def _is_router_excluded_by_environment(
99
93
 
100
94
  # Support ALL_ENVIRONMENTS as a special value
101
95
  if router_excluded_envs is ALL_ENVIRONMENTS or (
102
- isinstance(router_excluded_envs, set)
103
- and router_excluded_envs == ALL_ENVIRONMENTS
96
+ isinstance(router_excluded_envs, set) and router_excluded_envs == ALL_ENVIRONMENTS
104
97
  ):
105
- logger.debug(
106
- f"Skipping router module {module_name} due to ALL_ENVIRONMENTS exclusion."
107
- )
98
+ logger.debug(f"Skipping router module {module_name} due to ALL_ENVIRONMENTS exclusion.")
108
99
  return True
109
100
 
110
101
  # Normalize to set of Environment or str
@@ -117,16 +108,11 @@ def _is_router_excluded_by_environment(
117
108
  normalized_excluded_envs: set[Environment | str] = set()
118
109
  for e in router_excluded_envs:
119
110
  try:
120
- normalized_excluded_envs.add(
121
- Environment(e) if not isinstance(e, Environment) else e
122
- )
111
+ normalized_excluded_envs.add(Environment(e) if not isinstance(e, Environment) else e)
123
112
  except Exception:
124
113
  normalized_excluded_envs.add(str(e))
125
114
 
126
- if (
127
- environment in normalized_excluded_envs
128
- or str(environment) in normalized_excluded_envs
129
- ):
115
+ if environment in normalized_excluded_envs or str(environment) in normalized_excluded_envs:
130
116
  logger.debug(
131
117
  f"Skipping router module {module_name} due to ROUTER_EXCLUDED_ENVIRONMENTS restriction: {router_excluded_envs}"
132
118
  )
@@ -225,10 +211,10 @@ def _process_router_module(
225
211
  def register_all_routers(
226
212
  app: FastAPI,
227
213
  *,
228
- base_package: Optional[str] = None,
214
+ base_package: str | None = None,
229
215
  prefix: str = "",
230
- environment: Optional[Environment | str] = None,
231
- force_include_in_schema: Optional[bool] = None,
216
+ environment: Environment | str | None = None,
217
+ force_include_in_schema: bool | None = None,
232
218
  ) -> None:
233
219
  """
234
220
  Recursively discover and register all FastAPI routers under a routers package.
@@ -250,24 +236,18 @@ def register_all_routers(
250
236
  """
251
237
  if base_package is None:
252
238
  if __package__ is None:
253
- raise RuntimeError(
254
- "Cannot derive base_package; please pass base_package explicitly."
255
- )
239
+ raise RuntimeError("Cannot derive base_package; please pass base_package explicitly.")
256
240
  base_package = __package__
257
241
 
258
242
  package_module = _validate_base_package(base_package)
259
243
  environment = _normalize_environment(environment)
260
- force_include = _should_force_include_in_schema(
261
- environment, force_include_in_schema
262
- )
244
+ force_include = _should_force_include_in_schema(environment, force_include_in_schema)
263
245
 
264
246
  for _, module_name, _ in pkgutil.walk_packages(
265
247
  package_module.__path__, prefix=f"{base_package}."
266
248
  ):
267
249
  if _should_skip_module(module_name):
268
- logger.debug(
269
- "Skipping router module due to exclusion/private: %s", module_name
270
- )
250
+ logger.debug("Skipping router module due to exclusion/private: %s", module_name)
271
251
  continue
272
252
 
273
253
  try:
@@ -276,6 +256,4 @@ def register_all_routers(
276
256
  logger.exception("Failed to import router module %s: %s", module_name, exc)
277
257
  continue
278
258
 
279
- _process_router_module(
280
- app, module, module_name, prefix, environment, force_include
281
- )
259
+ _process_router_module(app, module, module_name, prefix, environment, force_include)