svc-infra 0.1.595__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/__init__.py +58 -2
- svc_infra/apf_payments/models.py +68 -38
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +39 -23
- svc_infra/apf_payments/provider/base.py +8 -3
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +74 -52
- svc_infra/apf_payments/schemas.py +84 -83
- svc_infra/apf_payments/service.py +27 -16
- svc_infra/apf_payments/settings.py +12 -11
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +34 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +240 -0
- svc_infra/api/fastapi/apf_payments/router.py +94 -73
- svc_infra/api/fastapi/apf_payments/setup.py +10 -9
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +1 -3
- svc_infra/api/fastapi/auth/add.py +14 -15
- svc_infra/api/fastapi/auth/gaurd.py +32 -20
- svc_infra/api/fastapi/auth/mfa/models.py +3 -4
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +13 -9
- svc_infra/api/fastapi/auth/mfa/router.py +9 -8
- svc_infra/api/fastapi/auth/mfa/security.py +4 -7
- svc_infra/api/fastapi/auth/mfa/utils.py +5 -3
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -3
- svc_infra/api/fastapi/auth/routers/apikey_router.py +19 -21
- svc_infra/api/fastapi/auth/routers/oauth_router.py +98 -52
- svc_infra/api/fastapi/auth/routers/session_router.py +6 -5
- svc_infra/api/fastapi/auth/security.py +25 -15
- svc_infra/api/fastapi/auth/sender.py +5 -0
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +5 -4
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +71 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/__init__.py +5 -1
- svc_infra/api/fastapi/db/http.py +10 -9
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +35 -30
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +39 -21
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +62 -25
- svc_infra/api/fastapi/db/sql/crud_router.py +205 -30
- svc_infra/api/fastapi/db/sql/session.py +19 -2
- svc_infra/api/fastapi/db/sql/users.py +18 -9
- svc_infra/api/fastapi/dependencies/ratelimit.py +76 -14
- svc_infra/api/fastapi/docs/add.py +163 -0
- svc_infra/api/fastapi/docs/landing.py +6 -6
- svc_infra/api/fastapi/docs/scoped.py +75 -36
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +2 -2
- svc_infra/api/fastapi/dual/protected.py +123 -10
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +18 -8
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +59 -7
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +2 -2
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/exceptions.py +2 -5
- svc_infra/api/fastapi/middleware/errors/handlers.py +50 -10
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +95 -0
- svc_infra/api/fastapi/middleware/idempotency.py +190 -68
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +39 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
- svc_infra/api/fastapi/middleware/ratelimit_store.py +45 -13
- svc_infra/api/fastapi/middleware/request_id.py +24 -10
- svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
- svc_infra/api/fastapi/middleware/timeout.py +176 -0
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +4 -3
- svc_infra/api/fastapi/openapi/conventions.py +13 -6
- svc_infra/api/fastapi/openapi/mutators.py +144 -17
- 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 -1
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +47 -32
- svc_infra/api/fastapi/routers/__init__.py +16 -10
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +167 -54
- svc_infra/api/fastapi/tenancy/add.py +20 -0
- svc_infra/api/fastapi/tenancy/context.py +113 -0
- svc_infra/api/fastapi/versioned.py +102 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +70 -4
- svc_infra/app/logging/add.py +10 -2
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +13 -5
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +40 -0
- svc_infra/billing/async_service.py +167 -0
- svc_infra/billing/jobs.py +231 -0
- svc_infra/billing/models.py +146 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +34 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +21 -5
- svc_infra/cache/add.py +167 -0
- svc_infra/cache/backend.py +9 -7
- svc_infra/cache/decorators.py +75 -20
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +26 -6
- svc_infra/cache/recache.py +26 -27
- svc_infra/cache/resources.py +6 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +4 -3
- svc_infra/cli/__init__.py +44 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +39 -2
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +18 -14
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +9 -10
- svc_infra/cli/cmds/db/ops_cmds.py +267 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +97 -29
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +13 -13
- svc_infra/cli/cmds/docs/docs_cmds.py +139 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +110 -0
- svc_infra/cli/cmds/health/__init__.py +179 -0
- svc_infra/cli/cmds/health/health_cmds.py +8 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +42 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +31 -13
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/cli/foundation/runner.py +4 -5
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +56 -0
- svc_infra/data/erasure.py +46 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +56 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +14 -13
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +2 -0
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +19 -5
- svc_infra/db/nosql/indexes.py +12 -9
- svc_infra/db/nosql/management.py +4 -4
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +21 -4
- svc_infra/db/nosql/mongo/settings.py +1 -1
- svc_infra/db/nosql/repository.py +46 -27
- svc_infra/db/nosql/resource.py +28 -16
- svc_infra/db/nosql/scaffold.py +14 -12
- svc_infra/db/nosql/service.py +2 -1
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +380 -0
- svc_infra/db/outbox.py +105 -0
- svc_infra/db/sql/apikey.py +34 -15
- svc_infra/db/sql/authref.py +8 -6
- svc_infra/db/sql/constants.py +5 -1
- svc_infra/db/sql/core.py +13 -13
- svc_infra/db/sql/management.py +5 -6
- svc_infra/db/sql/repository.py +92 -26
- svc_infra/db/sql/resource.py +18 -12
- svc_infra/db/sql/scaffold.py +11 -11
- svc_infra/db/sql/service.py +2 -1
- svc_infra/db/sql/service_with_hooks.py +4 -3
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
- svc_infra/db/sql/tenant.py +80 -0
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +12 -11
- svc_infra/db/sql/utils.py +105 -47
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +531 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +263 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +262 -0
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +68 -0
- svc_infra/exceptions.py +141 -0
- svc_infra/health/__init__.py +863 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +101 -0
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +38 -0
- svc_infra/jobs/builtins/webhook_delivery.py +93 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +49 -0
- svc_infra/jobs/queue.py +106 -0
- svc_infra/jobs/redis_queue.py +242 -0
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/scheduler.py +53 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/loaders/__init__.py +186 -0
- svc_infra/loaders/base.py +143 -0
- svc_infra/loaders/github.py +309 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +229 -0
- svc_infra/logging/__init__.py +375 -0
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +68 -11
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +6 -7
- svc_infra/obs/metrics/asgi.py +8 -7
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +3 -3
- svc_infra/obs/metrics/sqlalchemy.py +14 -13
- svc_infra/obs/metrics.py +9 -8
- 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 +213 -0
- svc_infra/security/audit.py +97 -18
- svc_infra/security/audit_service.py +10 -9
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +14 -7
- svc_infra/security/jwt_rotation.py +78 -29
- svc_infra/security/lockout.py +23 -16
- svc_infra/security/models.py +77 -44
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +12 -12
- svc_infra/security/passwords.py +3 -3
- svc_infra/security/permissions.py +31 -7
- svc_infra/security/session.py +7 -8
- svc_infra/security/signed_cookies.py +26 -6
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +250 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +331 -0
- svc_infra/storage/backends/memory.py +213 -0
- svc_infra/storage/backends/s3.py +334 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +181 -0
- svc_infra/storage/settings.py +193 -0
- svc_infra/testing/__init__.py +682 -0
- svc_infra/utils.py +170 -5
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +327 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +37 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +69 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +139 -0
- svc_infra/websocket/client.py +283 -0
- svc_infra/websocket/config.py +57 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +343 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-1.1.0.dist-info/LICENSE +21 -0
- svc_infra-1.1.0.dist-info/METADATA +362 -0
- svc_infra-1.1.0.dist-info/RECORD +364 -0
- svc_infra-0.1.595.dist-info/METADATA +0 -80
- svc_infra-0.1.595.dist-info/RECORD +0 -253
- {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
svc_infra/cache/tags.py
CHANGED
|
@@ -28,50 +28,25 @@ async def invalidate_tags(*tags: str) -> int:
|
|
|
28
28
|
if not tags:
|
|
29
29
|
return 0
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Preserve order while de-duplicating.
|
|
32
|
+
tags_to_delete = list(dict.fromkeys(tags))
|
|
32
33
|
|
|
33
|
-
#
|
|
34
|
+
# Cashews supports explicit tag deletion via delete_tags().
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
pass
|
|
36
|
+
if hasattr(_cache, "delete_tags"):
|
|
37
|
+
await _cache.delete_tags(*tags_to_delete)
|
|
38
|
+
return len(tags_to_delete)
|
|
39
39
|
except Exception as e:
|
|
40
|
-
logger.warning(f"
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for method_name in ("delete_tag", "invalidate_tag", "tag_invalidate"):
|
|
54
|
-
if hasattr(_cache, method_name):
|
|
55
|
-
try:
|
|
56
|
-
method = getattr(_cache, method_name)
|
|
57
|
-
result = await method(tag)
|
|
58
|
-
count += int(result) if isinstance(result, int) else 1
|
|
59
|
-
break
|
|
60
|
-
except Exception as e:
|
|
61
|
-
logger.debug(f"Tag method {method_name} failed for tag {tag}: {e}")
|
|
62
|
-
continue
|
|
63
|
-
else:
|
|
64
|
-
# Strategy 4: Pattern matching fallback
|
|
65
|
-
for method_name in ("delete_match", "invalidate_match", "invalidate"):
|
|
66
|
-
if hasattr(_cache, method_name):
|
|
67
|
-
try:
|
|
68
|
-
method = getattr(_cache, method_name)
|
|
69
|
-
pattern = f"*{tag}*"
|
|
70
|
-
result = await method(pattern)
|
|
71
|
-
count += int(result) if isinstance(result, int) else 1
|
|
72
|
-
break
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.debug(f"Pattern method {method_name} failed for tag {tag}: {e}")
|
|
75
|
-
continue
|
|
76
|
-
|
|
77
|
-
return count
|
|
40
|
+
logger.warning(f"Cache tag invalidation failed: {e}")
|
|
41
|
+
|
|
42
|
+
# Fallback: attempt private per-tag deletion when available.
|
|
43
|
+
deleted = 0
|
|
44
|
+
for tag in tags_to_delete:
|
|
45
|
+
try:
|
|
46
|
+
if hasattr(_cache, "_delete_tag"):
|
|
47
|
+
await _cache._delete_tag(tag)
|
|
48
|
+
deleted += 1
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.debug(f"Tag deletion failed for tag {tag}: {e}")
|
|
51
|
+
|
|
52
|
+
return deleted
|
svc_infra/cache/ttl.py
CHANGED
|
@@ -6,7 +6,6 @@ via environment variables with sensible defaults.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def _get_env_int(key: str, default: int) -> int:
|
|
@@ -36,7 +35,7 @@ TTL_SHORT: int = _get_env_int("CACHE_TTL_SHORT", 30) # 30 seconds
|
|
|
36
35
|
TTL_LONG: int = _get_env_int("CACHE_TTL_LONG", 3600) # 1 hour
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def get_ttl(duration_type: str) ->
|
|
38
|
+
def get_ttl(duration_type: str) -> int | None:
|
|
40
39
|
"""
|
|
41
40
|
Get TTL value by duration type name.
|
|
42
41
|
|
|
@@ -60,7 +59,7 @@ def get_ttl(duration_type: str) -> Optional[int]:
|
|
|
60
59
|
return ttl_map.get(duration_type.lower())
|
|
61
60
|
|
|
62
61
|
|
|
63
|
-
def validate_ttl(ttl:
|
|
62
|
+
def validate_ttl(ttl: int | None) -> int:
|
|
64
63
|
"""
|
|
65
64
|
Validate and normalize a TTL value.
|
|
66
65
|
|
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
|
|
|
@@ -42,7 +43,7 @@ def stable_hash(*args: Any, **kwargs: Any) -> str:
|
|
|
42
43
|
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
def join_key(parts: Iterable[
|
|
46
|
+
def join_key(parts: Iterable[str | int | None]) -> str:
|
|
46
47
|
"""
|
|
47
48
|
Join key parts into a cache key, filtering out empty values.
|
|
48
49
|
|
|
@@ -98,7 +99,7 @@ def format_tuple_key(key_tuple: tuple[str, ...], **kwargs) -> str:
|
|
|
98
99
|
raise ValueError(f"Failed to format key template: {e}") from e
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
def normalize_cache_key(key:
|
|
102
|
+
def normalize_cache_key(key: str | tuple[str, ...], **kwargs) -> str:
|
|
102
103
|
"""
|
|
103
104
|
Normalize a cache key from various input formats.
|
|
104
105
|
|
svc_infra/cli/__init__.py
CHANGED
|
@@ -4,10 +4,17 @@ import typer
|
|
|
4
4
|
|
|
5
5
|
from svc_infra.cli.cmds import (
|
|
6
6
|
_HELP,
|
|
7
|
+
jobs_app,
|
|
7
8
|
register_alembic,
|
|
9
|
+
register_db_ops,
|
|
10
|
+
register_docs,
|
|
11
|
+
register_dx,
|
|
12
|
+
register_health,
|
|
8
13
|
register_mongo,
|
|
9
14
|
register_mongo_scaffold,
|
|
10
15
|
register_obs,
|
|
16
|
+
register_sdk,
|
|
17
|
+
register_sql_export,
|
|
11
18
|
register_sql_scaffold,
|
|
12
19
|
)
|
|
13
20
|
from svc_infra.cli.foundation.typer_bootstrap import pre_cli
|
|
@@ -15,16 +22,45 @@ from svc_infra.cli.foundation.typer_bootstrap import pre_cli
|
|
|
15
22
|
app = typer.Typer(no_args_is_help=True, add_completion=False, help=_HELP)
|
|
16
23
|
pre_cli(app)
|
|
17
24
|
|
|
18
|
-
# ---
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# --- db ops group ---
|
|
26
|
+
db_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Database operations")
|
|
27
|
+
register_db_ops(db_app)
|
|
28
|
+
app.add_typer(db_app, name="db")
|
|
21
29
|
|
|
22
|
-
# ---
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
# --- sql group ---
|
|
31
|
+
sql_app = typer.Typer(no_args_is_help=True, add_completion=False, help="SQL commands")
|
|
32
|
+
register_alembic(sql_app)
|
|
33
|
+
register_sql_scaffold(sql_app)
|
|
34
|
+
register_sql_export(sql_app)
|
|
35
|
+
app.add_typer(sql_app, name="sql")
|
|
25
36
|
|
|
26
|
-
#
|
|
27
|
-
|
|
37
|
+
# --- mongo group ---
|
|
38
|
+
mongo_app = typer.Typer(no_args_is_help=True, add_completion=False, help="MongoDB commands")
|
|
39
|
+
register_mongo(mongo_app)
|
|
40
|
+
register_mongo_scaffold(mongo_app)
|
|
41
|
+
app.add_typer(mongo_app, name="mongo")
|
|
42
|
+
|
|
43
|
+
# --- health group ---
|
|
44
|
+
health_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Health checks")
|
|
45
|
+
register_health(health_app)
|
|
46
|
+
app.add_typer(health_app, name="health")
|
|
47
|
+
|
|
48
|
+
# -- obs group ---
|
|
49
|
+
obs_app = typer.Typer(no_args_is_help=True, add_completion=False, help="Observability commands")
|
|
50
|
+
register_obs(obs_app)
|
|
51
|
+
app.add_typer(obs_app, name="obs")
|
|
52
|
+
|
|
53
|
+
# -- dx commands ---
|
|
54
|
+
register_dx(app)
|
|
55
|
+
|
|
56
|
+
# -- jobs commands ---
|
|
57
|
+
app.add_typer(jobs_app, name="jobs")
|
|
58
|
+
|
|
59
|
+
# -- sdk commands ---
|
|
60
|
+
register_sdk(app)
|
|
61
|
+
|
|
62
|
+
# -- docs commands ---
|
|
63
|
+
register_docs(app)
|
|
28
64
|
|
|
29
65
|
|
|
30
66
|
def main():
|
svc_infra/cli/cmds/__init__.py
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from svc_infra.cli.cmds.db.nosql.mongo.mongo_cmds import register as register_mongo
|
|
9
|
+
except ModuleNotFoundError as exc:
|
|
10
|
+
_mongo_import_error = exc
|
|
11
|
+
|
|
12
|
+
def register_mongo(app: typer.Typer) -> None: # type: ignore[no-redef]
|
|
13
|
+
def _unavailable() -> Any:
|
|
14
|
+
raise ModuleNotFoundError(
|
|
15
|
+
"MongoDB CLI commands require optional dependencies. Install pymongo and motor "
|
|
16
|
+
"to enable `svc-infra mongo ...` commands."
|
|
17
|
+
) from _mongo_import_error
|
|
18
|
+
|
|
19
|
+
# Provide a single helpful command instead of failing CLI import.
|
|
20
|
+
app.command("unavailable")(_unavailable)
|
|
21
|
+
|
|
22
|
+
|
|
2
23
|
from svc_infra.cli.cmds.db.nosql.mongo.mongo_scaffold_cmds import (
|
|
3
24
|
register as register_mongo_scaffold,
|
|
4
25
|
)
|
|
26
|
+
from svc_infra.cli.cmds.db.ops_cmds import register as register_db_ops
|
|
5
27
|
from svc_infra.cli.cmds.db.sql.alembic_cmds import register as register_alembic
|
|
6
|
-
from svc_infra.cli.cmds.db.sql.
|
|
28
|
+
from svc_infra.cli.cmds.db.sql.sql_export_cmds import register as register_sql_export
|
|
29
|
+
from svc_infra.cli.cmds.db.sql.sql_scaffold_cmds import (
|
|
30
|
+
register as register_sql_scaffold,
|
|
31
|
+
)
|
|
32
|
+
from svc_infra.cli.cmds.docs.docs_cmds import register as register_docs
|
|
33
|
+
from svc_infra.cli.cmds.dx import register_dx
|
|
34
|
+
from svc_infra.cli.cmds.health.health_cmds import register as register_health
|
|
35
|
+
from svc_infra.cli.cmds.jobs.jobs_cmds import app as jobs_app
|
|
7
36
|
from svc_infra.cli.cmds.obs.obs_cmds import register as register_obs
|
|
37
|
+
from svc_infra.cli.cmds.sdk.sdk_cmds import register as register_sdk
|
|
8
38
|
|
|
9
39
|
from .help import _HELP
|
|
10
40
|
|
|
11
41
|
__all__ = [
|
|
12
42
|
"register_alembic",
|
|
13
43
|
"register_sql_scaffold",
|
|
44
|
+
"register_sql_export",
|
|
14
45
|
"register_mongo",
|
|
15
46
|
"register_mongo_scaffold",
|
|
47
|
+
"register_db_ops",
|
|
16
48
|
"register_obs",
|
|
49
|
+
"jobs_app",
|
|
50
|
+
"register_sdk",
|
|
51
|
+
"register_dx",
|
|
52
|
+
"register_docs",
|
|
53
|
+
"register_health",
|
|
17
54
|
"_HELP",
|
|
18
55
|
]
|
|
@@ -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
|
):
|
|
@@ -172,7 +173,9 @@ def cmd_ping(
|
|
|
172
173
|
|
|
173
174
|
import asyncio
|
|
174
175
|
|
|
175
|
-
from svc_infra.db.nosql.mongo.client import
|
|
176
|
+
from svc_infra.db.nosql.mongo.client import (
|
|
177
|
+
acquire_db,
|
|
178
|
+
) # local import to avoid side effects
|
|
176
179
|
|
|
177
180
|
async def _run():
|
|
178
181
|
await init_mongo()
|
|
@@ -188,6 +191,7 @@ def cmd_ping(
|
|
|
188
191
|
|
|
189
192
|
|
|
190
193
|
def register(app: typer.Typer) -> None:
|
|
191
|
-
app
|
|
192
|
-
app.command("
|
|
193
|
-
app.command("
|
|
194
|
+
# Attach to 'mongo' group app
|
|
195
|
+
app.command("prepare")(cmd_prepare)
|
|
196
|
+
app.command("setup-and-prepare")(cmd_setup_and_prepare)
|
|
197
|
+
app.command("ping")(cmd_ping)
|
|
@@ -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
|
|
|
@@ -25,10 +24,10 @@ def cmd_scaffold(
|
|
|
25
24
|
"--same-dir/--no-same-dir",
|
|
26
25
|
help="Put documents & schemas into the same directory.",
|
|
27
26
|
),
|
|
28
|
-
documents_filename:
|
|
27
|
+
documents_filename: str | None = typer.Option(
|
|
29
28
|
None, help="Custom filename for documents (separate-dir mode)."
|
|
30
29
|
),
|
|
31
|
-
schemas_filename:
|
|
30
|
+
schemas_filename: str | None = typer.Option(
|
|
32
31
|
None, help="Custom filename for schemas (separate-dir mode)."
|
|
33
32
|
),
|
|
34
33
|
):
|
|
@@ -53,7 +52,7 @@ def cmd_scaffold_documents(
|
|
|
53
52
|
dest_dir: Path = typer.Option(..., "--dest-dir", resolve_path=True),
|
|
54
53
|
entity_name: str = typer.Option("Item", "--entity-name"),
|
|
55
54
|
overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
|
|
56
|
-
documents_filename:
|
|
55
|
+
documents_filename: str | None = typer.Option(
|
|
57
56
|
None,
|
|
58
57
|
"--documents-filename",
|
|
59
58
|
help="Filename to write (e.g. product_doc.py). Defaults to <snake(entity)>.py",
|
|
@@ -73,7 +72,7 @@ def cmd_scaffold_schemas(
|
|
|
73
72
|
dest_dir: Path = typer.Option(..., "--dest-dir", resolve_path=True),
|
|
74
73
|
entity_name: str = typer.Option("Item", "--entity-name"),
|
|
75
74
|
overwrite: bool = typer.Option(False, "--overwrite/--no-overwrite"),
|
|
76
|
-
schemas_filename:
|
|
75
|
+
schemas_filename: str | None = typer.Option(
|
|
77
76
|
None,
|
|
78
77
|
"--schemas-filename",
|
|
79
78
|
help="Filename to write (e.g. product_schemas.py). Defaults to <snake(entity)>.py",
|
|
@@ -96,7 +95,7 @@ def cmd_scaffold_resources(
|
|
|
96
95
|
"--entity-name",
|
|
97
96
|
help="Used only to prefill example placeholders.",
|
|
98
97
|
),
|
|
99
|
-
filename:
|
|
98
|
+
filename: str | None = typer.Option(
|
|
100
99
|
None,
|
|
101
100
|
"--filename",
|
|
102
101
|
help='Output filename (default: "resources.py")',
|
|
@@ -127,7 +126,7 @@ def register(app: typer.Typer) -> None:
|
|
|
127
126
|
• mongo-scaffold-schemas
|
|
128
127
|
• mongo-scaffold-resources
|
|
129
128
|
"""
|
|
130
|
-
app.command("
|
|
131
|
-
app.command("
|
|
132
|
-
app.command("
|
|
133
|
-
app.command("
|
|
129
|
+
app.command("scaffold")(cmd_scaffold)
|
|
130
|
+
app.command("scaffold-documents")(cmd_scaffold_documents)
|
|
131
|
+
app.command("scaffold-schemas")(cmd_scaffold_schemas)
|
|
132
|
+
app.command("scaffold-resources")(cmd_scaffold_resources)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Database operations CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides CLI commands for database administration:
|
|
4
|
+
- wait: Wait for database to be ready before proceeding
|
|
5
|
+
- kill-queries: Terminate queries blocking a specific table
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_wait(
|
|
18
|
+
database_url: str | None = typer.Option(
|
|
19
|
+
None,
|
|
20
|
+
"--url",
|
|
21
|
+
"-u",
|
|
22
|
+
help="Database URL; overrides env SQL_URL.",
|
|
23
|
+
),
|
|
24
|
+
timeout: int = typer.Option(
|
|
25
|
+
60,
|
|
26
|
+
"--timeout",
|
|
27
|
+
"-t",
|
|
28
|
+
help="Maximum time to wait in seconds.",
|
|
29
|
+
),
|
|
30
|
+
interval: float = typer.Option(
|
|
31
|
+
2.0,
|
|
32
|
+
"--interval",
|
|
33
|
+
"-i",
|
|
34
|
+
help="Time between connection attempts in seconds.",
|
|
35
|
+
),
|
|
36
|
+
quiet: bool = typer.Option(
|
|
37
|
+
False,
|
|
38
|
+
"--quiet",
|
|
39
|
+
"-q",
|
|
40
|
+
help="Suppress progress messages.",
|
|
41
|
+
),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Wait for database to be ready.
|
|
45
|
+
|
|
46
|
+
Attempts to connect to the database repeatedly until successful
|
|
47
|
+
or timeout is reached. Useful in container startup scripts.
|
|
48
|
+
|
|
49
|
+
Exit codes:
|
|
50
|
+
0: Database is ready
|
|
51
|
+
1: Timeout reached, database not ready
|
|
52
|
+
"""
|
|
53
|
+
url = database_url or os.getenv("SQL_URL") or os.getenv("DATABASE_URL")
|
|
54
|
+
if not url:
|
|
55
|
+
typer.secho(
|
|
56
|
+
"ERROR: No database URL. Set --url, SQL_URL, or DATABASE_URL.",
|
|
57
|
+
fg=typer.colors.RED,
|
|
58
|
+
)
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
async def _wait() -> bool:
|
|
62
|
+
"""Async wait loop."""
|
|
63
|
+
from svc_infra.health import check_database
|
|
64
|
+
|
|
65
|
+
check = check_database(url)
|
|
66
|
+
deadline = time.monotonic() + timeout
|
|
67
|
+
attempt = 0
|
|
68
|
+
|
|
69
|
+
while time.monotonic() < deadline:
|
|
70
|
+
attempt += 1
|
|
71
|
+
if not quiet:
|
|
72
|
+
typer.echo(f"Attempt {attempt}: Connecting to database...")
|
|
73
|
+
|
|
74
|
+
result = await check()
|
|
75
|
+
|
|
76
|
+
if result.status == "healthy":
|
|
77
|
+
if not quiet:
|
|
78
|
+
typer.secho(
|
|
79
|
+
f"✓ Database ready ({result.latency_ms:.1f}ms)",
|
|
80
|
+
fg=typer.colors.GREEN,
|
|
81
|
+
)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
if not quiet:
|
|
85
|
+
msg = result.message or "Connection failed"
|
|
86
|
+
typer.echo(f" → {msg}")
|
|
87
|
+
|
|
88
|
+
remaining = deadline - time.monotonic()
|
|
89
|
+
if remaining > 0:
|
|
90
|
+
await asyncio.sleep(min(interval, remaining))
|
|
91
|
+
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
success = asyncio.run(_wait())
|
|
95
|
+
if not success:
|
|
96
|
+
typer.secho(
|
|
97
|
+
f"ERROR: Database not ready after {timeout}s",
|
|
98
|
+
fg=typer.colors.RED,
|
|
99
|
+
)
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_kill_queries(
|
|
104
|
+
table: str = typer.Argument(
|
|
105
|
+
...,
|
|
106
|
+
help="Table name to find blocking queries for.",
|
|
107
|
+
),
|
|
108
|
+
database_url: str | None = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
"--url",
|
|
111
|
+
"-u",
|
|
112
|
+
help="Database URL; overrides env SQL_URL.",
|
|
113
|
+
),
|
|
114
|
+
dry_run: bool = typer.Option(
|
|
115
|
+
False,
|
|
116
|
+
"--dry-run",
|
|
117
|
+
"-n",
|
|
118
|
+
help="Show queries that would be killed without actually killing them.",
|
|
119
|
+
),
|
|
120
|
+
force: bool = typer.Option(
|
|
121
|
+
False,
|
|
122
|
+
"--force",
|
|
123
|
+
"-f",
|
|
124
|
+
help="Terminate immediately (pg_terminate_backend) instead of cancel (pg_cancel_backend).",
|
|
125
|
+
),
|
|
126
|
+
quiet: bool = typer.Option(
|
|
127
|
+
False,
|
|
128
|
+
"--quiet",
|
|
129
|
+
"-q",
|
|
130
|
+
help="Suppress output except errors.",
|
|
131
|
+
),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Kill queries blocking operations on a table.
|
|
135
|
+
|
|
136
|
+
Finds queries that hold locks on the specified table and attempts
|
|
137
|
+
to cancel or terminate them. Useful when migrations are blocked.
|
|
138
|
+
|
|
139
|
+
By default uses pg_cancel_backend (graceful). Use --force for
|
|
140
|
+
pg_terminate_backend (immediate termination).
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
svc-infra db kill-queries users
|
|
144
|
+
svc-infra db kill-queries users --dry-run
|
|
145
|
+
svc-infra db kill-queries users --force
|
|
146
|
+
"""
|
|
147
|
+
url = database_url or os.getenv("SQL_URL") or os.getenv("DATABASE_URL")
|
|
148
|
+
if not url:
|
|
149
|
+
typer.secho(
|
|
150
|
+
"ERROR: No database URL. Set --url, SQL_URL, or DATABASE_URL.",
|
|
151
|
+
fg=typer.colors.RED,
|
|
152
|
+
)
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
async def _kill_queries() -> int:
|
|
156
|
+
"""Find and kill blocking queries. Returns count of killed queries."""
|
|
157
|
+
try:
|
|
158
|
+
import asyncpg
|
|
159
|
+
except ImportError:
|
|
160
|
+
typer.secho(
|
|
161
|
+
"ERROR: asyncpg not installed. Run: pip install asyncpg",
|
|
162
|
+
fg=typer.colors.RED,
|
|
163
|
+
)
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
|
|
166
|
+
# Normalize URL for asyncpg
|
|
167
|
+
db_url = url
|
|
168
|
+
if db_url.startswith("postgres://"):
|
|
169
|
+
db_url = db_url.replace("postgres://", "postgresql://", 1)
|
|
170
|
+
if "+asyncpg" in db_url:
|
|
171
|
+
db_url = db_url.replace("+asyncpg", "")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
conn = await asyncpg.connect(db_url)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
typer.secho(
|
|
177
|
+
f"ERROR: Failed to connect to database: {e}",
|
|
178
|
+
fg=typer.colors.RED,
|
|
179
|
+
)
|
|
180
|
+
raise typer.Exit(1)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Find queries with locks on the table
|
|
184
|
+
# Uses pg_stat_activity joined with pg_locks to find blocking queries
|
|
185
|
+
find_query = """
|
|
186
|
+
SELECT DISTINCT
|
|
187
|
+
a.pid,
|
|
188
|
+
a.usename,
|
|
189
|
+
a.application_name,
|
|
190
|
+
a.state,
|
|
191
|
+
a.query,
|
|
192
|
+
a.query_start,
|
|
193
|
+
l.locktype,
|
|
194
|
+
l.mode
|
|
195
|
+
FROM pg_stat_activity a
|
|
196
|
+
JOIN pg_locks l ON a.pid = l.pid
|
|
197
|
+
WHERE l.relation = $1::regclass
|
|
198
|
+
AND a.pid != pg_backend_pid()
|
|
199
|
+
ORDER BY a.query_start
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
rows = await conn.fetch(find_query, table)
|
|
204
|
+
except asyncpg.UndefinedTableError:
|
|
205
|
+
typer.secho(
|
|
206
|
+
f"ERROR: Table '{table}' does not exist",
|
|
207
|
+
fg=typer.colors.RED,
|
|
208
|
+
)
|
|
209
|
+
raise typer.Exit(1)
|
|
210
|
+
|
|
211
|
+
if not rows:
|
|
212
|
+
if not quiet:
|
|
213
|
+
typer.echo(f"No active queries found on table '{table}'")
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
if not quiet:
|
|
217
|
+
typer.echo(f"Found {len(rows)} query(ies) with locks on '{table}':\n")
|
|
218
|
+
for row in rows:
|
|
219
|
+
query_preview = (row["query"] or "")[:80].replace("\n", " ")
|
|
220
|
+
if len(row["query"] or "") > 80:
|
|
221
|
+
query_preview += "..."
|
|
222
|
+
typer.echo(f" PID {row['pid']}: {query_preview}")
|
|
223
|
+
typer.echo(f" User: {row['usename']}, State: {row['state']}")
|
|
224
|
+
typer.echo(f" Lock: {row['mode']} on {row['locktype']}")
|
|
225
|
+
typer.echo("")
|
|
226
|
+
|
|
227
|
+
if dry_run:
|
|
228
|
+
typer.echo("Dry run - no queries killed.")
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
# Kill the queries
|
|
232
|
+
kill_fn = "pg_terminate_backend" if force else "pg_cancel_backend"
|
|
233
|
+
killed = 0
|
|
234
|
+
|
|
235
|
+
for row in rows:
|
|
236
|
+
pid = row["pid"]
|
|
237
|
+
try:
|
|
238
|
+
result = await conn.fetchval(f"SELECT {kill_fn}($1)", pid)
|
|
239
|
+
if result:
|
|
240
|
+
killed += 1
|
|
241
|
+
if not quiet:
|
|
242
|
+
action = "Terminated" if force else "Cancelled"
|
|
243
|
+
typer.secho(f" {action} PID {pid}", fg=typer.colors.GREEN)
|
|
244
|
+
else:
|
|
245
|
+
if not quiet:
|
|
246
|
+
typer.echo(f" PID {pid}: already finished or permission denied")
|
|
247
|
+
except Exception as e:
|
|
248
|
+
if not quiet:
|
|
249
|
+
typer.secho(f" PID {pid}: Error - {e}", fg=typer.colors.YELLOW)
|
|
250
|
+
|
|
251
|
+
if not quiet:
|
|
252
|
+
typer.echo(f"\n{killed}/{len(rows)} queries killed.")
|
|
253
|
+
return killed
|
|
254
|
+
|
|
255
|
+
finally:
|
|
256
|
+
await conn.close()
|
|
257
|
+
|
|
258
|
+
count = asyncio.run(_kill_queries())
|
|
259
|
+
if count == 0 and not dry_run:
|
|
260
|
+
# Exit with 0 even if no queries found - that's success
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def register(app: typer.Typer) -> None:
|
|
265
|
+
"""Register database operations commands with the CLI app."""
|
|
266
|
+
app.command("wait")(cmd_wait)
|
|
267
|
+
app.command("kill-queries")(cmd_kill_queries)
|