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
svc_infra/cache/utils.py CHANGED
@@ -8,7 +8,8 @@ hashing complex objects, and formatting key templates.
8
8
  import hashlib
9
9
  import json
10
10
  import logging
11
- from typing import Any, Iterable, Union
11
+ from collections.abc import Iterable
12
+ from typing import Any
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
@@ -33,9 +34,7 @@ def stable_hash(*args: Any, **kwargs: Any) -> str:
33
34
  """
34
35
  try:
35
36
  # Use JSON serialization for stable, deterministic output
36
- raw = json.dumps(
37
- [args, kwargs], default=str, sort_keys=True, separators=(",", ":")
38
- )
37
+ raw = json.dumps([args, kwargs], default=str, sort_keys=True, separators=(",", ":"))
39
38
  except (TypeError, ValueError) as e:
40
39
  # Fallback to repr if JSON serialization fails
41
40
  logger.warning(f"JSON serialization failed for hash input, using repr: {e}")
@@ -44,7 +43,7 @@ def stable_hash(*args: Any, **kwargs: Any) -> str:
44
43
  return hashlib.sha1(raw.encode("utf-8")).hexdigest()
45
44
 
46
45
 
47
- def join_key(parts: Iterable[Union[str, int, None]]) -> str:
46
+ def join_key(parts: Iterable[str | int | None]) -> str:
48
47
  """
49
48
  Join key parts into a cache key, filtering out empty values.
50
49
 
@@ -100,7 +99,7 @@ def format_tuple_key(key_tuple: tuple[str, ...], **kwargs) -> str:
100
99
  raise ValueError(f"Failed to format key template: {e}") from e
101
100
 
102
101
 
103
- def normalize_cache_key(key: Union[str, tuple[str, ...]], **kwargs) -> str:
102
+ def normalize_cache_key(key: str | tuple[str, ...], **kwargs) -> str:
104
103
  """
105
104
  Normalize a cache key from various input formats.
106
105
 
svc_infra/cli/__init__.py CHANGED
@@ -23,9 +23,7 @@ app = typer.Typer(no_args_is_help=True, add_completion=False, help=_HELP)
23
23
  pre_cli(app)
24
24
 
25
25
  # --- db ops group ---
26
- db_app = typer.Typer(
27
- no_args_is_help=True, add_completion=False, help="Database operations"
28
- )
26
+ db_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Database operations")
29
27
  register_db_ops(db_app)
30
28
  app.add_typer(db_app, name="db")
31
29
 
@@ -37,24 +35,18 @@ register_sql_export(sql_app)
37
35
  app.add_typer(sql_app, name="sql")
38
36
 
39
37
  # --- mongo group ---
40
- mongo_app = typer.Typer(
41
- no_args_is_help=True, add_completion=False, help="MongoDB commands"
42
- )
38
+ mongo_app = typer.Typer(no_args_is_help=True, add_completion=False, help="MongoDB commands")
43
39
  register_mongo(mongo_app)
44
40
  register_mongo_scaffold(mongo_app)
45
41
  app.add_typer(mongo_app, name="mongo")
46
42
 
47
43
  # --- health group ---
48
- health_app = typer.Typer(
49
- no_args_is_help=True, add_completion=False, help="Health checks"
50
- )
44
+ health_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Health checks")
51
45
  register_health(health_app)
52
46
  app.add_typer(health_app, name="health")
53
47
 
54
48
  # -- obs group ---
55
- obs_app = typer.Typer(
56
- no_args_is_help=True, add_completion=False, help="Observability commands"
57
- )
49
+ obs_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Observability commands")
58
50
  register_obs(obs_app)
59
51
  app.add_typer(obs_app, name="obs")
60
52
 
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import importlib
4
4
  import os
5
- from typing import Any, Optional, Sequence
5
+ from collections.abc import Sequence
6
+ from typing import Any
6
7
 
7
8
  import typer
8
9
 
@@ -17,7 +18,7 @@ from svc_infra.db.nosql.utils import prepare_process_env
17
18
  # -------------------- helpers --------------------
18
19
 
19
20
 
20
- def _apply_mongo_env(mongo_url: Optional[str], mongo_db: Optional[str]) -> None:
21
+ def _apply_mongo_env(mongo_url: str | None, mongo_db: str | None) -> None:
21
22
  """If provided, set MONGO_URL / MONGO_DB for the current process."""
22
23
  if mongo_url:
23
24
  os.environ["MONGO_URL"] = mongo_url
@@ -63,13 +64,13 @@ def cmd_prepare(
63
64
  "--resources",
64
65
  help="Dotted path to NoSqlResource(s). e.g. 'app.db.mongo:RESOURCES'",
65
66
  ),
66
- mongo_url: Optional[str] = typer.Option(
67
+ mongo_url: str | None = typer.Option(
67
68
  None, "--mongo-url", help="Overrides env MONGO_URL for this command."
68
69
  ),
69
- mongo_db: Optional[str] = typer.Option(
70
+ mongo_db: str | None = typer.Option(
70
71
  None, "--mongo-db", help="Overrides env MONGO_DB for this command."
71
72
  ),
72
- service_id: Optional[str] = typer.Option(
73
+ service_id: str | None = typer.Option(
73
74
  None,
74
75
  "--service-id",
75
76
  help="Stable ID for this service/app. Defaults to top-level module name.",
@@ -125,13 +126,13 @@ def cmd_setup_and_prepare(
125
126
  "--resources",
126
127
  help="Dotted path to NoSqlResource(s). e.g. 'app.db.mongo:RESOURCES'",
127
128
  ),
128
- mongo_url: Optional[str] = typer.Option(
129
+ mongo_url: str | None = typer.Option(
129
130
  None, "--mongo-url", help="Overrides env MONGO_URL for this command."
130
131
  ),
131
- mongo_db: Optional[str] = typer.Option(
132
+ mongo_db: str | None = typer.Option(
132
133
  None, "--mongo-db", help="Overrides env MONGO_DB for this command."
133
134
  ),
134
- service_id: Optional[str] = typer.Option(
135
+ service_id: str | None = typer.Option(
135
136
  None,
136
137
  "--service-id",
137
138
  help="Stable ID for this service/app. Defaults to top-level module name.",
@@ -157,10 +158,10 @@ def cmd_setup_and_prepare(
157
158
 
158
159
 
159
160
  def cmd_ping(
160
- mongo_url: Optional[str] = typer.Option(
161
+ mongo_url: str | None = typer.Option(
161
162
  None, "--mongo-url", help="Overrides env MONGO_URL for this command."
162
163
  ),
163
- mongo_db: Optional[str] = typer.Option(
164
+ mongo_db: str | None = typer.Option(
164
165
  None, "--mongo-db", help="Overrides env MONGO_DB for this command."
165
166
  ),
166
167
  ):
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  import typer
7
6
 
@@ -17,9 +16,7 @@ def cmd_scaffold(
17
16
  entity_name: str = typer.Option(
18
17
  "Item", help="Entity class name (e.g., User, Member, Product)."
19
18
  ),
20
- documents_dir: Path = typer.Option(
21
- ..., help="Directory for Mongo document models."
22
- ),
19
+ documents_dir: Path = typer.Option(..., help="Directory for Mongo document models."),
23
20
  schemas_dir: Path = typer.Option(..., help="Directory for Pydantic CRUD schemas."),
24
21
  overwrite: bool = typer.Option(False, help="Overwrite existing files."),
25
22
  same_dir: bool = typer.Option(
@@ -27,10 +24,10 @@ def cmd_scaffold(
27
24
  "--same-dir/--no-same-dir",
28
25
  help="Put documents & schemas into the same directory.",
29
26
  ),
30
- documents_filename: Optional[str] = typer.Option(
27
+ documents_filename: str | None = typer.Option(
31
28
  None, help="Custom filename for documents (separate-dir mode)."
32
29
  ),
33
- schemas_filename: Optional[str] = typer.Option(
30
+ schemas_filename: str | None = typer.Option(
34
31
  None, help="Custom filename for schemas (separate-dir mode)."
35
32
  ),
36
33
  ):
@@ -55,7 +52,7 @@ def cmd_scaffold_documents(
55
52
  dest_dir: Path = typer.Option(..., "--dest-dir", resolve_path=True),
56
53
  entity_name: str = typer.Option("Item", "--entity-name"),
57
54
  overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
58
- documents_filename: Optional[str] = typer.Option(
55
+ documents_filename: str | None = typer.Option(
59
56
  None,
60
57
  "--documents-filename",
61
58
  help="Filename to write (e.g. product_doc.py). Defaults to <snake(entity)>.py",
@@ -75,7 +72,7 @@ def cmd_scaffold_schemas(
75
72
  dest_dir: Path = typer.Option(..., "--dest-dir", resolve_path=True),
76
73
  entity_name: str = typer.Option("Item", "--entity-name"),
77
74
  overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
78
- schemas_filename: Optional[str] = typer.Option(
75
+ schemas_filename: str | None = typer.Option(
79
76
  None,
80
77
  "--schemas-filename",
81
78
  help="Filename to write (e.g. product_schemas.py). Defaults to <snake(entity)>.py",
@@ -98,7 +95,7 @@ def cmd_scaffold_resources(
98
95
  "--entity-name",
99
96
  help="Used only to prefill example placeholders.",
100
97
  ),
101
- filename: Optional[str] = typer.Option(
98
+ filename: str | None = typer.Option(
102
99
  None,
103
100
  "--filename",
104
101
  help='Output filename (default: "resources.py")',
@@ -10,13 +10,12 @@ from __future__ import annotations
10
10
  import asyncio
11
11
  import os
12
12
  import time
13
- from typing import Optional
14
13
 
15
14
  import typer
16
15
 
17
16
 
18
17
  def cmd_wait(
19
- database_url: Optional[str] = typer.Option(
18
+ database_url: str | None = typer.Option(
20
19
  None,
21
20
  "--url",
22
21
  "-u",
@@ -106,7 +105,7 @@ def cmd_kill_queries(
106
105
  ...,
107
106
  help="Table name to find blocking queries for.",
108
107
  ),
109
- database_url: Optional[str] = typer.Option(
108
+ database_url: str | None = typer.Option(
110
109
  None,
111
110
  "--url",
112
111
  "-u",
@@ -244,9 +243,7 @@ def cmd_kill_queries(
244
243
  typer.secho(f" {action} PID {pid}", fg=typer.colors.GREEN)
245
244
  else:
246
245
  if not quiet:
247
- typer.echo(
248
- f" PID {pid}: already finished or permission denied"
249
- )
246
+ typer.echo(f" PID {pid}: already finished or permission denied")
250
247
  except Exception as e:
251
248
  if not quiet:
252
249
  typer.secho(f" PID {pid}: Error - {e}", fg=typer.colors.YELLOW)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  from importlib import import_module
6
- from typing import List, Optional
7
6
 
8
7
  import typer
9
8
 
@@ -19,13 +18,13 @@ from svc_infra.db.sql.core import stamp as core_stamp
19
18
  from svc_infra.db.sql.core import upgrade as core_upgrade
20
19
 
21
20
 
22
- def apply_database_url(database_url: Optional[str]) -> None:
21
+ def apply_database_url(database_url: str | None) -> None:
23
22
  """If provided, set SQL_URL for the current process."""
24
23
  if database_url:
25
24
  os.environ["SQL_URL"] = database_url
26
25
 
27
26
 
28
- def _find_pkgs(with_payments, discover_packages) -> List[str]:
27
+ def _find_pkgs(with_payments, discover_packages) -> list[str]:
29
28
  from os import getenv
30
29
 
31
30
  payments_enabled = (
@@ -43,12 +42,12 @@ def _find_pkgs(with_payments, discover_packages) -> List[str]:
43
42
 
44
43
 
45
44
  def cmd_init(
46
- database_url: Optional[str] = typer.Option(
45
+ database_url: str | None = typer.Option(
47
46
  None,
48
47
  help="Database URL; overrides env SQL_URL for this command. "
49
48
  "Async vs sync is auto-detected from the URL.",
50
49
  ),
51
- discover_packages: Optional[List[str]] = typer.Option(
50
+ discover_packages: list[str] | None = typer.Option(
52
51
  None,
53
52
  help="Packages to search for SQLAlchemy metadata; may pass multiple. "
54
53
  "If omitted, automatic discovery is used.",
@@ -74,19 +73,13 @@ def cmd_init(
74
73
 
75
74
  def cmd_revision(
76
75
  message: str = typer.Option(..., "-m", "--message", help="Revision message."),
77
- database_url: Optional[str] = typer.Option(
76
+ database_url: str | None = typer.Option(
78
77
  None, help="Database URL; overrides env for this command."
79
78
  ),
80
- autogenerate: bool = typer.Option(
81
- False, help="Autogenerate migrations by comparing metadata."
82
- ),
83
- head: Optional[str] = typer.Option(
84
- "head", help="Set the head to base this revision on."
85
- ),
86
- branch_label: Optional[str] = typer.Option(None, help="Branch label."),
87
- version_path: Optional[str] = typer.Option(
88
- None, help="Alternative versions/ path."
89
- ),
79
+ autogenerate: bool = typer.Option(False, help="Autogenerate migrations by comparing metadata."),
80
+ head: str | None = typer.Option("head", help="Set the head to base this revision on."),
81
+ branch_label: str | None = typer.Option(None, help="Branch label."),
82
+ version_path: str | None = typer.Option(None, help="Alternative versions/ path."),
90
83
  sql: bool = typer.Option(False, help="Don't generate Python; dump SQL to stdout."),
91
84
  ):
92
85
  """Create a new Alembic revision, either empty or autogenerated."""
@@ -102,10 +95,8 @@ def cmd_revision(
102
95
 
103
96
 
104
97
  def cmd_upgrade(
105
- revision_target: str = typer.Argument(
106
- "head", help="Target revision (default head)."
107
- ),
108
- database_url: Optional[str] = typer.Option(
98
+ revision_target: str = typer.Argument("head", help="Target revision (default head)."),
99
+ database_url: str | None = typer.Option(
109
100
  None, help="Database URL; overrides env for this command."
110
101
  ),
111
102
  ):
@@ -116,7 +107,7 @@ def cmd_upgrade(
116
107
 
117
108
  def cmd_downgrade(
118
109
  revision_target: str = typer.Argument("-1", help="Target revision (default -1)."),
119
- database_url: Optional[str] = typer.Option(
110
+ database_url: str | None = typer.Option(
120
111
  None, help="Database URL; overrides env for this command."
121
112
  ),
122
113
  ):
@@ -126,7 +117,7 @@ def cmd_downgrade(
126
117
 
127
118
 
128
119
  def cmd_current(
129
- database_url: Optional[str] = typer.Option(
120
+ database_url: str | None = typer.Option(
130
121
  None, help="Database URL; overrides env for this command."
131
122
  ),
132
123
  verbose: bool = typer.Option(False, help="Verbose output."),
@@ -141,7 +132,7 @@ def cmd_current(
141
132
 
142
133
 
143
134
  def cmd_history(
144
- database_url: Optional[str] = typer.Option(
135
+ database_url: str | None = typer.Option(
145
136
  None, help="Database URL; overrides env for this command."
146
137
  ),
147
138
  verbose: bool = typer.Option(False, help="Verbose output."),
@@ -153,7 +144,7 @@ def cmd_history(
153
144
 
154
145
  def cmd_stamp(
155
146
  revision_target: str = typer.Argument("head"),
156
- database_url: Optional[str] = typer.Option(
147
+ database_url: str | None = typer.Option(
157
148
  None, help="Database URL; overrides env for this command."
158
149
  ),
159
150
  ):
@@ -163,12 +154,10 @@ def cmd_stamp(
163
154
 
164
155
 
165
156
  def cmd_merge_heads(
166
- database_url: Optional[str] = typer.Option(
157
+ database_url: str | None = typer.Option(
167
158
  None, help="Database URL; overrides env for this command."
168
159
  ),
169
- message: Optional[str] = typer.Option(
170
- None, "-m", "--message", help="Merge revision message."
171
- ),
160
+ message: str | None = typer.Option(None, "-m", "--message", help="Merge revision message."),
172
161
  ):
173
162
  """Create a merge revision for multiple heads."""
174
163
  apply_database_url(database_url)
@@ -176,23 +165,19 @@ def cmd_merge_heads(
176
165
 
177
166
 
178
167
  def cmd_setup_and_migrate(
179
- database_url: Optional[str] = typer.Option(
168
+ database_url: str | None = typer.Option(
180
169
  None,
181
170
  help="Overrides env for this command. Async vs sync is auto-detected from the URL.",
182
171
  ),
183
- overwrite_scaffold: bool = typer.Option(
184
- False, help="Overwrite alembic scaffold if present."
185
- ),
186
- create_db_if_missing: bool = typer.Option(
187
- True, help="Create the database/schema if missing."
188
- ),
172
+ overwrite_scaffold: bool = typer.Option(False, help="Overwrite alembic scaffold if present."),
173
+ create_db_if_missing: bool = typer.Option(True, help="Create the database/schema if missing."),
189
174
  create_followup_revision: bool = typer.Option(
190
175
  True, help="Create an autogen follow-up revision if revisions already exist."
191
176
  ),
192
177
  initial_message: str = typer.Option("initial schema"),
193
178
  followup_message: str = typer.Option("autogen"),
194
179
  # NEW:
195
- discover_packages: Optional[List[str]] = typer.Option(
180
+ discover_packages: list[str] | None = typer.Option(
196
181
  None,
197
182
  help="Packages Alembic should import to discover models "
198
183
  "(e.g. app.models,svc_infra.apf_payments.models).",
@@ -267,7 +252,7 @@ def _import_callable(path: str):
267
252
  # Example: tests use a global `called` dict; point legacy to unit
268
253
  try:
269
254
  if hasattr(unit_mod, "called"):
270
- setattr(mod, "called", getattr(unit_mod, "called"))
255
+ mod.called = unit_mod.called # type: ignore[attr-defined]
271
256
  except Exception:
272
257
  pass
273
258
  # If legacy mod missing but unit exists, use unit
@@ -277,15 +262,13 @@ def _import_callable(path: str):
277
262
  mod = import_module(mod_name)
278
263
  fn = getattr(mod, fn_name, None)
279
264
  if not callable(fn):
280
- raise typer.BadParameter(
281
- f"Callable '{fn_name}' not found in module '{mod_name}'"
282
- )
265
+ raise typer.BadParameter(f"Callable '{fn_name}' not found in module '{mod_name}'")
283
266
  return fn
284
267
 
285
268
 
286
269
  def cmd_seed(
287
270
  target: str = typer.Argument(..., help="Seed callable path 'module:func'"),
288
- database_url: Optional[str] = typer.Option(
271
+ database_url: str | None = typer.Option(
289
272
  None,
290
273
  help="Database URL; overrides env for this command.",
291
274
  ),
@@ -5,7 +5,7 @@ import json
5
5
  import os
6
6
  import sys
7
7
  from pathlib import Path
8
- from typing import Any, Optional, cast
8
+ from typing import Any, cast
9
9
 
10
10
  import typer
11
11
  from sqlalchemy import text
@@ -20,20 +20,12 @@ except Exception: # pragma: no cover - fallback when async extras unavailable
20
20
 
21
21
 
22
22
  def export_tenant(
23
- table: str = typer.Argument(
24
- ..., help="Qualified table name to export (e.g., public.items)"
25
- ),
26
- tenant_id: str = typer.Option(
27
- ..., "--tenant-id", help="Tenant id value to filter by."
28
- ),
29
- tenant_field: str = typer.Option(
30
- "tenant_id", help="Column name for tenant id filter."
31
- ),
32
- output: Optional[Path] = typer.Option(
33
- None, "--output", help="Output file; defaults to stdout."
34
- ),
35
- limit: Optional[int] = typer.Option(None, help="Max rows to export."),
36
- database_url: Optional[str] = typer.Option(
23
+ table: str = typer.Argument(..., help="Qualified table name to export (e.g., public.items)"),
24
+ tenant_id: str = typer.Option(..., "--tenant-id", help="Tenant id value to filter by."),
25
+ tenant_field: str = typer.Option("tenant_id", help="Column name for tenant id filter."),
26
+ output: Path | None = typer.Option(None, "--output", help="Output file; defaults to stdout."),
27
+ limit: int | None = typer.Option(None, help="Max rows to export."),
28
+ database_url: str | None = typer.Option(
37
29
  None, "--database-url", help="Overrides env SQL_URL for this command."
38
30
  ),
39
31
  ):
@@ -61,7 +53,7 @@ def export_tenant(
61
53
  is_async_engine = sa_async is not None and isinstance(engine, sa_async.AsyncEngine)
62
54
 
63
55
  if is_async_engine:
64
- async_engine = cast(Any, engine)
56
+ async_engine = cast("Any", engine)
65
57
 
66
58
  async def _fetch() -> list[dict[str, Any]]:
67
59
  async with async_engine.connect() as conn:
@@ -70,7 +62,7 @@ def export_tenant(
70
62
 
71
63
  rows = asyncio.run(_fetch())
72
64
  else:
73
- sync_engine = cast(Engine, engine)
65
+ sync_engine = cast("Engine", engine)
74
66
  with sync_engine.connect() as conn:
75
67
  result = conn.execute(stmt, params)
76
68
  rows = [dict(row) for row in result.mappings()]
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional, cast
4
+ from typing import cast
5
5
 
6
6
  import click
7
7
  import typer
@@ -23,7 +23,7 @@ def cmd_scaffold(
23
23
  entity_name: str = typer.Option(
24
24
  "Item", help="Class name for entity/auth (e.g., User, Member, Product)."
25
25
  ),
26
- table_name: Optional[str] = typer.Option(
26
+ table_name: str | None = typer.Option(
27
27
  None,
28
28
  help="Optional table name. For kind=auth, can also be set via AUTH_TABLE_NAME; defaults to plural snake of entity.",
29
29
  ),
@@ -35,10 +35,10 @@ def cmd_scaffold(
35
35
  "--same-dir/--no-same-dir",
36
36
  help="Put models & schemas into the same dir.",
37
37
  ),
38
- models_filename: Optional[str] = typer.Option(
38
+ models_filename: str | None = typer.Option(
39
39
  None, help="Custom filename for models (separate-dir mode)."
40
40
  ),
41
- schemas_filename: Optional[str] = typer.Option(
41
+ schemas_filename: str | None = typer.Option(
42
42
  None, help="Custom filename for schemas (separate-dir mode)."
43
43
  ),
44
44
  ):
@@ -50,7 +50,7 @@ def cmd_scaffold(
50
50
  res = scaffold_core(
51
51
  models_dir=models_dir,
52
52
  schemas_dir=schemas_dir,
53
- kind=cast(Kind, kind.lower()),
53
+ kind=cast("Kind", kind.lower()),
54
54
  entity_name=entity_name,
55
55
  table_name=table_name,
56
56
  overwrite=overwrite,
@@ -70,13 +70,13 @@ def cmd_scaffold_models(
70
70
  click_type=click.Choice(["entity", "auth"], case_sensitive=False),
71
71
  ),
72
72
  entity_name: str = typer.Option("Item", "--entity-name"),
73
- table_name: Optional[str] = typer.Option(None, "--table-name"),
73
+ table_name: str | None = typer.Option(None, "--table-name"),
74
74
  include_tenant: bool = typer.Option(True, "--include-tenant/--no-include-tenant"),
75
75
  include_soft_delete: bool = typer.Option(
76
76
  False, "--include-soft-delete/--no-include-soft-delete"
77
77
  ),
78
78
  overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
79
- models_filename: Optional[str] = typer.Option(
79
+ models_filename: str | None = typer.Option(
80
80
  None,
81
81
  "--models-filename",
82
82
  help="Filename to write (e.g. project_models.py). Defaults to <snake(entity)>.py",
@@ -89,7 +89,7 @@ def cmd_scaffold_models(
89
89
  """
90
90
  res = scaffold_models_core(
91
91
  dest_dir=dest_dir,
92
- kind=cast(Kind, kind.lower()),
92
+ kind=cast("Kind", kind.lower()),
93
93
  entity_name=entity_name,
94
94
  table_name=table_name,
95
95
  include_tenant=include_tenant,
@@ -111,7 +111,7 @@ def cmd_scaffold_schemas(
111
111
  entity_name: str = typer.Option("Item", "--entity-name"),
112
112
  include_tenant: bool = typer.Option(True, "--include-tenant/--no-include-tenant"),
113
113
  overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
114
- schemas_filename: Optional[str] = typer.Option(
114
+ schemas_filename: str | None = typer.Option(
115
115
  None,
116
116
  "--schemas-filename",
117
117
  help="Filename to write (e.g. project_schemas.py). Defaults to <snake(entity)>.py",
@@ -124,7 +124,7 @@ def cmd_scaffold_schemas(
124
124
  """
125
125
  res = scaffold_schemas_core(
126
126
  dest_dir=dest_dir,
127
- kind=cast(Kind, kind.lower()),
127
+ kind=cast("Kind", kind.lower()),
128
128
  entity_name=entity_name,
129
129
  include_tenant=include_tenant,
130
130
  overwrite=overwrite,
@@ -4,7 +4,6 @@ import os
4
4
  from importlib.resources import as_file
5
5
  from importlib.resources import files as pkg_files
6
6
  from pathlib import Path
7
- from typing import Dict, List
8
7
 
9
8
  import click
10
9
  import typer
@@ -17,8 +16,8 @@ def _norm(name: str) -> str:
17
16
  return name.strip().lower().replace(" ", "-").replace("_", "-")
18
17
 
19
18
 
20
- def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
21
- topics: Dict[str, Path] = {}
19
+ def _discover_fs_topics(docs_dir: Path) -> dict[str, Path]:
20
+ topics: dict[str, Path] = {}
22
21
  if docs_dir.exists() and docs_dir.is_dir():
23
22
  for p in sorted(docs_dir.glob("*.md")):
24
23
  if p.is_file():
@@ -26,12 +25,12 @@ def _discover_fs_topics(docs_dir: Path) -> Dict[str, Path]:
26
25
  return topics
27
26
 
28
27
 
29
- def _discover_pkg_topics() -> Dict[str, Path]:
28
+ def _discover_pkg_topics() -> dict[str, Path]:
30
29
  """
31
30
  Discover docs shipped inside the installed package at svc_infra/docs/*,
32
31
  using importlib.resources so this works for wheels, sdists, and zipped wheels.
33
32
  """
34
- topics: Dict[str, Path] = {}
33
+ topics: dict[str, Path] = {}
35
34
  try:
36
35
  docs_root = pkg_files("svc_infra").joinpath("docs")
37
36
  # docs_root is a Traversable; it may be inside a zip. Iterate safely.
@@ -74,8 +73,8 @@ def _resolve_docs_dir(ctx: click.Context) -> Path | None:
74
73
 
75
74
 
76
75
  class DocsGroup(TyperGroup):
77
- def list_commands(self, ctx: click.Context) -> List[str]:
78
- names: List[str] = list(super().list_commands(ctx) or [])
76
+ def list_commands(self, ctx: click.Context) -> list[str]:
77
+ names: list[str] = list(super().list_commands(ctx) or [])
79
78
  dir_to_use = _resolve_docs_dir(ctx)
80
79
  fs = _discover_fs_topics(dir_to_use) if dir_to_use else {}
81
80
  pkg = _discover_pkg_topics()
@@ -122,9 +121,7 @@ def register(app: typer.Typer) -> None:
122
121
  # No group-level options; dynamic commands and 'show' handle topics.
123
122
  return None
124
123
 
125
- @docs_app.command(
126
- "show", help="Show docs for a topic (alternative to dynamic subcommand)"
127
- )
124
+ @docs_app.command("show", help="Show docs for a topic (alternative to dynamic subcommand)")
128
125
  def show(topic: str) -> None:
129
126
  key = _norm(topic)
130
127
  ctx = click.get_current_context()
@@ -18,7 +18,7 @@ app = typer.Typer(no_args_is_help=True, add_completion=False)
18
18
  def cmd_openapi(path: str = typer.Argument(..., help="Path to OpenAPI JSON")):
19
19
  try:
20
20
  check_openapi_problem_schema(path=path)
21
- except Exception as e: # noqa: BLE001
21
+ except Exception as e:
22
22
  typer.secho(f"OpenAPI check failed: {e}", fg=typer.colors.RED, err=True)
23
23
  raise typer.Exit(2)
24
24
  typer.secho("OpenAPI checks passed", fg=typer.colors.GREEN)
@@ -28,7 +28,7 @@ def cmd_openapi(path: str = typer.Argument(..., help="Path to OpenAPI JSON")):
28
28
  def cmd_migrations(project_root: str = typer.Option(".", help="Project root")):
29
29
  try:
30
30
  check_migrations_up_to_date(project_root=project_root)
31
- except Exception as e: # noqa: BLE001
31
+ except Exception as e:
32
32
  typer.secho(f"Migrations check failed: {e}", fg=typer.colors.RED, err=True)
33
33
  raise typer.Exit(2)
34
34
  typer.secho("Migrations checks passed", fg=typer.colors.GREEN)
@@ -37,9 +37,7 @@ def cmd_migrations(project_root: str = typer.Option(".", help="Project root")):
37
37
  @app.command("changelog")
38
38
  def cmd_changelog(
39
39
  version: str = typer.Argument(..., help="Version (e.g., 0.1.604)"),
40
- commits_file: str = typer.Option(
41
- None, help="Path to JSON lines of commits (sha,subject)"
42
- ),
40
+ commits_file: str = typer.Option(None, help="Path to JSON lines of commits (sha,subject)"),
43
41
  ):
44
42
  """Generate a changelog section from commit messages.
45
43
 
@@ -56,9 +54,7 @@ def cmd_changelog(
56
54
  )
57
55
  raise typer.Exit(2)
58
56
  rows = [
59
- json.loads(line)
60
- for line in Path(commits_file).read_text().splitlines()
61
- if line.strip()
57
+ json.loads(line) for line in Path(commits_file).read_text().splitlines() if line.strip()
62
58
  ]
63
59
  commits = [Commit(sha=r["sha"], subject=r["subject"]) for r in rows]
64
60
  out = generate_release_section(version=version, commits=commits)
@@ -67,9 +63,7 @@ def cmd_changelog(
67
63
 
68
64
  @app.command("ci")
69
65
  def cmd_ci(
70
- run: bool = typer.Option(
71
- False, help="Execute the steps; default just prints a plan"
72
- ),
66
+ run: bool = typer.Option(False, help="Execute the steps; default just prints a plan"),
73
67
  openapi: str | None = typer.Option(None, help="Path to OpenAPI JSON to lint"),
74
68
  project_root: str = typer.Option(".", help="Project root for migrations check"),
75
69
  ):