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
|
@@ -3,11 +3,24 @@ from __future__ import annotations
|
|
|
3
3
|
import base64
|
|
4
4
|
import contextvars
|
|
5
5
|
import json
|
|
6
|
-
|
|
6
|
+
import logging
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Callable,
|
|
10
|
+
Generic,
|
|
11
|
+
Iterable,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
TypeVar,
|
|
16
|
+
cast,
|
|
17
|
+
)
|
|
7
18
|
|
|
8
19
|
from fastapi import Query, Request
|
|
9
20
|
from pydantic import BaseModel, Field
|
|
10
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
11
24
|
T = TypeVar("T")
|
|
12
25
|
|
|
13
26
|
|
|
@@ -44,13 +57,13 @@ def _encode_cursor(payload: dict) -> str:
|
|
|
44
57
|
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
45
58
|
|
|
46
59
|
|
|
47
|
-
def decode_cursor(token: Optional[str]) -> dict:
|
|
60
|
+
def decode_cursor(token: Optional[str]) -> dict[Any, Any]:
|
|
48
61
|
"""Public: decode an incoming cursor token for debugging/ops."""
|
|
49
62
|
if not token:
|
|
50
63
|
return {}
|
|
51
64
|
s = token + "=" * (-len(token) % 4)
|
|
52
65
|
raw = base64.urlsafe_b64decode(s.encode("ascii")).decode("utf-8")
|
|
53
|
-
return json.loads(raw)
|
|
66
|
+
return cast(dict[Any, Any], json.loads(raw))
|
|
54
67
|
|
|
55
68
|
|
|
56
69
|
# ---------- Context ----------
|
|
@@ -85,11 +98,15 @@ class PaginationContext(Generic[T]):
|
|
|
85
98
|
|
|
86
99
|
@property
|
|
87
100
|
def cursor(self) -> Optional[str]:
|
|
88
|
-
return (
|
|
101
|
+
return (
|
|
102
|
+
(self.cursor_params or CursorParams()).cursor if self.allow_cursor else None
|
|
103
|
+
)
|
|
89
104
|
|
|
90
105
|
@property
|
|
91
106
|
def limit(self) -> int:
|
|
92
|
-
|
|
107
|
+
# For cursor-based pagination, always honor the requested limit, even on the first page
|
|
108
|
+
# (cursor may be None for the first page).
|
|
109
|
+
if self.allow_cursor and self.cursor_params:
|
|
93
110
|
return self.cursor_params.limit
|
|
94
111
|
if self.allow_page and self.page_params:
|
|
95
112
|
return self.limit_override or self.page_params.page_size
|
|
@@ -101,7 +118,11 @@ class PaginationContext(Generic[T]):
|
|
|
101
118
|
|
|
102
119
|
@property
|
|
103
120
|
def page_size(self) -> Optional[int]:
|
|
104
|
-
return
|
|
121
|
+
return (
|
|
122
|
+
self.page_params.page_size
|
|
123
|
+
if (self.allow_page and self.page_params)
|
|
124
|
+
else None
|
|
125
|
+
)
|
|
105
126
|
|
|
106
127
|
@property
|
|
107
128
|
def offset(self) -> int:
|
|
@@ -110,7 +131,11 @@ class PaginationContext(Generic[T]):
|
|
|
110
131
|
return 0
|
|
111
132
|
|
|
112
133
|
def wrap(
|
|
113
|
-
self,
|
|
134
|
+
self,
|
|
135
|
+
items: list[T],
|
|
136
|
+
*,
|
|
137
|
+
next_cursor: Optional[str] = None,
|
|
138
|
+
total: Optional[int] = None,
|
|
114
139
|
):
|
|
115
140
|
if self.envelope:
|
|
116
141
|
return Paginated[T](items=items, next_cursor=next_cursor, total=total)
|
|
@@ -125,8 +150,8 @@ class PaginationContext(Generic[T]):
|
|
|
125
150
|
return _encode_cursor({"after": last_key})
|
|
126
151
|
|
|
127
152
|
|
|
128
|
-
_pagination_ctx: contextvars.ContextVar[PaginationContext] =
|
|
129
|
-
"pagination_ctx", default=None
|
|
153
|
+
_pagination_ctx: contextvars.ContextVar[PaginationContext | None] = (
|
|
154
|
+
contextvars.ContextVar("pagination_ctx", default=None)
|
|
130
155
|
)
|
|
131
156
|
|
|
132
157
|
|
|
@@ -146,7 +171,9 @@ def use_pagination() -> PaginationContext:
|
|
|
146
171
|
|
|
147
172
|
|
|
148
173
|
# ---------- Utilities ----------
|
|
149
|
-
def text_filter(
|
|
174
|
+
def text_filter(
|
|
175
|
+
items: Iterable[T], q: Optional[str], *getters: Callable[[T], str]
|
|
176
|
+
) -> list[T]:
|
|
150
177
|
if not q:
|
|
151
178
|
return list(items)
|
|
152
179
|
ql = q.lower()
|
|
@@ -157,8 +184,8 @@ def text_filter(items: Iterable[T], q: Optional[str], *getters: Callable[[T], st
|
|
|
157
184
|
if ql in (g(it) or "").lower():
|
|
158
185
|
out.append(it)
|
|
159
186
|
break
|
|
160
|
-
except Exception:
|
|
161
|
-
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.debug("text_filter getter failed for item: %s", e)
|
|
162
189
|
return out
|
|
163
190
|
|
|
164
191
|
|
|
@@ -215,7 +242,7 @@ def make_pagination_injector(
|
|
|
215
242
|
# Cursor-only (common case)
|
|
216
243
|
if allow_cursor and not allow_page and not include_filters:
|
|
217
244
|
|
|
218
|
-
async def
|
|
245
|
+
async def _inject_cursor(
|
|
219
246
|
request: Request,
|
|
220
247
|
cursor: str | None = Query(None),
|
|
221
248
|
limit: int = Query(default_limit, ge=1, le=max_limit),
|
|
@@ -233,12 +260,12 @@ def make_pagination_injector(
|
|
|
233
260
|
)
|
|
234
261
|
return None
|
|
235
262
|
|
|
236
|
-
return
|
|
263
|
+
return _inject_cursor
|
|
237
264
|
|
|
238
265
|
# Cursor + filters
|
|
239
266
|
if allow_cursor and not allow_page and include_filters:
|
|
240
267
|
|
|
241
|
-
async def
|
|
268
|
+
async def _inject_cursor_with_filters(
|
|
242
269
|
request: Request,
|
|
243
270
|
cursor: str | None = Query(None),
|
|
244
271
|
limit: int = Query(default_limit, ge=1, le=max_limit),
|
|
@@ -270,12 +297,12 @@ def make_pagination_injector(
|
|
|
270
297
|
)
|
|
271
298
|
return None
|
|
272
299
|
|
|
273
|
-
return
|
|
300
|
+
return _inject_cursor_with_filters
|
|
274
301
|
|
|
275
302
|
# Page-only
|
|
276
303
|
if not allow_cursor and allow_page:
|
|
277
304
|
|
|
278
|
-
async def
|
|
305
|
+
async def _inject_page(
|
|
279
306
|
request: Request,
|
|
280
307
|
page: int = Query(1, ge=1),
|
|
281
308
|
page_size: int = Query(default_limit, ge=1, le=max_limit),
|
|
@@ -293,10 +320,10 @@ def make_pagination_injector(
|
|
|
293
320
|
)
|
|
294
321
|
return None
|
|
295
322
|
|
|
296
|
-
return
|
|
323
|
+
return _inject_page
|
|
297
324
|
|
|
298
325
|
# Both cursor + page (rare; exposes all)
|
|
299
|
-
async def
|
|
326
|
+
async def _inject_all(
|
|
300
327
|
request: Request,
|
|
301
328
|
cursor: str | None = Query(None),
|
|
302
329
|
limit: int = Query(default_limit, ge=1, le=max_limit),
|
|
@@ -336,7 +363,7 @@ def make_pagination_injector(
|
|
|
336
363
|
)
|
|
337
364
|
return None
|
|
338
365
|
|
|
339
|
-
return
|
|
366
|
+
return _inject_all
|
|
340
367
|
|
|
341
368
|
|
|
342
369
|
# ----- Convenience helpers for routers -----
|
|
@@ -4,12 +4,18 @@ import importlib
|
|
|
4
4
|
import logging
|
|
5
5
|
import pkgutil
|
|
6
6
|
from types import ModuleType
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Any, Optional
|
|
8
8
|
|
|
9
9
|
from fastapi import FastAPI
|
|
10
10
|
from fastapi.routing import APIRoute
|
|
11
11
|
|
|
12
|
-
from svc_infra.app.env import
|
|
12
|
+
from svc_infra.app.env import (
|
|
13
|
+
ALL_ENVIRONMENTS,
|
|
14
|
+
CURRENT_ENVIRONMENT,
|
|
15
|
+
DEV_ENV,
|
|
16
|
+
LOCAL_ENV,
|
|
17
|
+
Environment,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
logger = logging.getLogger(__name__)
|
|
15
21
|
|
|
@@ -49,7 +55,9 @@ def _validate_base_package(base_package: str) -> ModuleType:
|
|
|
49
55
|
try:
|
|
50
56
|
package_module: ModuleType = importlib.import_module(base_package)
|
|
51
57
|
except Exception as exc:
|
|
52
|
-
raise RuntimeError(
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
f"Could not import base_package '{base_package}': {exc}"
|
|
60
|
+
) from exc
|
|
53
61
|
|
|
54
62
|
if not hasattr(package_module, "__path__"):
|
|
55
63
|
raise RuntimeError(
|
|
@@ -64,7 +72,11 @@ def _normalize_environment(environment: Optional[Environment | str]) -> Environm
|
|
|
64
72
|
return (
|
|
65
73
|
CURRENT_ENVIRONMENT
|
|
66
74
|
if environment is None
|
|
67
|
-
else (
|
|
75
|
+
else (
|
|
76
|
+
Environment(environment)
|
|
77
|
+
if not isinstance(environment, Environment)
|
|
78
|
+
else environment
|
|
79
|
+
)
|
|
68
80
|
)
|
|
69
81
|
|
|
70
82
|
|
|
@@ -87,9 +99,12 @@ def _is_router_excluded_by_environment(
|
|
|
87
99
|
|
|
88
100
|
# Support ALL_ENVIRONMENTS as a special value
|
|
89
101
|
if router_excluded_envs is ALL_ENVIRONMENTS or (
|
|
90
|
-
isinstance(router_excluded_envs, set)
|
|
102
|
+
isinstance(router_excluded_envs, set)
|
|
103
|
+
and router_excluded_envs == ALL_ENVIRONMENTS
|
|
91
104
|
):
|
|
92
|
-
logger.debug(
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"Skipping router module {module_name} due to ALL_ENVIRONMENTS exclusion."
|
|
107
|
+
)
|
|
93
108
|
return True
|
|
94
109
|
|
|
95
110
|
# Normalize to set of Environment or str
|
|
@@ -99,14 +114,19 @@ def _is_router_excluded_by_environment(
|
|
|
99
114
|
)
|
|
100
115
|
return False
|
|
101
116
|
|
|
102
|
-
normalized_excluded_envs = set()
|
|
117
|
+
normalized_excluded_envs: set[Environment | str] = set()
|
|
103
118
|
for e in router_excluded_envs:
|
|
104
119
|
try:
|
|
105
|
-
normalized_excluded_envs.add(
|
|
120
|
+
normalized_excluded_envs.add(
|
|
121
|
+
Environment(e) if not isinstance(e, Environment) else e
|
|
122
|
+
)
|
|
106
123
|
except Exception:
|
|
107
124
|
normalized_excluded_envs.add(str(e))
|
|
108
125
|
|
|
109
|
-
if
|
|
126
|
+
if (
|
|
127
|
+
environment in normalized_excluded_envs
|
|
128
|
+
or str(environment) in normalized_excluded_envs
|
|
129
|
+
):
|
|
110
130
|
logger.debug(
|
|
111
131
|
f"Skipping router module {module_name} due to ROUTER_EXCLUDED_ENVIRONMENTS restriction: {router_excluded_envs}"
|
|
112
132
|
)
|
|
@@ -126,7 +146,7 @@ def _is_router_included_by_environment(
|
|
|
126
146
|
f"ROUTER_ENVIRONMENTS in {module_name} must be a set/list/tuple, got {type(router_envs)}"
|
|
127
147
|
)
|
|
128
148
|
return True
|
|
129
|
-
normalized = set()
|
|
149
|
+
normalized: set[Environment | str] = set()
|
|
130
150
|
for e in router_envs:
|
|
131
151
|
try:
|
|
132
152
|
normalized.add(Environment(e) if not isinstance(e, Environment) else e)
|
|
@@ -163,7 +183,7 @@ def _build_include_kwargs(module: ModuleType, prefix: str, force_include: bool)
|
|
|
163
183
|
router_tag = getattr(module, "ROUTER_TAG", None)
|
|
164
184
|
include_in_schema = getattr(module, "INCLUDE_ROUTER_IN_SCHEMA", True)
|
|
165
185
|
|
|
166
|
-
include_kwargs = {"prefix": prefix}
|
|
186
|
+
include_kwargs: dict[str, Any] = {"prefix": prefix}
|
|
167
187
|
if router_prefix:
|
|
168
188
|
include_kwargs["prefix"] = prefix.rstrip("/") + router_prefix
|
|
169
189
|
if router_tag:
|
|
@@ -230,18 +250,24 @@ def register_all_routers(
|
|
|
230
250
|
"""
|
|
231
251
|
if base_package is None:
|
|
232
252
|
if __package__ is None:
|
|
233
|
-
raise RuntimeError(
|
|
253
|
+
raise RuntimeError(
|
|
254
|
+
"Cannot derive base_package; please pass base_package explicitly."
|
|
255
|
+
)
|
|
234
256
|
base_package = __package__
|
|
235
257
|
|
|
236
258
|
package_module = _validate_base_package(base_package)
|
|
237
259
|
environment = _normalize_environment(environment)
|
|
238
|
-
force_include = _should_force_include_in_schema(
|
|
260
|
+
force_include = _should_force_include_in_schema(
|
|
261
|
+
environment, force_include_in_schema
|
|
262
|
+
)
|
|
239
263
|
|
|
240
264
|
for _, module_name, _ in pkgutil.walk_packages(
|
|
241
265
|
package_module.__path__, prefix=f"{base_package}."
|
|
242
266
|
):
|
|
243
267
|
if _should_skip_module(module_name):
|
|
244
|
-
logger.debug(
|
|
268
|
+
logger.debug(
|
|
269
|
+
"Skipping router module due to exclusion/private: %s", module_name
|
|
270
|
+
)
|
|
245
271
|
continue
|
|
246
272
|
|
|
247
273
|
try:
|
|
@@ -250,4 +276,6 @@ def register_all_routers(
|
|
|
250
276
|
logger.exception("Failed to import router module %s: %s", module_name, exc)
|
|
251
277
|
continue
|
|
252
278
|
|
|
253
|
-
_process_router_module(
|
|
279
|
+
_process_router_module(
|
|
280
|
+
app, module, module_name, prefix, environment, force_include
|
|
281
|
+
)
|
|
@@ -14,6 +14,7 @@ router = public_router(tags=["Health Check"])
|
|
|
14
14
|
PING_PATH,
|
|
15
15
|
status_code=status.HTTP_200_OK,
|
|
16
16
|
description="Operation to check if the service is up and running",
|
|
17
|
+
operation_id="health_ping_get",
|
|
17
18
|
)
|
|
18
19
|
def ping():
|
|
19
20
|
logging.info("Health check: /ping endpoint accessed. Service is responsive.")
|