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/api/fastapi/db/http.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
4
5
|
|
|
5
6
|
from fastapi import Query
|
|
6
7
|
from pydantic import BaseModel
|
|
@@ -21,37 +22,37 @@ def dep_limit_offset(
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class OrderParams(BaseModel):
|
|
24
|
-
order_by:
|
|
25
|
+
order_by: str | None = None
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def dep_order(
|
|
28
|
-
order_by:
|
|
29
|
+
order_by: str | None = Query(None, description="Comma-separated fields; '-' for DESC"),
|
|
29
30
|
) -> OrderParams:
|
|
30
31
|
return OrderParams(order_by=order_by)
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class SearchParams(BaseModel):
|
|
34
|
-
q:
|
|
35
|
-
fields:
|
|
35
|
+
q: str | None = None
|
|
36
|
+
fields: str | None = None
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def dep_search(
|
|
39
|
-
q:
|
|
40
|
-
fields:
|
|
40
|
+
q: str | None = Query(None, description="Search query"),
|
|
41
|
+
fields: str | None = Query(None, description="Comma-separated list of fields"),
|
|
41
42
|
) -> SearchParams:
|
|
42
43
|
return SearchParams(q=q, fields=fields)
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class Page(BaseModel, Generic[T]):
|
|
46
47
|
total: int
|
|
47
|
-
items:
|
|
48
|
+
items: list[T]
|
|
48
49
|
limit: int
|
|
49
50
|
offset: int
|
|
50
51
|
|
|
51
52
|
@classmethod
|
|
52
53
|
def from_items(
|
|
53
54
|
cls, *, total: int, items: Sequence[T] | Iterable[T], limit: int, offset: int
|
|
54
|
-
) ->
|
|
55
|
+
) -> Page[T]:
|
|
55
56
|
return cls(total=total, items=list(items), limit=limit, offset=offset)
|
|
56
57
|
|
|
57
58
|
|
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
|
|
9
|
+
from svc_infra.db.nosql.resource import NoSqlResource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _missing_mongo_dependency() -> ModuleNotFoundError:
|
|
13
|
+
return ModuleNotFoundError(
|
|
14
|
+
"MongoDB support is an optional dependency. Install pymongo (and motor) to use "
|
|
15
|
+
"Mongo helpers like add_mongo_db/add_mongo_health/add_mongo_resources."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from .mongo.add import add_mongo_db, add_mongo_health, add_mongo_resources
|
|
21
|
+
except ModuleNotFoundError as exc:
|
|
22
|
+
mongo_import_error = exc
|
|
23
|
+
|
|
24
|
+
# NOTE: pymongo provides `bson`, which can be absent in minimal installs/CI.
|
|
25
|
+
# We keep imports working for non-mongo users/tests by providing stubs.
|
|
26
|
+
def add_mongo_db(app: FastAPI, *, dsn_env: str = "MONGO_URL") -> None:
|
|
27
|
+
raise _missing_mongo_dependency() from mongo_import_error
|
|
28
|
+
|
|
29
|
+
def add_mongo_health(
|
|
30
|
+
app: FastAPI,
|
|
31
|
+
*,
|
|
32
|
+
prefix: str = "/_mongo/health",
|
|
33
|
+
include_in_schema: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
raise _missing_mongo_dependency() from mongo_import_error
|
|
36
|
+
|
|
37
|
+
def add_mongo_resources(app: FastAPI, resources: Sequence[NoSqlResource]) -> None:
|
|
38
|
+
raise _missing_mongo_dependency() from mongo_import_error
|
|
39
|
+
|
|
2
40
|
|
|
3
41
|
__all__ = [
|
|
4
42
|
# MongoDB
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Sequence
|
|
6
6
|
|
|
7
7
|
from bson import ObjectId
|
|
8
8
|
from fastapi import FastAPI
|
|
@@ -24,7 +24,8 @@ from .health import make_mongo_health_router
|
|
|
24
24
|
def add_mongo_db_with_url(app: FastAPI, url: str, db_name: str) -> None:
|
|
25
25
|
@asynccontextmanager
|
|
26
26
|
async def lifespan(_app: FastAPI):
|
|
27
|
-
|
|
27
|
+
# MongoSettings expects url as AnyUrl, which can be constructed from str via Pydantic
|
|
28
|
+
await init_mongo(MongoSettings(url=url, db_name=db_name)) # type: ignore[arg-type] # Pydantic coerces str to AnyUrl
|
|
28
29
|
try:
|
|
29
30
|
expected = get_mongo_dbname_from_env(required=False)
|
|
30
31
|
db = await acquire_db()
|
|
@@ -38,8 +39,8 @@ def add_mongo_db_with_url(app: FastAPI, url: str, db_name: str) -> None:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def add_mongo_db(app: FastAPI, *, dsn_env: str = "MONGO_URL") -> None:
|
|
41
|
-
@
|
|
42
|
-
async def
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def lifespan(_app: FastAPI):
|
|
43
44
|
if not os.getenv(dsn_env):
|
|
44
45
|
raise RuntimeError(f"Missing environment variable {dsn_env} for Mongo URL")
|
|
45
46
|
await init_mongo()
|
|
@@ -47,10 +48,12 @@ def add_mongo_db(app: FastAPI, *, dsn_env: str = "MONGO_URL") -> None:
|
|
|
47
48
|
db = await acquire_db()
|
|
48
49
|
if expected and db.name != expected:
|
|
49
50
|
raise RuntimeError(f"Connected to Mongo DB '{db.name}', expected '{expected}'.")
|
|
51
|
+
try:
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
await close_mongo()
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
async def _shutdown() -> None:
|
|
53
|
-
await close_mongo()
|
|
56
|
+
app.router.lifespan_context = lifespan
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
def add_mongo_health(
|
|
@@ -62,46 +65,48 @@ def add_mongo_health(
|
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
def add_mongo_resources(app: FastAPI, resources: Sequence[NoSqlResource]) -> None:
|
|
65
|
-
for
|
|
68
|
+
for resource in resources:
|
|
66
69
|
repo = NoSqlRepository(
|
|
67
|
-
collection_name=
|
|
68
|
-
id_field=
|
|
69
|
-
soft_delete=
|
|
70
|
-
soft_delete_field=
|
|
71
|
-
soft_delete_flag_field=
|
|
70
|
+
collection_name=resource.resolved_collection(),
|
|
71
|
+
id_field=resource.id_field,
|
|
72
|
+
soft_delete=resource.soft_delete,
|
|
73
|
+
soft_delete_field=resource.soft_delete_field,
|
|
74
|
+
soft_delete_flag_field=resource.soft_delete_flag_field,
|
|
72
75
|
)
|
|
73
|
-
svc =
|
|
76
|
+
svc = resource.service_factory(repo) if resource.service_factory else NoSqlService(repo)
|
|
74
77
|
|
|
75
|
-
if
|
|
76
|
-
Read, Create, Update =
|
|
77
|
-
|
|
78
|
+
if resource.read_schema and resource.create_schema and resource.update_schema:
|
|
79
|
+
Read, Create, Update = (
|
|
80
|
+
resource.read_schema,
|
|
81
|
+
resource.create_schema,
|
|
82
|
+
resource.update_schema,
|
|
83
|
+
)
|
|
84
|
+
elif resource.document_model is not None:
|
|
78
85
|
# CRITICAL: teach Pydantic to dump ObjectId/PyObjectId
|
|
79
86
|
Read, Create, Update = make_document_crud_schemas(
|
|
80
|
-
|
|
81
|
-
create_exclude=
|
|
82
|
-
read_name=
|
|
83
|
-
create_name=
|
|
84
|
-
update_name=
|
|
85
|
-
read_exclude=
|
|
86
|
-
update_exclude=
|
|
87
|
+
resource.document_model,
|
|
88
|
+
create_exclude=resource.create_exclude,
|
|
89
|
+
read_name=resource.read_name,
|
|
90
|
+
create_name=resource.create_name,
|
|
91
|
+
update_name=resource.update_name,
|
|
92
|
+
read_exclude=resource.read_exclude,
|
|
93
|
+
update_exclude=resource.update_exclude,
|
|
87
94
|
json_encoders={ObjectId: str, PyObjectId: str},
|
|
88
95
|
)
|
|
89
96
|
else:
|
|
90
97
|
raise RuntimeError(
|
|
91
|
-
f"Resource for collection '{
|
|
98
|
+
f"Resource for collection '{resource.collection}' requires either explicit schemas "
|
|
92
99
|
f"(read/create/update) or a 'document_model' to derive them."
|
|
93
100
|
)
|
|
94
101
|
|
|
95
102
|
router = make_crud_router_plus_mongo(
|
|
96
|
-
collection=r.resolved_collection(),
|
|
97
|
-
repo=repo,
|
|
98
103
|
service=svc,
|
|
99
104
|
read_schema=Read,
|
|
100
105
|
create_schema=Create,
|
|
101
106
|
update_schema=Update,
|
|
102
|
-
prefix=
|
|
103
|
-
tags=
|
|
104
|
-
search_fields=
|
|
107
|
+
prefix=resource.prefix,
|
|
108
|
+
tags=resource.tags,
|
|
109
|
+
search_fields=resource.search_fields,
|
|
105
110
|
default_ordering=None,
|
|
106
111
|
allowed_order_fields=None,
|
|
107
112
|
)
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Annotated, Any, cast
|
|
2
3
|
|
|
3
4
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
8
|
+
|
|
9
|
+
HAS_MOTOR = True
|
|
10
|
+
except ImportError: # pragma: no cover
|
|
11
|
+
HAS_MOTOR = False
|
|
12
|
+
AsyncIOMotorDatabase = Any # type: ignore[assignment, misc]
|
|
5
13
|
|
|
6
14
|
from svc_infra.api.fastapi.db.http import (
|
|
7
15
|
LimitOffsetParams,
|
|
@@ -20,7 +28,7 @@ DBDep = Annotated[AsyncIOMotorDatabase, Depends(acquire_db)]
|
|
|
20
28
|
|
|
21
29
|
|
|
22
30
|
def _parse_sort(
|
|
23
|
-
order_spec:
|
|
31
|
+
order_spec: str | None, allowed_order_fields: list[str] | None
|
|
24
32
|
) -> list[tuple[str, int]]:
|
|
25
33
|
if not order_spec:
|
|
26
34
|
return []
|
|
@@ -36,16 +44,19 @@ def _parse_sort(
|
|
|
36
44
|
def make_crud_router_plus_mongo(
|
|
37
45
|
*,
|
|
38
46
|
service: NoSqlService,
|
|
39
|
-
read_schema:
|
|
40
|
-
create_schema:
|
|
41
|
-
update_schema:
|
|
47
|
+
read_schema: type[Any],
|
|
48
|
+
create_schema: type[Any],
|
|
49
|
+
update_schema: type[Any],
|
|
42
50
|
prefix: str,
|
|
43
51
|
tags: list[str] | None = None,
|
|
44
|
-
search_fields:
|
|
45
|
-
default_ordering:
|
|
46
|
-
allowed_order_fields:
|
|
52
|
+
search_fields: Sequence[str] | None = None,
|
|
53
|
+
default_ordering: str | None = None,
|
|
54
|
+
allowed_order_fields: list[str] | None = None,
|
|
47
55
|
mount_under_db_prefix: bool = True,
|
|
48
56
|
) -> APIRouter:
|
|
57
|
+
read_model = cast("Any", read_schema)
|
|
58
|
+
page_model = cast("Any", Page[read_schema]) # type: ignore[valid-type]
|
|
59
|
+
|
|
49
60
|
router_prefix = ("/_mongo" + prefix) if mount_under_db_prefix else prefix
|
|
50
61
|
router = public_router(
|
|
51
62
|
prefix=router_prefix,
|
|
@@ -56,7 +67,7 @@ def make_crud_router_plus_mongo(
|
|
|
56
67
|
# LIST
|
|
57
68
|
@router.get(
|
|
58
69
|
"",
|
|
59
|
-
response_model=
|
|
70
|
+
response_model=page_model,
|
|
60
71
|
description=f"List items in {prefix} collection",
|
|
61
72
|
)
|
|
62
73
|
async def list_items(
|
|
@@ -68,20 +79,23 @@ def make_crud_router_plus_mongo(
|
|
|
68
79
|
sort = _parse_sort(op.order_by or default_ordering, allowed_order_fields)
|
|
69
80
|
if sp.q and search_fields:
|
|
70
81
|
items = await service.search(
|
|
71
|
-
db,
|
|
82
|
+
db,
|
|
83
|
+
q=sp.q,
|
|
84
|
+
fields=search_fields,
|
|
85
|
+
limit=lp.limit,
|
|
86
|
+
offset=lp.offset,
|
|
87
|
+
sort=sort,
|
|
72
88
|
)
|
|
73
89
|
total = await service.count_filtered(db, q=sp.q, fields=search_fields)
|
|
74
90
|
else:
|
|
75
91
|
items = await service.list(db, limit=lp.limit, offset=lp.offset, sort=sort)
|
|
76
92
|
total = await service.count(db)
|
|
77
|
-
return Page[
|
|
78
|
-
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
79
|
-
)
|
|
93
|
+
return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
|
|
80
94
|
|
|
81
95
|
# GET by id
|
|
82
96
|
@router.get(
|
|
83
97
|
"/{item_id}",
|
|
84
|
-
response_model=
|
|
98
|
+
response_model=read_model,
|
|
85
99
|
description=f"Get item from {prefix} collection",
|
|
86
100
|
)
|
|
87
101
|
async def get_item(db: DBDep, item_id: Any):
|
|
@@ -93,22 +107,26 @@ def make_crud_router_plus_mongo(
|
|
|
93
107
|
# CREATE
|
|
94
108
|
@router.post(
|
|
95
109
|
"",
|
|
96
|
-
response_model=
|
|
110
|
+
response_model=read_model,
|
|
97
111
|
status_code=201,
|
|
98
112
|
description=f"Create item in {prefix} collection",
|
|
99
113
|
)
|
|
100
|
-
async def create_item(db: DBDep, payload: create_schema = Body(...)):
|
|
101
|
-
data = payload.model_dump(exclude_unset=True)
|
|
114
|
+
async def create_item(db: DBDep, payload: create_schema = Body(...)): # type: ignore[valid-type]
|
|
115
|
+
data = cast("Any", payload).model_dump(exclude_unset=True)
|
|
102
116
|
return await service.create(db, data)
|
|
103
117
|
|
|
104
118
|
# UPDATE
|
|
105
119
|
@router.patch(
|
|
106
120
|
"/{item_id}",
|
|
107
|
-
response_model=
|
|
121
|
+
response_model=read_model,
|
|
108
122
|
description=f"Update item in {prefix} collection",
|
|
109
123
|
)
|
|
110
|
-
async def update_item(
|
|
111
|
-
|
|
124
|
+
async def update_item(
|
|
125
|
+
db: DBDep,
|
|
126
|
+
item_id: Any,
|
|
127
|
+
payload: update_schema = Body(...), # type: ignore[valid-type]
|
|
128
|
+
):
|
|
129
|
+
data = cast("Any", payload).model_dump(exclude_unset=True)
|
|
112
130
|
row = await service.update(db, item_id, data)
|
|
113
131
|
if not row:
|
|
114
132
|
raise HTTPException(404, "Not found")
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from svc_infra.api.fastapi.db.sql.add import
|
|
1
|
+
from svc_infra.api.fastapi.db.sql.add import (
|
|
2
|
+
add_sql_db,
|
|
3
|
+
add_sql_health,
|
|
4
|
+
add_sql_resources,
|
|
5
|
+
)
|
|
2
6
|
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
3
7
|
|
|
4
8
|
__all__ = [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Optional, Sequence
|
|
6
6
|
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ from svc_infra.db.sql.management import make_crud_schemas
|
|
|
10
10
|
from svc_infra.db.sql.repository import SqlRepository
|
|
11
11
|
from svc_infra.db.sql.resource import SqlResource
|
|
12
12
|
|
|
13
|
-
from .crud_router import make_crud_router_plus_sql
|
|
13
|
+
from .crud_router import make_crud_router_plus_sql, make_tenant_crud_router_plus_sql
|
|
14
14
|
from .health import _make_db_health_router
|
|
15
15
|
from .session import dispose_session, initialize_session
|
|
16
16
|
|
|
@@ -37,46 +37,83 @@ def add_sql_resources(app: FastAPI, resources: Sequence[SqlResource]) -> None:
|
|
|
37
37
|
update_name=r.update_name,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
if r.tenant_field:
|
|
41
|
+
# wrap service factory/instance through tenant router
|
|
42
|
+
def _factory():
|
|
43
|
+
return svc
|
|
44
|
+
|
|
45
|
+
router = make_tenant_crud_router_plus_sql(
|
|
46
|
+
model=r.model,
|
|
47
|
+
service_factory=_factory,
|
|
48
|
+
read_schema=Read,
|
|
49
|
+
create_schema=Create,
|
|
50
|
+
update_schema=Update,
|
|
51
|
+
prefix=r.prefix,
|
|
52
|
+
tenant_field=r.tenant_field,
|
|
53
|
+
tags=r.tags,
|
|
54
|
+
search_fields=r.search_fields,
|
|
55
|
+
default_ordering=r.ordering_default,
|
|
56
|
+
allowed_order_fields=r.allowed_order_fields,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
router = make_crud_router_plus_sql(
|
|
60
|
+
model=r.model,
|
|
61
|
+
service=svc,
|
|
62
|
+
read_schema=Read,
|
|
63
|
+
create_schema=Create,
|
|
64
|
+
update_schema=Update,
|
|
65
|
+
prefix=r.prefix,
|
|
66
|
+
tags=r.tags,
|
|
67
|
+
search_fields=r.search_fields,
|
|
68
|
+
default_ordering=r.ordering_default,
|
|
69
|
+
allowed_order_fields=r.allowed_order_fields,
|
|
70
|
+
)
|
|
52
71
|
app.include_router(router)
|
|
53
72
|
|
|
54
73
|
|
|
55
|
-
def add_sql_db(app: FastAPI, *, url:
|
|
56
|
-
"""Configure DB lifecycle for the app (either explicit URL or from env).
|
|
74
|
+
def add_sql_db(app: FastAPI, *, url: str | None = None, dsn_env: str = "SQL_URL") -> None:
|
|
75
|
+
"""Configure DB lifecycle for the app (either explicit URL or from env).
|
|
76
|
+
|
|
77
|
+
This preserves any existing lifespan context (like user-defined lifespans)
|
|
78
|
+
and wraps it with the database session initialization/cleanup.
|
|
79
|
+
"""
|
|
80
|
+
# Preserve existing lifespan to wrap it
|
|
81
|
+
existing_lifespan = getattr(app.router, "lifespan_context", None)
|
|
82
|
+
|
|
57
83
|
if url:
|
|
58
84
|
|
|
59
85
|
@asynccontextmanager
|
|
60
|
-
async def
|
|
86
|
+
async def lifespan_with_url(_app: FastAPI):
|
|
61
87
|
initialize_session(url)
|
|
62
88
|
try:
|
|
63
|
-
|
|
89
|
+
if existing_lifespan is not None:
|
|
90
|
+
async with existing_lifespan(_app):
|
|
91
|
+
yield
|
|
92
|
+
else:
|
|
93
|
+
yield
|
|
64
94
|
finally:
|
|
65
95
|
await dispose_session()
|
|
66
96
|
|
|
67
|
-
app.router.lifespan_context =
|
|
97
|
+
app.router.lifespan_context = lifespan_with_url
|
|
68
98
|
return
|
|
69
99
|
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
# Use lifespan context manager instead of deprecated on_event
|
|
101
|
+
@asynccontextmanager
|
|
102
|
+
async def lifespan_from_env(_app: FastAPI):
|
|
72
103
|
env_url = os.getenv(dsn_env)
|
|
73
104
|
if not env_url:
|
|
74
105
|
raise RuntimeError(f"Missing environment variable {dsn_env} for database URL")
|
|
75
106
|
initialize_session(env_url)
|
|
107
|
+
try:
|
|
108
|
+
if existing_lifespan is not None:
|
|
109
|
+
async with existing_lifespan(_app):
|
|
110
|
+
yield
|
|
111
|
+
else:
|
|
112
|
+
yield
|
|
113
|
+
finally:
|
|
114
|
+
await dispose_session()
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
async def _shutdown() -> None: # noqa: ANN202
|
|
79
|
-
await dispose_session()
|
|
116
|
+
app.router.lifespan_context = lifespan_from_env
|
|
80
117
|
|
|
81
118
|
|
|
82
119
|
def add_sql_health(
|
|
@@ -89,7 +126,7 @@ def setup_sql(
|
|
|
89
126
|
app: FastAPI,
|
|
90
127
|
resources: Sequence[SqlResource],
|
|
91
128
|
*,
|
|
92
|
-
url:
|
|
129
|
+
url: str | None = None,
|
|
93
130
|
dsn_env: str = "SQL_URL",
|
|
94
131
|
include_health: bool = True,
|
|
95
132
|
health_prefix: str = "/_sql/health",
|