svc-infra 0.1.600__py3-none-any.whl → 0.1.640__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/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/db/sql/add.py +32 -13
- svc_infra/api/fastapi/db/sql/crud_router.py +178 -16
- svc_infra/api/fastapi/db/sql/session.py +16 -0
- svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
- svc_infra/api/fastapi/docs/add.py +160 -0
- svc_infra/api/fastapi/docs/landing.py +1 -1
- svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
- svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
- svc_infra/api/fastapi/middleware/timeout.py +148 -0
- svc_infra/api/fastapi/openapi/mutators.py +114 -0
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +3 -1
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +11 -1
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/app/README.md +5 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +230 -0
- svc_infra/billing/models.py +131 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +33 -0
- svc_infra/billing/service.py +115 -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 +4 -0
- svc_infra/cache/add.py +158 -0
- svc_infra/cache/backend.py +5 -2
- svc_infra/cache/decorators.py +19 -1
- svc_infra/cache/keys.py +24 -4
- svc_infra/cli/__init__.py +28 -8
- svc_infra/cli/cmds/__init__.py +8 -0
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +53 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +40 -0
- svc_infra/data/retention.py +55 -0
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/sql/repository.py +51 -11
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +9 -1
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +9 -2
- svc_infra/db/sql/tenant.py +79 -0
- svc_infra/db/sql/utils.py +18 -4
- svc_infra/docs/acceptance-matrix.md +71 -0
- svc_infra/docs/acceptance.md +44 -0
- svc_infra/docs/admin.md +425 -0
- svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
- svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
- svc_infra/docs/adr/0004-tenancy-model.md +42 -0
- svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
- svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
- svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
- svc_infra/docs/adr/0008-billing-primitives.md +143 -0
- svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
- svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
- svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
- svc_infra/docs/api.md +59 -0
- svc_infra/docs/auth.md +11 -0
- svc_infra/docs/billing.md +190 -0
- svc_infra/docs/cache.md +76 -0
- svc_infra/docs/cli.md +74 -0
- svc_infra/docs/contributing.md +34 -0
- svc_infra/docs/data-lifecycle.md +52 -0
- svc_infra/docs/database.md +14 -0
- svc_infra/docs/docs-and-sdks.md +62 -0
- svc_infra/docs/environment.md +114 -0
- svc_infra/docs/getting-started.md +63 -0
- svc_infra/docs/idempotency.md +111 -0
- svc_infra/docs/jobs.md +67 -0
- svc_infra/docs/observability.md +16 -0
- svc_infra/docs/ops.md +37 -0
- svc_infra/docs/rate-limiting.md +125 -0
- svc_infra/docs/repo-review.md +48 -0
- svc_infra/docs/security.md +176 -0
- svc_infra/docs/tenancy.md +35 -0
- svc_infra/docs/timeouts-and-resource-limits.md +147 -0
- svc_infra/docs/webhooks.md +112 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +67 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +72 -0
- svc_infra/jobs/builtins/webhook_delivery.py +14 -2
- svc_infra/jobs/queue.py +9 -1
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/worker.py +17 -1
- svc_infra/mcp/svc_infra_mcp.py +85 -28
- svc_infra/obs/add.py +54 -7
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +6 -2
- svc_infra/security/permissions.py +1 -0
- svc_infra/webhooks/service.py +10 -2
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/METADATA +40 -14
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/RECORD +118 -44
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/entry_points.txt +0 -0
svc_infra/mcp/svc_infra_mcp.py
CHANGED
|
@@ -5,6 +5,9 @@ from enum import Enum
|
|
|
5
5
|
from ai_infra.llm.tools.custom.cli import cli_cmd_help, cli_subcmd_help
|
|
6
6
|
from ai_infra.mcp.server.tools import mcp_from_functions
|
|
7
7
|
|
|
8
|
+
from svc_infra.app.env import prepare_env
|
|
9
|
+
from svc_infra.cli.foundation.runner import run_from_root
|
|
10
|
+
|
|
8
11
|
CLI_PROG = "svc-infra"
|
|
9
12
|
|
|
10
13
|
|
|
@@ -17,34 +20,73 @@ async def svc_infra_cmd_help() -> dict:
|
|
|
17
20
|
return await cli_cmd_help(CLI_PROG)
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
# No dedicated 'docs list' function — users can use 'docs --help' to discover topics.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def svc_infra_docs_help() -> dict:
|
|
27
|
+
"""
|
|
28
|
+
Run 'svc-infra docs --help' and return its output.
|
|
29
|
+
Prepares the project environment and executes from the repo root so
|
|
30
|
+
environment-provided docs directories and local topics are discoverable.
|
|
31
|
+
"""
|
|
32
|
+
root = prepare_env()
|
|
33
|
+
text = await run_from_root(root, CLI_PROG, ["docs", "--help"])
|
|
34
|
+
return {
|
|
35
|
+
"ok": True,
|
|
36
|
+
"action": "docs_help",
|
|
37
|
+
"project_root": str(root),
|
|
38
|
+
"help": text,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
20
42
|
class Subcommand(str, Enum):
|
|
21
|
-
# SQL commands
|
|
22
|
-
sql_init = "sql
|
|
23
|
-
sql_revision = "sql
|
|
24
|
-
sql_upgrade = "sql
|
|
25
|
-
sql_downgrade = "sql
|
|
26
|
-
sql_current = "sql
|
|
27
|
-
sql_history = "sql
|
|
28
|
-
sql_stamp = "sql
|
|
29
|
-
sql_merge_heads = "sql
|
|
30
|
-
sql_setup_and_migrate = "sql
|
|
31
|
-
sql_scaffold = "sql
|
|
32
|
-
sql_scaffold_models = "sql
|
|
33
|
-
sql_scaffold_schemas = "sql
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
# SQL group commands
|
|
44
|
+
sql_init = "sql init"
|
|
45
|
+
sql_revision = "sql revision"
|
|
46
|
+
sql_upgrade = "sql upgrade"
|
|
47
|
+
sql_downgrade = "sql downgrade"
|
|
48
|
+
sql_current = "sql current"
|
|
49
|
+
sql_history = "sql history"
|
|
50
|
+
sql_stamp = "sql stamp"
|
|
51
|
+
sql_merge_heads = "sql merge-heads"
|
|
52
|
+
sql_setup_and_migrate = "sql setup-and-migrate"
|
|
53
|
+
sql_scaffold = "sql scaffold"
|
|
54
|
+
sql_scaffold_models = "sql scaffold-models"
|
|
55
|
+
sql_scaffold_schemas = "sql scaffold-schemas"
|
|
56
|
+
sql_export_tenant = "sql export-tenant"
|
|
57
|
+
sql_seed = "sql seed"
|
|
58
|
+
|
|
59
|
+
# Mongo group commands
|
|
60
|
+
mongo_prepare = "mongo prepare"
|
|
61
|
+
mongo_setup_and_prepare = "mongo setup-and-prepare"
|
|
62
|
+
mongo_ping = "mongo ping"
|
|
63
|
+
mongo_scaffold = "mongo scaffold"
|
|
64
|
+
mongo_scaffold_documents = "mongo scaffold-documents"
|
|
65
|
+
mongo_scaffold_schemas = "mongo scaffold-schemas"
|
|
66
|
+
mongo_scaffold_resources = "mongo scaffold-resources"
|
|
67
|
+
|
|
68
|
+
# Observability group commands
|
|
69
|
+
obs_up = "obs up"
|
|
70
|
+
obs_down = "obs down"
|
|
71
|
+
obs_scaffold = "obs scaffold"
|
|
72
|
+
|
|
73
|
+
# Docs group
|
|
74
|
+
docs_help = "docs --help"
|
|
75
|
+
docs_show = "docs show"
|
|
76
|
+
|
|
77
|
+
# DX group
|
|
78
|
+
dx_openapi = "dx openapi"
|
|
79
|
+
dx_migrations = "dx migrations"
|
|
80
|
+
dx_changelog = "dx changelog"
|
|
81
|
+
dx_ci = "dx ci"
|
|
82
|
+
|
|
83
|
+
# Jobs group
|
|
84
|
+
jobs_run = "jobs run"
|
|
85
|
+
|
|
86
|
+
# SDK group
|
|
87
|
+
sdk_ts = "sdk ts"
|
|
88
|
+
sdk_py = "sdk py"
|
|
89
|
+
sdk_postman = "sdk postman"
|
|
48
90
|
|
|
49
91
|
|
|
50
92
|
async def svc_infra_subcmd_help(subcommand: Subcommand) -> dict:
|
|
@@ -52,7 +94,19 @@ async def svc_infra_subcmd_help(subcommand: Subcommand) -> dict:
|
|
|
52
94
|
Get help text for a specific subcommand of svc-infra CLI.
|
|
53
95
|
(Enum keeps a tight schema; function signature remains simple.)
|
|
54
96
|
"""
|
|
55
|
-
|
|
97
|
+
tokens = subcommand.value.split()
|
|
98
|
+
if len(tokens) == 1:
|
|
99
|
+
return await cli_subcmd_help(CLI_PROG, subcommand)
|
|
100
|
+
|
|
101
|
+
root = prepare_env()
|
|
102
|
+
text = await run_from_root(root, CLI_PROG, [*tokens, "--help"])
|
|
103
|
+
return {
|
|
104
|
+
"ok": True,
|
|
105
|
+
"action": "subcommand_help",
|
|
106
|
+
"subcommand": subcommand.value,
|
|
107
|
+
"project_root": str(root),
|
|
108
|
+
"help": text,
|
|
109
|
+
}
|
|
56
110
|
|
|
57
111
|
|
|
58
112
|
mcp = mcp_from_functions(
|
|
@@ -60,8 +114,11 @@ mcp = mcp_from_functions(
|
|
|
60
114
|
functions=[
|
|
61
115
|
svc_infra_cmd_help,
|
|
62
116
|
svc_infra_subcmd_help,
|
|
117
|
+
svc_infra_docs_help,
|
|
118
|
+
# Docs listing is available via 'docs --help'; no separate MCP function needed.
|
|
63
119
|
],
|
|
64
120
|
)
|
|
65
121
|
|
|
122
|
+
|
|
66
123
|
if __name__ == "__main__":
|
|
67
124
|
mcp.run(transport="stdio")
|
svc_infra/obs/add.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Callable, Iterable, Optional
|
|
3
|
+
from typing import Any, Callable, Iterable, Optional, Protocol
|
|
4
4
|
|
|
5
5
|
from svc_infra.obs.settings import ObservabilitySettings
|
|
6
6
|
|
|
@@ -9,12 +9,20 @@ def _want_metrics(cfg: ObservabilitySettings) -> bool:
|
|
|
9
9
|
return bool(cfg.METRICS_ENABLED)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class RouteClassifier(Protocol):
|
|
13
|
+
def __call__(
|
|
14
|
+
self, route_path: str, method: str
|
|
15
|
+
) -> str: # e.g., returns "public|internal|admin"
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
12
19
|
def add_observability(
|
|
13
20
|
app: Any | None = None,
|
|
14
21
|
*,
|
|
15
22
|
db_engines: Optional[Iterable[Any]] = None,
|
|
16
23
|
metrics_path: str | None = None,
|
|
17
24
|
skip_metric_paths: Optional[Iterable[str]] = None,
|
|
25
|
+
route_classifier: RouteClassifier | None = None,
|
|
18
26
|
) -> Callable[[], None]:
|
|
19
27
|
"""
|
|
20
28
|
Enable Prometheus metrics for the ASGI app and optional SQLAlchemy pool metrics.
|
|
@@ -25,14 +33,53 @@ def add_observability(
|
|
|
25
33
|
# --- Metrics (Prometheus) — import lazily so CLIs/tests don’t require prometheus_client
|
|
26
34
|
if app is not None and _want_metrics(cfg):
|
|
27
35
|
try:
|
|
28
|
-
from svc_infra.obs.metrics.asgi import
|
|
36
|
+
from svc_infra.obs.metrics.asgi import ( # lazy
|
|
37
|
+
PrometheusMiddleware,
|
|
38
|
+
add_prometheus,
|
|
39
|
+
metrics_endpoint,
|
|
40
|
+
)
|
|
29
41
|
|
|
30
42
|
path = metrics_path or cfg.METRICS_PATH
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
skip_paths = tuple(skip_metric_paths or (path, "/health", "/healthz"))
|
|
44
|
+
# If a route_classifier is provided, use a custom route_resolver to append class label
|
|
45
|
+
if route_classifier is None:
|
|
46
|
+
add_prometheus(
|
|
47
|
+
app,
|
|
48
|
+
path=path,
|
|
49
|
+
skip_paths=skip_paths,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
# Install middleware manually to pass route_resolver
|
|
53
|
+
def _resolver(req):
|
|
54
|
+
# Base template
|
|
55
|
+
from svc_infra.obs.metrics.asgi import _route_template # type: ignore
|
|
56
|
+
|
|
57
|
+
base = _route_template(req)
|
|
58
|
+
method = getattr(req, "method", "GET")
|
|
59
|
+
cls = route_classifier(base, method)
|
|
60
|
+
# Encode as base|class for downstream label splitting in dashboards
|
|
61
|
+
return f"{base}|{cls}"
|
|
62
|
+
|
|
63
|
+
app.add_middleware(
|
|
64
|
+
PrometheusMiddleware,
|
|
65
|
+
skip_paths=skip_paths,
|
|
66
|
+
route_resolver=_resolver,
|
|
67
|
+
)
|
|
68
|
+
# Mount /metrics endpoint without re-adding middleware
|
|
69
|
+
try:
|
|
70
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
71
|
+
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV
|
|
72
|
+
|
|
73
|
+
router = public_router()
|
|
74
|
+
router.add_api_route(
|
|
75
|
+
path,
|
|
76
|
+
endpoint=metrics_endpoint(),
|
|
77
|
+
include_in_schema=CURRENT_ENVIRONMENT in (LOCAL_ENV, DEV_ENV),
|
|
78
|
+
tags=["observability"],
|
|
79
|
+
)
|
|
80
|
+
app.include_router(router)
|
|
81
|
+
except Exception:
|
|
82
|
+
app.add_route(path, metrics_endpoint())
|
|
36
83
|
except Exception:
|
|
37
84
|
pass
|
|
38
85
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Service HTTP Overview",
|
|
3
|
+
"tags": ["svc-infra", "http"],
|
|
4
|
+
"timezone": "browser",
|
|
5
|
+
"panels": [
|
|
6
|
+
{
|
|
7
|
+
"type": "timeseries",
|
|
8
|
+
"title": "Success Rate (5m)",
|
|
9
|
+
"targets": [
|
|
10
|
+
{
|
|
11
|
+
"expr": "sum(rate(http_server_requests_total{code!~\"5..\"}[5m])) / sum(rate(http_server_requests_total[5m]))",
|
|
12
|
+
"legendFormat": "success_rate"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "timeseries",
|
|
18
|
+
"title": "Latency p99",
|
|
19
|
+
"targets": [
|
|
20
|
+
{
|
|
21
|
+
"expr": "histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le))",
|
|
22
|
+
"legendFormat": "p99"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "table",
|
|
28
|
+
"title": "Top Routes by Error (5m)",
|
|
29
|
+
"targets": [
|
|
30
|
+
{
|
|
31
|
+
"expr": "topk(10, sum(rate(http_server_requests_total{code=~\"5..\"}[5m])) by (route))",
|
|
32
|
+
"legendFormat": "{{route}}"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"templating": {
|
|
38
|
+
"list": []
|
|
39
|
+
},
|
|
40
|
+
"time": {
|
|
41
|
+
"from": "now-6h",
|
|
42
|
+
"to": "now"
|
|
43
|
+
},
|
|
44
|
+
"refresh": "30s"
|
|
45
|
+
}
|
svc_infra/security/headers.py
CHANGED
|
@@ -6,8 +6,21 @@ SECURE_DEFAULTS = {
|
|
|
6
6
|
"X-Frame-Options": "DENY",
|
|
7
7
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
8
8
|
"X-XSS-Protection": "0",
|
|
9
|
-
# CSP
|
|
10
|
-
|
|
9
|
+
# CSP with practical defaults - allows inline styles/scripts and data URIs for images
|
|
10
|
+
# Also allows cdn.jsdelivr.net for FastAPI docs (Swagger UI, ReDoc)
|
|
11
|
+
# Still secure: blocks arbitrary external scripts, prevents framing, restricts form actions
|
|
12
|
+
# Override via headers_overrides in add_security() for stricter or custom policies
|
|
13
|
+
"Content-Security-Policy": (
|
|
14
|
+
"default-src 'self'; "
|
|
15
|
+
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
|
16
|
+
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
|
17
|
+
"img-src 'self' data: https:; "
|
|
18
|
+
"connect-src 'self'; "
|
|
19
|
+
"font-src 'self' https://cdn.jsdelivr.net; "
|
|
20
|
+
"frame-ancestors 'none'; "
|
|
21
|
+
"base-uri 'self'; "
|
|
22
|
+
"form-action 'self'"
|
|
23
|
+
),
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
|
svc_infra/security/hibp.py
CHANGED
|
@@ -5,7 +5,7 @@ import time
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import Dict, Optional
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
from svc_infra.http import new_httpx_client
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def sha1_hex(data: str) -> str:
|
|
@@ -39,7 +39,11 @@ class HIBPClient:
|
|
|
39
39
|
self.timeout = timeout
|
|
40
40
|
self.user_agent = user_agent
|
|
41
41
|
self._cache: Dict[str, CacheEntry] = {}
|
|
42
|
-
|
|
42
|
+
# Use central factory for consistent defaults; retain explicit timeout override
|
|
43
|
+
self._http = new_httpx_client(
|
|
44
|
+
timeout_seconds=self.timeout,
|
|
45
|
+
headers={"User-Agent": self.user_agent},
|
|
46
|
+
)
|
|
43
47
|
|
|
44
48
|
def _get_cached(self, prefix: str) -> Optional[str]:
|
|
45
49
|
now = time.time()
|
svc_infra/webhooks/service.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from typing import Dict, List
|
|
6
|
-
|
|
7
6
|
from uuid import uuid4
|
|
8
7
|
|
|
9
8
|
from svc_infra.db.outbox import OutboxStore
|
|
@@ -22,7 +21,16 @@ class InMemoryWebhookSubscriptions:
|
|
|
22
21
|
self._subs: Dict[str, List[WebhookSubscription]] = {}
|
|
23
22
|
|
|
24
23
|
def add(self, topic: str, url: str, secret: str) -> None:
|
|
25
|
-
|
|
24
|
+
# Upsert semantics per (topic, url): if a subscription already exists
|
|
25
|
+
# for this topic and URL, rotate its secret instead of adding a new row.
|
|
26
|
+
# This mirrors typical real-world secret rotation flows where the
|
|
27
|
+
# endpoint remains the same but the signing secret changes.
|
|
28
|
+
lst = self._subs.setdefault(topic, [])
|
|
29
|
+
for sub in lst:
|
|
30
|
+
if sub.url == url:
|
|
31
|
+
sub.secret = secret
|
|
32
|
+
return
|
|
33
|
+
lst.append(WebhookSubscription(topic, url, secret))
|
|
26
34
|
|
|
27
35
|
def get_for_topic(self, topic: str) -> List[WebhookSubscription]:
|
|
28
36
|
return list(self._subs.get(topic, []))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: svc-infra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.640
|
|
4
4
|
Summary: Infrastructure for building and deploying prod-ready services
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
|
|
@@ -77,22 +77,48 @@ Description-Content-Type: text/markdown
|
|
|
77
77
|
# svc-infra
|
|
78
78
|
|
|
79
79
|
[](https://pypi.org/project/svc-infra/)
|
|
80
|
-
[](
|
|
80
|
+
[](.)
|
|
81
81
|
|
|
82
82
|
svc-infra packages the shared building blocks we use to ship production FastAPI services fast—HTTP APIs with secure auth, durable persistence, background execution, cache, observability, and webhook plumbing that all share the same batteries-included defaults.
|
|
83
83
|
|
|
84
84
|
## Helper index
|
|
85
85
|
|
|
86
|
-
|
|
|
86
|
+
| Area | What it covers | Guide |
|
|
87
87
|
| --- | --- | --- |
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
88
|
+
| Getting Started | Overview and entry points | [This page](src/svc_infra/docs/getting-started.md) |
|
|
89
|
+
| Environment | Feature switches and env vars | [Environment](src/svc_infra/docs/environment.md) |
|
|
90
|
+
| API | FastAPI bootstrap, middleware, docs wiring | [API guide](src/svc_infra/docs/api.md) |
|
|
91
|
+
| Auth | Sessions, OAuth/OIDC, MFA, SMTP delivery | [Auth](src/svc_infra/docs/auth.md) |
|
|
92
|
+
| Security | Password policy, lockout, signed cookies, headers | [Security](ssrc/svc_infra/docs/ecurity.md) |
|
|
93
|
+
| Database | SQL + Mongo wiring, Alembic helpers, inbox/outbox patterns | [Database](src/svc_infra/docs/database.md) |
|
|
94
|
+
| Tenancy | Multi-tenant boundaries and helpers | [Tenancy](src/svc_infra/docs/tenancy.md) |
|
|
95
|
+
| Idempotency | Idempotent endpoints and middleware | [Idempotency](src/svc_infra/docs/idempotency.md) |
|
|
96
|
+
| Rate Limiting | Middleware, dependency limiter, headers | [Rate limiting](rsrc/svc_infra/docs/ate-limiting.md) |
|
|
97
|
+
| Cache | cashews decorators, namespace management, TTL helpers | [Cache](src/svc_infra/docs/cache.md) |
|
|
98
|
+
| Jobs | JobQueue, scheduler, CLI worker | [Jobs](src/svc_infra/docs/jobs.md) |
|
|
99
|
+
| Observability | Prometheus, Grafana, OpenTelemetry | [Observability](src/svc_infra/docs/observability.md) |
|
|
100
|
+
| Ops | Probes, breakers, SLOs & dashboards | [Ops](src/svc_infra/docs/ops.md) |
|
|
101
|
+
| Webhooks | Subscription store, signing, retry worker | [Webhooks](src/svc_infra/docs/webhooks.md) |
|
|
102
|
+
| CLI | Command groups for sql/mongo/obs/docs/dx/sdk/jobs | [CLI](src/svc_infra/docs/cli.md) |
|
|
103
|
+
| Docs & SDKs | Publishing docs, generating SDKs | [Docs & SDKs](src/svc_infra/docs/docs-and-sdks.md) |
|
|
104
|
+
| Acceptance | Acceptance harness and flows | [Acceptance](src/svc_infra/docs/acceptance.md), [Matrix](src/svc_infra/docs/acceptance-matrix.md) |
|
|
105
|
+
| Contributing | Dev setup and quality gates | [Contributing](src/svc_infra/docs/contributing.md) |
|
|
106
|
+
| Repo Review | Checklist for releasing/PRs | [Repo review](src/svc_infra/docs/repo-review.md) |
|
|
107
|
+
| Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](src/svc_infra/docs/data-lifecycle.md) |
|
|
108
|
+
|
|
109
|
+
## Quick Start with Template Example
|
|
110
|
+
|
|
111
|
+
See **ALL** svc-infra features working together in a complete example:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# One-time setup (from repo root)
|
|
115
|
+
make setup-template # Scaffolds models, runs migrations
|
|
116
|
+
|
|
117
|
+
# Run the example server
|
|
118
|
+
make run-template # Starts at http://localhost:8001
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See [`examples/README.md`](examples/README.md) for full documentation and manual setup options.
|
|
96
122
|
|
|
97
123
|
## Minimal FastAPI bootstrap
|
|
98
124
|
|
|
@@ -120,9 +146,9 @@ async def handle_webhook(payload = Depends(require_signature(lambda: ["current",
|
|
|
120
146
|
- **API** – toggle logging/observability and docs exposure with `ENABLE_LOGGING`, `LOG_LEVEL`, `LOG_FORMAT`, `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and `CORS_ALLOW_ORIGINS`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/api/fastapi/setup.py†L47-L88】
|
|
121
147
|
- **Auth** – configure JWT secrets, SMTP, cookies, and policy using the `AUTH_…` settings family (e.g., `AUTH_JWT__SECRET`, `AUTH_SMTP_HOST`, `AUTH_SESSION_COOKIE_SECURE`). 【F:src/svc_infra/api/fastapi/auth/settings.py†L23-L91】
|
|
122
148
|
- **Database** – set connection URLs or components via `SQL_URL`/`SQL_URL_FILE`, `DB_DIALECT`, `DB_HOST`, `DB_USER`, `DB_PASSWORD`, plus Mongo knobs like `MONGO_URL`, `MONGO_DB`, and `MONGO_URL_FILE`. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L55-L114】【F:src/svc_infra/db/sql/utils.py†L85-L206】【F:src/svc_infra/db/nosql/mongo/settings.py†L9-L13】【F:src/svc_infra/db/nosql/utils.py†L56-L113】
|
|
123
|
-
- **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:docs/jobs.md†L11-L48】
|
|
149
|
+
- **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:src/svc_infra/docs/jobs.md†L11-L48】
|
|
124
150
|
- **Cache** – namespace keys and lifetimes through `CACHE_PREFIX`, `CACHE_VERSION`, and TTL overrides `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG`. 【F:src/svc_infra/cache/README.md†L20-L173】【F:src/svc_infra/cache/ttl.py†L26-L55】
|
|
125
151
|
- **Observability** – turn metrics on/off or adjust scrape paths with `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and Prometheus/Grafana flags like `SVC_INFRA_DISABLE_PROMETHEUS`, `SVC_INFRA_RATE_WINDOW`, `SVC_INFRA_DASHBOARD_REFRESH`, `SVC_INFRA_DASHBOARD_RANGE`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/obs/metrics/asgi.py†L49-L206】【F:src/svc_infra/obs/cloud_dash.py†L85-L108】
|
|
126
|
-
- **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:docs/webhooks.md†L32-L53】
|
|
127
|
-
- **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:docs/security.md†L24-L70】
|
|
152
|
+
- **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:src/svc_infra/docs/webhooks.md†L32-L53】
|
|
153
|
+
- **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:src/svc_infra/docs/security.md†L24-L70】
|
|
128
154
|
|