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