svc-infra 0.1.589__py3-none-any.whl → 0.1.706__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/README.md +732 -0
- svc_infra/apf_payments/models.py +133 -42
- svc_infra/apf_payments/provider/__init__.py +4 -0
- svc_infra/apf_payments/provider/aiydan.py +871 -0
- svc_infra/apf_payments/provider/base.py +30 -9
- svc_infra/apf_payments/provider/stripe.py +156 -62
- svc_infra/apf_payments/schemas.py +19 -10
- svc_infra/apf_payments/service.py +211 -68
- svc_infra/apf_payments/settings.py +27 -3
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +15 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +245 -0
- svc_infra/api/fastapi/apf_payments/router.py +145 -46
- svc_infra/api/fastapi/apf_payments/setup.py +26 -8
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +6 -2
- svc_infra/api/fastapi/auth/add.py +27 -14
- svc_infra/api/fastapi/auth/gaurd.py +104 -13
- svc_infra/api/fastapi/auth/mfa/models.py +3 -1
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
- svc_infra/api/fastapi/auth/mfa/router.py +15 -8
- svc_infra/api/fastapi/auth/mfa/security.py +1 -2
- svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
- svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -1
- svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
- svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
- svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
- svc_infra/api/fastapi/auth/security.py +31 -10
- svc_infra/api/fastapi/auth/sender.py +8 -1
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/auth/state.py +3 -1
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +73 -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 +3 -1
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +71 -26
- svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
- svc_infra/api/fastapi/db/sql/health.py +3 -1
- svc_infra/api/fastapi/db/sql/session.py +18 -0
- svc_infra/api/fastapi/db/sql/users.py +29 -5
- svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
- svc_infra/api/fastapi/docs/add.py +173 -0
- svc_infra/api/fastapi/docs/landing.py +4 -2
- svc_infra/api/fastapi/docs/scoped.py +62 -15
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +126 -4
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +40 -13
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +10 -2
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +3 -1
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
- svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
- svc_infra/api/fastapi/middleware/idempotency.py +197 -70
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
- svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
- svc_infra/api/fastapi/middleware/request_id.py +27 -11
- svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- svc_infra/api/fastapi/middleware/timeout.py +177 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -3
- svc_infra/api/fastapi/openapi/conventions.py +9 -2
- svc_infra/api/fastapi/openapi/mutators.py +165 -20
- svc_infra/api/fastapi/openapi/pipeline.py +1 -1
- svc_infra/api/fastapi/openapi/security.py +3 -1
- svc_infra/api/fastapi/ops/add.py +75 -0
- svc_infra/api/fastapi/pagination.py +47 -20
- svc_infra/api/fastapi/routers/__init__.py +43 -15
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +188 -56
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +69 -1
- svc_infra/app/logging/add.py +9 -2
- svc_infra/app/logging/formats.py +12 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +241 -0
- svc_infra/billing/models.py +177 -0
- svc_infra/billing/quotas.py +103 -0
- svc_infra/billing/schemas.py +36 -0
- svc_infra/billing/service.py +123 -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 +9 -0
- svc_infra/cache/add.py +170 -0
- svc_infra/cache/backend.py +7 -6
- svc_infra/cache/decorators.py +81 -15
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +24 -4
- svc_infra/cache/recache.py +26 -14
- svc_infra/cache/resources.py +14 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/utils.py +3 -1
- svc_infra/cli/__init__.py +52 -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 +7 -4
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
- svc_infra/cli/cmds/db/ops_cmds.py +270 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +116 -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 +47 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
- svc_infra/cli/foundation/runner.py +6 -2
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +58 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +61 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +9 -9
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +3 -0
- svc_infra/db/nosql/core.py +30 -9
- svc_infra/db/nosql/indexes.py +3 -1
- svc_infra/db/nosql/management.py +1 -1
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +19 -2
- svc_infra/db/nosql/mongo/settings.py +6 -2
- svc_infra/db/nosql/repository.py +35 -15
- svc_infra/db/nosql/resource.py +20 -3
- svc_infra/db/nosql/scaffold.py +9 -3
- svc_infra/db/nosql/service.py +3 -1
- svc_infra/db/nosql/types.py +6 -2
- svc_infra/db/ops.py +384 -0
- svc_infra/db/outbox.py +108 -0
- svc_infra/db/sql/apikey.py +37 -9
- svc_infra/db/sql/authref.py +9 -3
- svc_infra/db/sql/constants.py +12 -8
- svc_infra/db/sql/core.py +2 -2
- svc_infra/db/sql/management.py +11 -8
- svc_infra/db/sql/repository.py +99 -26
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/scaffold.py +6 -2
- svc_infra/db/sql/service.py +15 -5
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- 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 +88 -0
- svc_infra/db/sql/uniq_hooks.py +9 -3
- svc_infra/db/sql/utils.py +138 -51
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/deploy/__init__.py +538 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +264 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +264 -0
- svc_infra/dx/add.py +65 -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 +864 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +105 -0
- svc_infra/jobs/builtins/outbox_processor.py +40 -0
- svc_infra/jobs/builtins/webhook_delivery.py +95 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +50 -0
- svc_infra/jobs/queue.py +116 -0
- svc_infra/jobs/redis_queue.py +256 -0
- svc_infra/jobs/runner.py +79 -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 +142 -0
- svc_infra/loaders/github.py +311 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +235 -0
- svc_infra/logging/__init__.py +374 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +65 -9
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +52 -0
- svc_infra/obs/metrics/asgi.py +13 -7
- svc_infra/obs/metrics/http.py +9 -5
- svc_infra/obs/metrics/sqlalchemy.py +13 -9
- svc_infra/obs/metrics.py +53 -0
- svc_infra/obs/settings.py +6 -2
- svc_infra/security/add.py +217 -0
- svc_infra/security/audit.py +212 -0
- svc_infra/security/audit_service.py +74 -0
- svc_infra/security/headers.py +52 -0
- svc_infra/security/hibp.py +101 -0
- svc_infra/security/jwt_rotation.py +105 -0
- svc_infra/security/lockout.py +102 -0
- svc_infra/security/models.py +287 -0
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +130 -0
- svc_infra/security/passwords.py +79 -0
- svc_infra/security/permissions.py +171 -0
- svc_infra/security/session.py +98 -0
- svc_infra/security/signed_cookies.py +100 -0
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +253 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +339 -0
- svc_infra/storage/backends/memory.py +216 -0
- svc_infra/storage/backends/s3.py +353 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +185 -0
- svc_infra/storage/settings.py +195 -0
- svc_infra/testing/__init__.py +685 -0
- svc_infra/utils.py +7 -3
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +339 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +39 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +70 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +140 -0
- svc_infra/websocket/client.py +282 -0
- svc_infra/websocket/config.py +69 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +344 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-0.1.706.dist-info/LICENSE +21 -0
- svc_infra-0.1.706.dist-info/METADATA +356 -0
- svc_infra-0.1.706.dist-info/RECORD +357 -0
- svc_infra-0.1.589.dist-info/METADATA +0 -79
- svc_infra-0.1.589.dist-info/RECORD +0 -234
- {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
svc_infra/api/fastapi/setup.py
CHANGED
|
@@ -9,14 +9,20 @@ from fastapi import FastAPI
|
|
|
9
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
10
10
|
from fastapi.responses import HTMLResponse
|
|
11
11
|
from fastapi.routing import APIRoute
|
|
12
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
12
13
|
|
|
13
14
|
from svc_infra.api.fastapi.docs.landing import CardSpec, DocTargets, render_index_html
|
|
14
15
|
from svc_infra.api.fastapi.docs.scoped import DOC_SCOPES
|
|
15
16
|
from svc_infra.api.fastapi.middleware.errors.catchall import CatchAllExceptionMiddleware
|
|
16
17
|
from svc_infra.api.fastapi.middleware.errors.handlers import register_error_handlers
|
|
18
|
+
from svc_infra.api.fastapi.middleware.graceful_shutdown import install_graceful_shutdown
|
|
17
19
|
from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
|
|
18
20
|
from svc_infra.api.fastapi.middleware.ratelimit import SimpleRateLimitMiddleware
|
|
19
21
|
from svc_infra.api.fastapi.middleware.request_id import RequestIdMiddleware
|
|
22
|
+
from svc_infra.api.fastapi.middleware.timeout import (
|
|
23
|
+
BodyReadTimeoutMiddleware,
|
|
24
|
+
HandlerTimeoutMiddleware,
|
|
25
|
+
)
|
|
20
26
|
from svc_infra.api.fastapi.openapi.models import APIVersionSpec, ServiceInfo
|
|
21
27
|
from svc_infra.api.fastapi.openapi.mutators import setup_mutators
|
|
22
28
|
from svc_infra.api.fastapi.openapi.pipeline import apply_mutators
|
|
@@ -34,8 +40,9 @@ def _gen_operation_id_factory():
|
|
|
34
40
|
|
|
35
41
|
def _gen(route: APIRoute) -> str:
|
|
36
42
|
base = route.name or getattr(route.endpoint, "__name__", "op")
|
|
37
|
-
base = _normalize(base)
|
|
38
|
-
|
|
43
|
+
base = _normalize(str(base)) # Convert Enum to str if needed
|
|
44
|
+
tag_raw = route.tags[0] if route.tags else ""
|
|
45
|
+
tag = _normalize(str(tag_raw)) if tag_raw else ""
|
|
39
46
|
method = next(iter(route.methods or ["GET"])).lower()
|
|
40
47
|
|
|
41
48
|
candidate = base
|
|
@@ -55,34 +62,103 @@ def _gen_operation_id_factory():
|
|
|
55
62
|
return _gen
|
|
56
63
|
|
|
57
64
|
|
|
65
|
+
def _origin_to_regex(origin: str) -> str | None:
|
|
66
|
+
"""Convert a wildcard origin pattern to a regex.
|
|
67
|
+
|
|
68
|
+
Supports patterns like:
|
|
69
|
+
- "https://*.vercel.app" -> matches any subdomain
|
|
70
|
+
- "https://nfrax-*.vercel.app" -> matches nfrax-xxx.vercel.app
|
|
71
|
+
|
|
72
|
+
Returns None if the origin is not a pattern (no wildcards).
|
|
73
|
+
"""
|
|
74
|
+
import re
|
|
75
|
+
|
|
76
|
+
if "*" not in origin:
|
|
77
|
+
return None
|
|
78
|
+
# Escape special regex chars except *, then replace * with regex pattern
|
|
79
|
+
escaped = re.escape(origin).replace(r"\*", "[a-zA-Z0-9_-]+")
|
|
80
|
+
return f"^{escaped}$"
|
|
81
|
+
|
|
82
|
+
|
|
58
83
|
def _setup_cors(app: FastAPI, public_cors_origins: list[str] | str | None = None):
|
|
84
|
+
# Collect origins from parameter
|
|
59
85
|
if isinstance(public_cors_origins, list):
|
|
60
|
-
|
|
86
|
+
param_origins = [o.strip() for o in public_cors_origins if o and o.strip()]
|
|
61
87
|
elif isinstance(public_cors_origins, str):
|
|
62
|
-
|
|
88
|
+
param_origins = [
|
|
89
|
+
o.strip() for o in public_cors_origins.split(",") if o and o.strip()
|
|
90
|
+
]
|
|
63
91
|
else:
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
param_origins = []
|
|
93
|
+
|
|
94
|
+
# Collect origins from environment variable
|
|
95
|
+
env_value = os.getenv("CORS_ALLOW_ORIGINS", "")
|
|
96
|
+
env_origins = [o.strip() for o in env_value.split(",") if o and o.strip()]
|
|
97
|
+
|
|
98
|
+
# Merge both sources, removing duplicates while preserving order
|
|
99
|
+
seen = set()
|
|
100
|
+
origins = []
|
|
101
|
+
for o in param_origins + env_origins:
|
|
102
|
+
if o not in seen:
|
|
103
|
+
seen.add(o)
|
|
104
|
+
origins.append(o)
|
|
66
105
|
|
|
67
106
|
if not origins:
|
|
68
107
|
return
|
|
69
108
|
|
|
70
109
|
cors_kwargs = dict(allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
|
110
|
+
|
|
111
|
+
# Check for "*" (allow all) first
|
|
71
112
|
if "*" in origins:
|
|
72
113
|
cors_kwargs["allow_origin_regex"] = ".*"
|
|
73
114
|
else:
|
|
74
|
-
|
|
115
|
+
# Separate exact origins from wildcard patterns
|
|
116
|
+
exact_origins = []
|
|
117
|
+
patterns = []
|
|
118
|
+
for o in origins:
|
|
119
|
+
regex = _origin_to_regex(o)
|
|
120
|
+
if regex:
|
|
121
|
+
patterns.append(regex)
|
|
122
|
+
else:
|
|
123
|
+
exact_origins.append(o)
|
|
124
|
+
|
|
125
|
+
# If we have patterns, combine into a single regex with exact origins
|
|
126
|
+
if patterns:
|
|
127
|
+
# Convert exact origins to regex patterns too
|
|
128
|
+
import re
|
|
129
|
+
|
|
130
|
+
for exact in exact_origins:
|
|
131
|
+
patterns.append(f"^{re.escape(exact)}$")
|
|
132
|
+
# Combine all patterns with OR
|
|
133
|
+
cors_kwargs["allow_origin_regex"] = "|".join(patterns)
|
|
134
|
+
else:
|
|
135
|
+
# No patterns, just use allow_origins
|
|
136
|
+
cors_kwargs["allow_origins"] = exact_origins
|
|
137
|
+
|
|
138
|
+
app.add_middleware(CORSMiddleware, **cors_kwargs) # type: ignore[arg-type] # CORSMiddleware accepts these kwargs
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _setup_middlewares(app: FastAPI, skip_paths: list[str] | None = None):
|
|
142
|
+
"""Configure middleware stack. All middlewares are pure ASGI for streaming compatibility.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
app: FastAPI application
|
|
146
|
+
skip_paths: Paths to skip for certain middlewares (e.g., long-running or streaming endpoints)
|
|
147
|
+
"""
|
|
148
|
+
paths = skip_paths or []
|
|
75
149
|
|
|
76
|
-
app.add_middleware(CORSMiddleware, **cors_kwargs)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _setup_middlewares(app: FastAPI):
|
|
80
150
|
app.add_middleware(RequestIdMiddleware)
|
|
151
|
+
# Timeouts: enforce body read timeout first, then total handler timeout
|
|
152
|
+
app.add_middleware(BodyReadTimeoutMiddleware)
|
|
153
|
+
app.add_middleware(HandlerTimeoutMiddleware, skip_paths=paths)
|
|
81
154
|
app.add_middleware(CatchAllExceptionMiddleware)
|
|
82
|
-
|
|
83
|
-
app.add_middleware(
|
|
155
|
+
# Idempotency and rate limiting
|
|
156
|
+
app.add_middleware(IdempotencyMiddleware, skip_paths=paths)
|
|
157
|
+
app.add_middleware(SimpleRateLimitMiddleware, skip_paths=paths)
|
|
84
158
|
register_error_handlers(app)
|
|
85
|
-
_add_route_logger(app)
|
|
159
|
+
_add_route_logger(app, skip_paths=paths)
|
|
160
|
+
# Graceful shutdown: track in-flight and wait on shutdown
|
|
161
|
+
install_graceful_shutdown(app)
|
|
86
162
|
|
|
87
163
|
|
|
88
164
|
def _coerce_list(value: str | Iterable[str] | None) -> list[str]:
|
|
@@ -97,23 +173,30 @@ def _dump_or_none(model):
|
|
|
97
173
|
return model.model_dump(exclude_none=True) if model is not None else None
|
|
98
174
|
|
|
99
175
|
|
|
100
|
-
def _build_child_app(
|
|
101
|
-
|
|
176
|
+
def _build_child_app(
|
|
177
|
+
service: ServiceInfo, spec: APIVersionSpec, skip_paths: list[str] | None = None
|
|
178
|
+
) -> FastAPI:
|
|
179
|
+
title = (
|
|
180
|
+
f"{service.name} • {spec.tag}" if getattr(spec, "tag", None) else service.name
|
|
181
|
+
)
|
|
102
182
|
child = FastAPI(
|
|
103
183
|
title=title,
|
|
104
184
|
version=service.release,
|
|
105
185
|
contact=_dump_or_none(service.contact),
|
|
106
186
|
license_info=_dump_or_none(service.license),
|
|
107
187
|
terms_of_service=service.terms_of_service,
|
|
108
|
-
description=service.description,
|
|
188
|
+
description=service.description or "",
|
|
109
189
|
generate_unique_id_function=_gen_operation_id_factory(),
|
|
110
190
|
)
|
|
111
191
|
|
|
112
|
-
_setup_middlewares(child)
|
|
192
|
+
_setup_middlewares(child, skip_paths=skip_paths)
|
|
113
193
|
|
|
114
194
|
# ---- OpenAPI pipeline (DRY!) ----
|
|
115
|
-
include_api_key =
|
|
116
|
-
|
|
195
|
+
include_api_key = (
|
|
196
|
+
bool(spec.include_api_key) if spec.include_api_key is not None else False
|
|
197
|
+
)
|
|
198
|
+
tag_str = str(spec.tag).strip("/")
|
|
199
|
+
mount_path = f"/{tag_str}"
|
|
117
200
|
server_url = (
|
|
118
201
|
mount_path
|
|
119
202
|
if not spec.public_base_url
|
|
@@ -130,11 +213,17 @@ def _build_child_app(service: ServiceInfo, spec: APIVersionSpec) -> FastAPI:
|
|
|
130
213
|
|
|
131
214
|
if spec.routers_package:
|
|
132
215
|
register_all_routers(
|
|
133
|
-
child,
|
|
216
|
+
child,
|
|
217
|
+
base_package=spec.routers_package,
|
|
218
|
+
prefix="",
|
|
219
|
+
environment=CURRENT_ENVIRONMENT,
|
|
134
220
|
)
|
|
135
221
|
|
|
136
222
|
logger.info(
|
|
137
|
-
"[%s] initialized version %s [env: %s]",
|
|
223
|
+
"[%s] initialized version %s [env: %s]",
|
|
224
|
+
service.name,
|
|
225
|
+
spec.tag,
|
|
226
|
+
CURRENT_ENVIRONMENT,
|
|
138
227
|
)
|
|
139
228
|
return child
|
|
140
229
|
|
|
@@ -146,23 +235,25 @@ def _build_parent_app(
|
|
|
146
235
|
root_routers: list[str] | str | None,
|
|
147
236
|
root_server_url: str | None = None,
|
|
148
237
|
root_include_api_key: bool = False,
|
|
238
|
+
skip_paths: list[str] | None = None,
|
|
239
|
+
**fastapi_kwargs, # Accept FastAPI kwargs
|
|
149
240
|
) -> FastAPI:
|
|
150
|
-
|
|
151
|
-
|
|
241
|
+
# Root docs are now enabled in all environments to match root card visibility
|
|
152
242
|
parent = FastAPI(
|
|
153
243
|
title=service.name,
|
|
154
244
|
version=service.release,
|
|
155
245
|
contact=_dump_or_none(service.contact),
|
|
156
246
|
license_info=_dump_or_none(service.license),
|
|
157
247
|
terms_of_service=service.terms_of_service,
|
|
158
|
-
description=service.description,
|
|
159
|
-
docs_url=
|
|
160
|
-
redoc_url=
|
|
161
|
-
openapi_url=
|
|
248
|
+
description=service.description or "",
|
|
249
|
+
docs_url="/docs",
|
|
250
|
+
redoc_url="/redoc",
|
|
251
|
+
openapi_url="/openapi.json",
|
|
252
|
+
**fastapi_kwargs, # Forward to FastAPI constructor
|
|
162
253
|
)
|
|
163
254
|
|
|
164
255
|
_setup_cors(parent, public_cors_origins)
|
|
165
|
-
_setup_middlewares(parent)
|
|
256
|
+
_setup_middlewares(parent, skip_paths=skip_paths)
|
|
166
257
|
|
|
167
258
|
mutators = setup_mutators(
|
|
168
259
|
service=service,
|
|
@@ -181,23 +272,54 @@ def _build_parent_app(
|
|
|
181
272
|
)
|
|
182
273
|
# app-provided root routers
|
|
183
274
|
for pkg in _coerce_list(root_routers):
|
|
184
|
-
register_all_routers(
|
|
275
|
+
register_all_routers(
|
|
276
|
+
parent, base_package=pkg, prefix="", environment=CURRENT_ENVIRONMENT
|
|
277
|
+
)
|
|
185
278
|
|
|
186
279
|
return parent
|
|
187
280
|
|
|
188
281
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
282
|
+
class RouteLoggerMiddleware:
|
|
283
|
+
"""Pure ASGI middleware to add X-Handled-By header."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, app: ASGIApp, skip_paths: list[str] | None = None):
|
|
286
|
+
self.app = app
|
|
287
|
+
self.skip_paths = skip_paths or []
|
|
288
|
+
|
|
289
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
290
|
+
if scope.get("type") != "http":
|
|
291
|
+
await self.app(scope, receive, send)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
path = scope.get("path", "")
|
|
295
|
+
method = scope.get("method", "")
|
|
296
|
+
|
|
297
|
+
# Skip specified paths
|
|
298
|
+
if any(skip in path for skip in self.skip_paths):
|
|
299
|
+
await self.app(scope, receive, send)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Wrap send to add header after response starts
|
|
303
|
+
async def send_wrapper(message):
|
|
304
|
+
if message["type"] == "http.response.start":
|
|
305
|
+
route = scope.get("route")
|
|
306
|
+
route_path = getattr(route, "path_format", None) or getattr(
|
|
307
|
+
route, "path", None
|
|
308
|
+
)
|
|
309
|
+
if route_path:
|
|
310
|
+
root_path = scope.get("root_path", "") or ""
|
|
311
|
+
headers = list(message.get("headers", []))
|
|
312
|
+
headers.append(
|
|
313
|
+
(b"x-handled-by", f"{method} {root_path}{route_path}".encode())
|
|
314
|
+
)
|
|
315
|
+
message = {**message, "headers": headers}
|
|
316
|
+
await send(message)
|
|
317
|
+
|
|
318
|
+
await self.app(scope, receive, send_wrapper)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _add_route_logger(app: FastAPI, skip_paths: list[str] | None = None):
|
|
322
|
+
app.add_middleware(RouteLoggerMiddleware, skip_paths=skip_paths)
|
|
201
323
|
|
|
202
324
|
|
|
203
325
|
def setup_service_api(
|
|
@@ -208,6 +330,8 @@ def setup_service_api(
|
|
|
208
330
|
public_cors_origins: list[str] | str | None = None,
|
|
209
331
|
root_public_base_url: str | None = None,
|
|
210
332
|
root_include_api_key: bool | None = None,
|
|
333
|
+
skip_paths: list[str] | None = None,
|
|
334
|
+
**fastapi_kwargs, # Forward all other FastAPI kwargs (lifespan, etc.)
|
|
211
335
|
) -> FastAPI:
|
|
212
336
|
# infer if not explicitly provided
|
|
213
337
|
effective_root_include_api_key = (
|
|
@@ -223,31 +347,35 @@ def setup_service_api(
|
|
|
223
347
|
root_routers=root_routers,
|
|
224
348
|
root_server_url=root_server,
|
|
225
349
|
root_include_api_key=effective_root_include_api_key,
|
|
350
|
+
skip_paths=skip_paths,
|
|
351
|
+
**fastapi_kwargs, # Forward to _build_parent_app
|
|
226
352
|
)
|
|
227
353
|
|
|
228
354
|
# Mount each version
|
|
229
355
|
for spec in versions:
|
|
230
|
-
child = _build_child_app(service, spec)
|
|
231
|
-
|
|
232
|
-
|
|
356
|
+
child = _build_child_app(service, spec, skip_paths=skip_paths)
|
|
357
|
+
tag_str = str(spec.tag).strip("/")
|
|
358
|
+
mount_path = f"/{tag_str}"
|
|
359
|
+
parent.mount(mount_path, child, name=tag_str)
|
|
233
360
|
|
|
234
|
-
@parent.get("/", include_in_schema=False)
|
|
361
|
+
@parent.get("/", include_in_schema=False, response_class=HTMLResponse)
|
|
235
362
|
def index():
|
|
236
363
|
cards: list[CardSpec] = []
|
|
237
364
|
is_local_dev = CURRENT_ENVIRONMENT in (LOCAL_ENV, DEV_ENV)
|
|
238
365
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
366
|
+
# Root card - always show in all environments
|
|
367
|
+
cards.append(
|
|
368
|
+
CardSpec(
|
|
369
|
+
tag="",
|
|
370
|
+
docs=DocTargets(
|
|
371
|
+
swagger="/docs", redoc="/redoc", openapi_json="/openapi.json"
|
|
372
|
+
),
|
|
246
373
|
)
|
|
374
|
+
)
|
|
247
375
|
|
|
248
376
|
# Version cards
|
|
249
377
|
for spec in versions:
|
|
250
|
-
tag = spec.tag.strip("/")
|
|
378
|
+
tag = str(spec.tag).strip("/")
|
|
251
379
|
cards.append(
|
|
252
380
|
CardSpec(
|
|
253
381
|
tag=tag,
|
|
@@ -265,11 +393,15 @@ def setup_service_api(
|
|
|
265
393
|
cards.append(
|
|
266
394
|
CardSpec(
|
|
267
395
|
tag=scope.strip("/"),
|
|
268
|
-
docs=DocTargets(
|
|
396
|
+
docs=DocTargets(
|
|
397
|
+
swagger=swagger, redoc=redoc, openapi_json=openapi_json
|
|
398
|
+
),
|
|
269
399
|
)
|
|
270
400
|
)
|
|
271
401
|
|
|
272
|
-
html = render_index_html(
|
|
402
|
+
html = render_index_html(
|
|
403
|
+
service_name=service.name, release=service.release, cards=cards
|
|
404
|
+
)
|
|
273
405
|
return HTMLResponse(html)
|
|
274
406
|
|
|
275
407
|
return parent
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from .context import set_tenant_resolver
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_tenancy(app: FastAPI, *, resolver: Optional[Callable[..., Any]] = None) -> None:
|
|
11
|
+
"""Wire tenancy resolver for the application.
|
|
12
|
+
|
|
13
|
+
Provide a resolver(request, identity, header) -> Optional[str] to override
|
|
14
|
+
the default resolution. Pass None to clear a previous override.
|
|
15
|
+
"""
|
|
16
|
+
set_tenant_resolver(resolver)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["add_tenancy"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
try: # optional import; auth may not be used by all consumers
|
|
8
|
+
from svc_infra.api.fastapi.auth.security import OptionalIdentity
|
|
9
|
+
except Exception: # pragma: no cover - fallback for minimal builds
|
|
10
|
+
OptionalIdentity = None # type: ignore[misc,assignment]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_tenant_resolver: Optional[Callable[..., Any]] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_tenant_resolver(
|
|
17
|
+
fn: Optional[Callable[..., Any]],
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Set or clear a global override hook for tenant resolution.
|
|
20
|
+
|
|
21
|
+
The function receives (request, identity, tenant_header) and should return a tenant id
|
|
22
|
+
string or None to fall back to default logic.
|
|
23
|
+
"""
|
|
24
|
+
global _tenant_resolver
|
|
25
|
+
_tenant_resolver = fn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _maybe_await(x):
|
|
29
|
+
if callable(getattr(x, "__await__", None)):
|
|
30
|
+
return await x
|
|
31
|
+
return x
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def resolve_tenant_id(
|
|
35
|
+
request: Request,
|
|
36
|
+
tenant_header: Optional[str] = None,
|
|
37
|
+
identity: Any = Depends(OptionalIdentity) if OptionalIdentity else None, # type: ignore[arg-type]
|
|
38
|
+
) -> Optional[str]:
|
|
39
|
+
"""Resolve tenant id from override, identity, header, or request.state.
|
|
40
|
+
|
|
41
|
+
Order:
|
|
42
|
+
1) Global override hook (set_tenant_resolver)
|
|
43
|
+
2) Auth identity: user.tenant_id then api_key.tenant_id (if available)
|
|
44
|
+
3) X-Tenant-Id header
|
|
45
|
+
4) request.state.tenant_id
|
|
46
|
+
"""
|
|
47
|
+
# read header value if not provided directly (supports direct calls without DI)
|
|
48
|
+
if tenant_header is None:
|
|
49
|
+
try:
|
|
50
|
+
tenant_header = request.headers.get("X-Tenant-Id")
|
|
51
|
+
except Exception:
|
|
52
|
+
tenant_header = None
|
|
53
|
+
|
|
54
|
+
# 1) global override
|
|
55
|
+
if _tenant_resolver is not None:
|
|
56
|
+
try:
|
|
57
|
+
v = _tenant_resolver(request, identity, tenant_header)
|
|
58
|
+
v2 = await _maybe_await(v)
|
|
59
|
+
if v2:
|
|
60
|
+
return str(v2)
|
|
61
|
+
except Exception:
|
|
62
|
+
# fall through to defaults
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# 2) from identity
|
|
66
|
+
try:
|
|
67
|
+
if identity and getattr(identity, "user", None) is not None:
|
|
68
|
+
tid = getattr(identity.user, "tenant_id", None)
|
|
69
|
+
if tid:
|
|
70
|
+
return str(tid)
|
|
71
|
+
if identity and getattr(identity, "api_key", None) is not None:
|
|
72
|
+
tid = getattr(identity.api_key, "tenant_id", None)
|
|
73
|
+
if tid:
|
|
74
|
+
return str(tid)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# 3) from header
|
|
79
|
+
if tenant_header and isinstance(tenant_header, str) and tenant_header.strip():
|
|
80
|
+
return tenant_header.strip()
|
|
81
|
+
|
|
82
|
+
# 4) request.state
|
|
83
|
+
try:
|
|
84
|
+
st_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
|
|
85
|
+
if st_tid:
|
|
86
|
+
return str(st_tid)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def require_tenant_id(
|
|
94
|
+
tenant_id: Optional[str] = Depends(resolve_tenant_id),
|
|
95
|
+
) -> str:
|
|
96
|
+
if not tenant_id:
|
|
97
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
98
|
+
return tenant_id
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# DX aliases
|
|
102
|
+
TenantId = Annotated[str, Depends(require_tenant_id)]
|
|
103
|
+
OptionalTenantId = Annotated[Optional[str], Depends(resolve_tenant_id)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = [
|
|
107
|
+
"TenantId",
|
|
108
|
+
"OptionalTenantId",
|
|
109
|
+
"resolve_tenant_id",
|
|
110
|
+
"require_tenant_id",
|
|
111
|
+
"set_tenant_resolver",
|
|
112
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for capturing routers from add_* functions for versioned routing.
|
|
3
|
+
|
|
4
|
+
This module provides helpers to use integration functions (add_banking, add_payments, etc.)
|
|
5
|
+
under versioned routing without creating separate documentation cards.
|
|
6
|
+
|
|
7
|
+
See: svc-infra/docs/versioned-integrations.md
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Callable, TypeVar
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, FastAPI
|
|
16
|
+
|
|
17
|
+
__all__ = ["extract_router"]
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_router(
|
|
23
|
+
add_function: Callable[..., T],
|
|
24
|
+
*,
|
|
25
|
+
prefix: str,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
) -> tuple[APIRouter, T]:
|
|
28
|
+
"""
|
|
29
|
+
Capture the router from an add_* function for versioned mounting.
|
|
30
|
+
|
|
31
|
+
This allows you to use integration functions like add_banking(), add_payments(),
|
|
32
|
+
etc. under versioned routing (e.g., /v0/banking) without creating separate
|
|
33
|
+
documentation cards.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
add_function: The add_* function to capture from (e.g., add_banking)
|
|
37
|
+
prefix: URL prefix for the routes (e.g., "/banking")
|
|
38
|
+
**kwargs: Arguments to pass to the add_function
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (router, return_value) where:
|
|
42
|
+
- router: The captured APIRouter with all routes
|
|
43
|
+
- return_value: The original return value from add_function (e.g., provider instance)
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```python
|
|
47
|
+
# In routers/v0/banking.py
|
|
48
|
+
from svc_infra.api.fastapi.versioned import extract_router
|
|
49
|
+
from fin_infra.banking import add_banking
|
|
50
|
+
|
|
51
|
+
router, banking_provider = extract_router(
|
|
52
|
+
add_banking,
|
|
53
|
+
prefix="/banking",
|
|
54
|
+
provider="plaid",
|
|
55
|
+
cache_ttl=60,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# svc-infra auto-discovers 'router' and mounts at /v0/banking
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Pattern:
|
|
62
|
+
1. Creates a mock FastAPI app
|
|
63
|
+
2. Intercepts include_router to capture the router
|
|
64
|
+
3. Patches add_prefixed_docs to prevent separate card creation
|
|
65
|
+
4. Calls the add_function which creates all routes
|
|
66
|
+
5. Returns the captured router for auto-discovery
|
|
67
|
+
|
|
68
|
+
See Also:
|
|
69
|
+
- docs/versioned-integrations.md: Full pattern documentation
|
|
70
|
+
- api/fastapi/dual/public.py: Similar pattern for dual routers
|
|
71
|
+
"""
|
|
72
|
+
# Create mock app to capture router
|
|
73
|
+
mock_app = FastAPI()
|
|
74
|
+
captured_router: APIRouter | None = None
|
|
75
|
+
|
|
76
|
+
def _capture_router(router: APIRouter, **_kwargs: Any) -> None:
|
|
77
|
+
"""Intercept include_router to capture instead of mount."""
|
|
78
|
+
nonlocal captured_router
|
|
79
|
+
captured_router = router
|
|
80
|
+
|
|
81
|
+
setattr(mock_app, "include_router", _capture_router)
|
|
82
|
+
|
|
83
|
+
# Patch add_prefixed_docs to prevent separate card (no-op if function doesn't call it)
|
|
84
|
+
def _noop_docs(*args: Any, **kwargs: Any) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# Call add_function with patches active
|
|
88
|
+
with patch("svc_infra.api.fastapi.docs.scoped.add_prefixed_docs", _noop_docs):
|
|
89
|
+
result = add_function(
|
|
90
|
+
mock_app,
|
|
91
|
+
prefix=prefix,
|
|
92
|
+
**kwargs,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if captured_router is None:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
f"Failed to capture router from {add_function.__name__}. "
|
|
98
|
+
f"The function may not call app.include_router()."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return captured_router, result
|
svc_infra/app/README.md
CHANGED
|
@@ -14,9 +14,8 @@ This README shows:
|
|
|
14
14
|
|
|
15
15
|
```python
|
|
16
16
|
# main.py (or wherever your app starts)
|
|
17
|
-
from svc_infra.
|
|
17
|
+
from svc_infra.app.logging import setup_logging, LogLevelOptions
|
|
18
18
|
from svc_infra.app.env import pick
|
|
19
|
-
from svc_infra.logging.logging import LogLevelOptions
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
---
|
|
@@ -39,7 +38,8 @@ What you get by default:
|
|
|
39
38
|
Set via code:
|
|
40
39
|
|
|
41
40
|
```python
|
|
42
|
-
from svc_infra.logging.
|
|
41
|
+
from svc_infra.app.logging.formats import LogFormatOptions
|
|
42
|
+
from svc_infra.app.logging import LogLevelOptions
|
|
43
43
|
|
|
44
44
|
setup_logging(
|
|
45
45
|
level=LogLevelOptions.INFO, # or "INFO"
|
|
@@ -119,7 +119,7 @@ Old (pre-filter) example:
|
|
|
119
119
|
|
|
120
120
|
```python
|
|
121
121
|
from svc_infra.app.env import pick
|
|
122
|
-
from svc_infra.
|
|
122
|
+
from svc_infra.app.logging import setup_logging, LogLevelOptions
|
|
123
123
|
|
|
124
124
|
setup_logging(
|
|
125
125
|
level=pick(
|
|
@@ -183,7 +183,7 @@ LOG_DROP_PATHS=/metrics,/health,/healthz
|
|
|
183
183
|
## 7) One-liner quickstart
|
|
184
184
|
|
|
185
185
|
```python
|
|
186
|
-
from svc_infra.logging import setup_logging
|
|
186
|
+
from svc_infra.app.logging import setup_logging
|
|
187
187
|
setup_logging() # done: sensible defaults + filters in prod/test
|
|
188
188
|
```
|
|
189
189
|
|
svc_infra/app/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .env import pick
|
|
1
|
+
from .env import MissingSecretError, pick, require_secret
|
|
2
2
|
from .logging import setup_logging
|
|
3
3
|
from .logging.formats import LoggingConfig, LogLevelOptions
|
|
4
4
|
|
|
@@ -7,4 +7,6 @@ __all__ = [
|
|
|
7
7
|
"LoggingConfig",
|
|
8
8
|
"LogLevelOptions",
|
|
9
9
|
"pick",
|
|
10
|
+
"require_secret",
|
|
11
|
+
"MissingSecretError",
|
|
10
12
|
]
|