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.
- svc_infra/apf_payments/models.py +47 -108
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +42 -100
- svc_infra/apf_payments/provider/base.py +10 -26
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +63 -135
- svc_infra/apf_payments/schemas.py +82 -90
- svc_infra/apf_payments/service.py +40 -86
- svc_infra/apf_payments/settings.py +10 -13
- svc_infra/api/__init__.py +13 -13
- svc_infra/api/fastapi/__init__.py +19 -0
- svc_infra/api/fastapi/admin/add.py +13 -18
- svc_infra/api/fastapi/apf_payments/router.py +47 -84
- svc_infra/api/fastapi/apf_payments/setup.py +7 -13
- svc_infra/api/fastapi/auth/__init__.py +1 -1
- svc_infra/api/fastapi/auth/_cookies.py +3 -9
- svc_infra/api/fastapi/auth/add.py +4 -8
- svc_infra/api/fastapi/auth/gaurd.py +9 -26
- svc_infra/api/fastapi/auth/mfa/models.py +4 -7
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +3 -3
- svc_infra/api/fastapi/auth/mfa/router.py +9 -15
- svc_infra/api/fastapi/auth/mfa/security.py +3 -5
- svc_infra/api/fastapi/auth/mfa/utils.py +3 -2
- svc_infra/api/fastapi/auth/mfa/verify.py +2 -9
- svc_infra/api/fastapi/auth/providers.py +4 -6
- svc_infra/api/fastapi/auth/routers/apikey_router.py +16 -18
- svc_infra/api/fastapi/auth/routers/oauth_router.py +37 -85
- svc_infra/api/fastapi/auth/routers/session_router.py +3 -6
- svc_infra/api/fastapi/auth/security.py +17 -28
- svc_infra/api/fastapi/auth/sender.py +1 -3
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +6 -7
- svc_infra/api/fastapi/auth/ws_security.py +2 -2
- svc_infra/api/fastapi/billing/router.py +6 -8
- svc_infra/api/fastapi/db/http.py +10 -11
- svc_infra/api/fastapi/db/nosql/mongo/add.py +5 -15
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +14 -15
- svc_infra/api/fastapi/db/sql/add.py +6 -14
- svc_infra/api/fastapi/db/sql/crud_router.py +27 -40
- svc_infra/api/fastapi/db/sql/health.py +1 -3
- svc_infra/api/fastapi/db/sql/session.py +4 -5
- svc_infra/api/fastapi/db/sql/users.py +8 -11
- svc_infra/api/fastapi/dependencies/ratelimit.py +4 -6
- svc_infra/api/fastapi/docs/add.py +13 -23
- svc_infra/api/fastapi/docs/landing.py +6 -8
- svc_infra/api/fastapi/docs/scoped.py +34 -42
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +12 -21
- svc_infra/api/fastapi/dual/router.py +14 -31
- svc_infra/api/fastapi/ease.py +57 -13
- svc_infra/api/fastapi/http/conditional.py +3 -5
- svc_infra/api/fastapi/middleware/errors/catchall.py +2 -6
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -4
- svc_infra/api/fastapi/middleware/errors/handlers.py +12 -18
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +4 -13
- svc_infra/api/fastapi/middleware/idempotency.py +11 -16
- svc_infra/api/fastapi/middleware/idempotency_store.py +14 -14
- svc_infra/api/fastapi/middleware/optimistic_lock.py +5 -8
- svc_infra/api/fastapi/middleware/ratelimit.py +8 -8
- svc_infra/api/fastapi/middleware/ratelimit_store.py +7 -8
- svc_infra/api/fastapi/middleware/request_id.py +1 -3
- svc_infra/api/fastapi/middleware/timeout.py +9 -10
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -6
- svc_infra/api/fastapi/openapi/conventions.py +4 -4
- svc_infra/api/fastapi/openapi/mutators.py +13 -31
- svc_infra/api/fastapi/openapi/pipeline.py +2 -2
- svc_infra/api/fastapi/openapi/responses.py +4 -6
- svc_infra/api/fastapi/openapi/security.py +1 -3
- svc_infra/api/fastapi/ops/add.py +7 -9
- svc_infra/api/fastapi/pagination.py +25 -37
- svc_infra/api/fastapi/routers/__init__.py +16 -38
- svc_infra/api/fastapi/setup.py +13 -31
- svc_infra/api/fastapi/tenancy/add.py +3 -2
- svc_infra/api/fastapi/tenancy/context.py +8 -7
- svc_infra/api/fastapi/versioned.py +3 -2
- svc_infra/app/env.py +5 -7
- svc_infra/app/logging/add.py +2 -1
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +3 -2
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +19 -2
- svc_infra/billing/async_service.py +27 -7
- svc_infra/billing/jobs.py +23 -33
- svc_infra/billing/models.py +21 -52
- svc_infra/billing/quotas.py +5 -7
- svc_infra/billing/schemas.py +4 -6
- svc_infra/cache/__init__.py +12 -5
- svc_infra/cache/add.py +6 -9
- svc_infra/cache/backend.py +6 -5
- svc_infra/cache/decorators.py +17 -28
- svc_infra/cache/keys.py +2 -2
- svc_infra/cache/recache.py +22 -35
- svc_infra/cache/resources.py +8 -16
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +5 -6
- svc_infra/cli/__init__.py +4 -12
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +11 -10
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +6 -9
- svc_infra/cli/cmds/db/ops_cmds.py +3 -6
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +24 -41
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +9 -17
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +10 -10
- svc_infra/cli/cmds/docs/docs_cmds.py +7 -10
- svc_infra/cli/cmds/dx/dx_cmds.py +5 -11
- svc_infra/cli/cmds/jobs/jobs_cmds.py +2 -7
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -7
- svc_infra/cli/cmds/sdk/sdk_cmds.py +5 -15
- svc_infra/cli/foundation/runner.py +6 -11
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +5 -5
- svc_infra/data/backup.py +8 -10
- svc_infra/data/erasure.py +3 -2
- svc_infra/data/fixtures.py +3 -3
- svc_infra/data/retention.py +8 -13
- svc_infra/db/crud_schema.py +9 -8
- svc_infra/db/nosql/__init__.py +0 -1
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +7 -14
- svc_infra/db/nosql/indexes.py +11 -10
- svc_infra/db/nosql/management.py +3 -3
- svc_infra/db/nosql/mongo/client.py +3 -3
- svc_infra/db/nosql/mongo/settings.py +2 -6
- svc_infra/db/nosql/repository.py +27 -28
- svc_infra/db/nosql/resource.py +15 -20
- svc_infra/db/nosql/scaffold.py +13 -17
- svc_infra/db/nosql/service.py +3 -4
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/types.py +2 -6
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +14 -18
- svc_infra/db/outbox.py +15 -18
- svc_infra/db/sql/apikey.py +12 -21
- svc_infra/db/sql/authref.py +3 -7
- svc_infra/db/sql/constants.py +9 -9
- svc_infra/db/sql/core.py +11 -11
- svc_infra/db/sql/management.py +2 -6
- svc_infra/db/sql/repository.py +17 -24
- svc_infra/db/sql/resource.py +14 -13
- svc_infra/db/sql/scaffold.py +13 -17
- svc_infra/db/sql/service.py +7 -16
- svc_infra/db/sql/service_with_hooks.py +4 -3
- svc_infra/db/sql/tenant.py +6 -14
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +14 -19
- svc_infra/db/sql/utils.py +24 -53
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +8 -15
- svc_infra/documents/add.py +7 -8
- svc_infra/documents/ease.py +8 -8
- svc_infra/documents/models.py +3 -3
- svc_infra/documents/storage.py +11 -13
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +1 -3
- svc_infra/dx/changelog.py +2 -2
- svc_infra/dx/checks.py +1 -1
- svc_infra/health/__init__.py +15 -16
- svc_infra/http/client.py +10 -14
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +3 -5
- svc_infra/jobs/builtins/webhook_delivery.py +1 -3
- svc_infra/jobs/loader.py +4 -5
- svc_infra/jobs/queue.py +14 -24
- svc_infra/jobs/redis_queue.py +20 -34
- svc_infra/jobs/runner.py +7 -11
- svc_infra/jobs/scheduler.py +5 -5
- svc_infra/jobs/worker.py +1 -1
- svc_infra/loaders/base.py +5 -4
- svc_infra/loaders/github.py +1 -3
- svc_infra/loaders/url.py +3 -9
- svc_infra/logging/__init__.py +7 -6
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +2 -2
- svc_infra/obs/add.py +4 -3
- svc_infra/obs/cloud_dash.py +1 -1
- svc_infra/obs/metrics/__init__.py +3 -3
- svc_infra/obs/metrics/asgi.py +9 -14
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +5 -9
- svc_infra/obs/metrics/sqlalchemy.py +9 -12
- svc_infra/obs/metrics.py +3 -3
- svc_infra/obs/settings.py +2 -6
- svc_infra/resilience/__init__.py +44 -0
- svc_infra/resilience/circuit_breaker.py +328 -0
- svc_infra/resilience/retry.py +289 -0
- svc_infra/security/__init__.py +167 -0
- svc_infra/security/add.py +5 -9
- svc_infra/security/audit.py +14 -17
- svc_infra/security/audit_service.py +9 -9
- svc_infra/security/hibp.py +3 -6
- svc_infra/security/jwt_rotation.py +7 -10
- svc_infra/security/lockout.py +12 -11
- svc_infra/security/models.py +37 -46
- svc_infra/security/oauth_models.py +8 -8
- svc_infra/security/org_invites.py +11 -13
- svc_infra/security/passwords.py +4 -6
- svc_infra/security/permissions.py +8 -7
- svc_infra/security/session.py +6 -7
- svc_infra/security/signed_cookies.py +9 -9
- svc_infra/storage/add.py +5 -8
- svc_infra/storage/backends/local.py +13 -21
- svc_infra/storage/backends/memory.py +4 -7
- svc_infra/storage/backends/s3.py +17 -36
- svc_infra/storage/base.py +2 -2
- svc_infra/storage/easy.py +4 -8
- svc_infra/storage/settings.py +16 -18
- svc_infra/testing/__init__.py +36 -39
- svc_infra/utils.py +169 -8
- svc_infra/webhooks/__init__.py +1 -1
- svc_infra/webhooks/add.py +17 -29
- svc_infra/webhooks/encryption.py +2 -2
- svc_infra/webhooks/fastapi.py +2 -4
- svc_infra/webhooks/router.py +3 -3
- svc_infra/webhooks/service.py +5 -6
- svc_infra/webhooks/signing.py +5 -5
- svc_infra/websocket/add.py +2 -3
- svc_infra/websocket/client.py +3 -2
- svc_infra/websocket/config.py +6 -18
- svc_infra/websocket/manager.py +9 -10
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/METADATA +11 -5
- svc_infra-1.1.0.dist-info/RECORD +364 -0
- svc_infra/billing/service.py +0 -123
- svc_infra-0.1.706.dist-info/RECORD +0 -357
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/LICENSE +0 -0
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
- {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
|
|
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[
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
27
|
+
documents_filename: str | None = typer.Option(
|
|
31
28
|
None, help="Custom filename for documents (separate-dir mode)."
|
|
32
29
|
),
|
|
33
|
-
schemas_filename:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
82
|
-
),
|
|
83
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
157
|
+
database_url: str | None = typer.Option(
|
|
167
158
|
None, help="Database URL; overrides env for this command."
|
|
168
159
|
),
|
|
169
|
-
message:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
25
|
-
),
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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:
|
|
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:
|
|
38
|
+
models_filename: str | None = typer.Option(
|
|
39
39
|
None, help="Custom filename for models (separate-dir mode)."
|
|
40
40
|
),
|
|
41
|
-
schemas_filename:
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
21
|
-
topics:
|
|
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() ->
|
|
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:
|
|
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) ->
|
|
78
|
-
names:
|
|
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()
|
svc_infra/cli/cmds/dx/dx_cmds.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
):
|