svc-infra 0.1.706__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/apf_payments/models.py +47 -108
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +42 -100
- svc_infra/apf_payments/provider/base.py +10 -26
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +63 -135
- svc_infra/apf_payments/schemas.py +82 -90
- svc_infra/apf_payments/service.py +40 -86
- svc_infra/apf_payments/settings.py +10 -13
- svc_infra/api/__init__.py +13 -13
- svc_infra/api/fastapi/__init__.py +19 -0
- svc_infra/api/fastapi/admin/add.py +13 -18
- svc_infra/api/fastapi/apf_payments/router.py +47 -84
- svc_infra/api/fastapi/apf_payments/setup.py +7 -13
- svc_infra/api/fastapi/auth/__init__.py +1 -1
- svc_infra/api/fastapi/auth/_cookies.py +3 -9
- svc_infra/api/fastapi/auth/add.py +4 -8
- svc_infra/api/fastapi/auth/gaurd.py +9 -26
- svc_infra/api/fastapi/auth/mfa/models.py +4 -7
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +3 -3
- svc_infra/api/fastapi/auth/mfa/router.py +9 -15
- svc_infra/api/fastapi/auth/mfa/security.py +3 -5
- svc_infra/api/fastapi/auth/mfa/utils.py +3 -2
- svc_infra/api/fastapi/auth/mfa/verify.py +2 -9
- svc_infra/api/fastapi/auth/providers.py +4 -6
- svc_infra/api/fastapi/auth/routers/apikey_router.py +16 -18
- svc_infra/api/fastapi/auth/routers/oauth_router.py +37 -85
- svc_infra/api/fastapi/auth/routers/session_router.py +3 -6
- svc_infra/api/fastapi/auth/security.py +17 -28
- svc_infra/api/fastapi/auth/sender.py +1 -3
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +6 -7
- svc_infra/api/fastapi/auth/ws_security.py +2 -2
- svc_infra/api/fastapi/billing/router.py +6 -8
- svc_infra/api/fastapi/db/http.py +10 -11
- svc_infra/api/fastapi/db/nosql/mongo/add.py +5 -15
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +14 -15
- svc_infra/api/fastapi/db/sql/add.py +6 -14
- svc_infra/api/fastapi/db/sql/crud_router.py +27 -40
- svc_infra/api/fastapi/db/sql/health.py +1 -3
- svc_infra/api/fastapi/db/sql/session.py +4 -5
- svc_infra/api/fastapi/db/sql/users.py +8 -11
- svc_infra/api/fastapi/dependencies/ratelimit.py +4 -6
- svc_infra/api/fastapi/docs/add.py +13 -23
- svc_infra/api/fastapi/docs/landing.py +6 -8
- svc_infra/api/fastapi/docs/scoped.py +34 -42
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +12 -21
- svc_infra/api/fastapi/dual/router.py +14 -31
- svc_infra/api/fastapi/ease.py +57 -13
- svc_infra/api/fastapi/http/conditional.py +3 -5
- svc_infra/api/fastapi/middleware/errors/catchall.py +2 -6
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -4
- svc_infra/api/fastapi/middleware/errors/handlers.py +12 -18
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +4 -13
- svc_infra/api/fastapi/middleware/idempotency.py +11 -16
- svc_infra/api/fastapi/middleware/idempotency_store.py +14 -14
- svc_infra/api/fastapi/middleware/optimistic_lock.py +5 -8
- svc_infra/api/fastapi/middleware/ratelimit.py +8 -8
- svc_infra/api/fastapi/middleware/ratelimit_store.py +7 -8
- svc_infra/api/fastapi/middleware/request_id.py +1 -3
- svc_infra/api/fastapi/middleware/timeout.py +9 -10
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -6
- svc_infra/api/fastapi/openapi/conventions.py +4 -4
- svc_infra/api/fastapi/openapi/mutators.py +13 -31
- 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 -3
- svc_infra/api/fastapi/ops/add.py +7 -9
- svc_infra/api/fastapi/pagination.py +25 -37
- svc_infra/api/fastapi/routers/__init__.py +16 -38
- svc_infra/api/fastapi/setup.py +13 -31
- svc_infra/api/fastapi/tenancy/add.py +3 -2
- svc_infra/api/fastapi/tenancy/context.py +8 -7
- svc_infra/api/fastapi/versioned.py +3 -2
- svc_infra/app/env.py +5 -7
- svc_infra/app/logging/add.py +2 -1
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +3 -2
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +19 -2
- svc_infra/billing/async_service.py +27 -7
- svc_infra/billing/jobs.py +23 -33
- svc_infra/billing/models.py +21 -52
- svc_infra/billing/quotas.py +5 -7
- svc_infra/billing/schemas.py +4 -6
- svc_infra/cache/__init__.py +12 -5
- svc_infra/cache/add.py +6 -9
- svc_infra/cache/backend.py +6 -5
- svc_infra/cache/decorators.py +17 -28
- svc_infra/cache/keys.py +2 -2
- svc_infra/cache/recache.py +22 -35
- svc_infra/cache/resources.py +8 -16
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +5 -6
- svc_infra/cli/__init__.py +4 -12
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +11 -10
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +6 -9
- svc_infra/cli/cmds/db/ops_cmds.py +3 -6
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +24 -41
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +9 -17
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +10 -10
- svc_infra/cli/cmds/docs/docs_cmds.py +7 -10
- svc_infra/cli/cmds/dx/dx_cmds.py +5 -11
- svc_infra/cli/cmds/jobs/jobs_cmds.py +2 -7
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -7
- svc_infra/cli/cmds/sdk/sdk_cmds.py +5 -15
- svc_infra/cli/foundation/runner.py +6 -11
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +5 -5
- svc_infra/data/backup.py +8 -10
- svc_infra/data/erasure.py +3 -2
- svc_infra/data/fixtures.py +3 -3
- svc_infra/data/retention.py +8 -13
- svc_infra/db/crud_schema.py +9 -8
- svc_infra/db/nosql/__init__.py +0 -1
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +7 -14
- svc_infra/db/nosql/indexes.py +11 -10
- svc_infra/db/nosql/management.py +3 -3
- svc_infra/db/nosql/mongo/client.py +3 -3
- svc_infra/db/nosql/mongo/settings.py +2 -6
- svc_infra/db/nosql/repository.py +27 -28
- svc_infra/db/nosql/resource.py +15 -20
- svc_infra/db/nosql/scaffold.py +13 -17
- svc_infra/db/nosql/service.py +3 -4
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/types.py +2 -6
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +14 -18
- svc_infra/db/outbox.py +15 -18
- svc_infra/db/sql/apikey.py +12 -21
- svc_infra/db/sql/authref.py +3 -7
- svc_infra/db/sql/constants.py +9 -9
- svc_infra/db/sql/core.py +11 -11
- svc_infra/db/sql/management.py +2 -6
- svc_infra/db/sql/repository.py +17 -24
- svc_infra/db/sql/resource.py +14 -13
- svc_infra/db/sql/scaffold.py +13 -17
- svc_infra/db/sql/service.py +7 -16
- svc_infra/db/sql/service_with_hooks.py +4 -3
- svc_infra/db/sql/tenant.py +6 -14
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +14 -19
- svc_infra/db/sql/utils.py +24 -53
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +8 -15
- svc_infra/documents/add.py +7 -8
- svc_infra/documents/ease.py +8 -8
- svc_infra/documents/models.py +3 -3
- svc_infra/documents/storage.py +11 -13
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +1 -3
- svc_infra/dx/changelog.py +2 -2
- svc_infra/dx/checks.py +1 -1
- svc_infra/health/__init__.py +15 -16
- svc_infra/http/client.py +10 -14
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +3 -5
- svc_infra/jobs/builtins/webhook_delivery.py +1 -3
- svc_infra/jobs/loader.py +4 -5
- svc_infra/jobs/queue.py +14 -24
- svc_infra/jobs/redis_queue.py +20 -34
- svc_infra/jobs/runner.py +7 -11
- svc_infra/jobs/scheduler.py +5 -5
- svc_infra/jobs/worker.py +1 -1
- svc_infra/loaders/base.py +5 -4
- svc_infra/loaders/github.py +1 -3
- svc_infra/loaders/url.py +3 -9
- svc_infra/logging/__init__.py +7 -6
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +2 -2
- svc_infra/obs/add.py +4 -3
- svc_infra/obs/cloud_dash.py +1 -1
- svc_infra/obs/metrics/__init__.py +3 -3
- svc_infra/obs/metrics/asgi.py +9 -14
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +5 -9
- svc_infra/obs/metrics/sqlalchemy.py +9 -12
- svc_infra/obs/metrics.py +3 -3
- svc_infra/obs/settings.py +2 -6
- 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 +5 -9
- svc_infra/security/audit.py +14 -17
- svc_infra/security/audit_service.py +9 -9
- svc_infra/security/hibp.py +3 -6
- svc_infra/security/jwt_rotation.py +7 -10
- svc_infra/security/lockout.py +12 -11
- svc_infra/security/models.py +37 -46
- svc_infra/security/oauth_models.py +8 -8
- svc_infra/security/org_invites.py +11 -13
- svc_infra/security/passwords.py +4 -6
- svc_infra/security/permissions.py +8 -7
- svc_infra/security/session.py +6 -7
- svc_infra/security/signed_cookies.py +9 -9
- svc_infra/storage/add.py +5 -8
- svc_infra/storage/backends/local.py +13 -21
- svc_infra/storage/backends/memory.py +4 -7
- svc_infra/storage/backends/s3.py +17 -36
- svc_infra/storage/base.py +2 -2
- svc_infra/storage/easy.py +4 -8
- svc_infra/storage/settings.py +16 -18
- svc_infra/testing/__init__.py +36 -39
- svc_infra/utils.py +169 -8
- svc_infra/webhooks/__init__.py +1 -1
- svc_infra/webhooks/add.py +17 -29
- svc_infra/webhooks/encryption.py +2 -2
- svc_infra/webhooks/fastapi.py +2 -4
- svc_infra/webhooks/router.py +3 -3
- svc_infra/webhooks/service.py +5 -6
- svc_infra/webhooks/signing.py +5 -5
- svc_infra/websocket/add.py +2 -3
- svc_infra/websocket/client.py +3 -2
- svc_infra/websocket/config.py +6 -18
- svc_infra/websocket/manager.py +9 -10
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/METADATA +11 -5
- svc_infra-1.1.0.dist-info/RECORD +364 -0
- svc_infra/billing/service.py +0 -123
- svc_infra-0.1.706.dist-info/RECORD +0 -357
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/LICENSE +0 -0
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -14,12 +13,8 @@ app = typer.Typer(help="Background jobs and scheduler commands")
|
|
|
14
13
|
|
|
15
14
|
@app.command("run")
|
|
16
15
|
def run(
|
|
17
|
-
poll_interval: float = typer.Option(
|
|
18
|
-
|
|
19
|
-
),
|
|
20
|
-
max_loops: Optional[int] = typer.Option(
|
|
21
|
-
None, help="Max loops before exit (for tests)"
|
|
22
|
-
),
|
|
16
|
+
poll_interval: float = typer.Option(0.5, help="Sleep seconds between loops when idle"),
|
|
17
|
+
max_loops: int | None = typer.Option(None, help="Max loops before exit (for tests)"),
|
|
23
18
|
):
|
|
24
19
|
"""Run scheduler ticks and process jobs in a simple loop."""
|
|
25
20
|
|
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import socket
|
|
5
5
|
import subprocess
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
8
9
|
from urllib.parse import urlparse
|
|
9
10
|
|
|
10
11
|
import typer
|
|
@@ -29,9 +30,7 @@ def _run(cmd: list[str], *, env: dict | None = None):
|
|
|
29
30
|
def _emit_local_stack(root: Path, metrics_url: str):
|
|
30
31
|
write(
|
|
31
32
|
root / "docker-compose.yml",
|
|
32
|
-
render_template(
|
|
33
|
-
"svc_infra.obs.providers.grafana.templates", "docker-compose.yml.tmpl", {}
|
|
34
|
-
),
|
|
33
|
+
render_template("svc_infra.obs.providers.grafana.templates", "docker-compose.yml.tmpl", {}),
|
|
35
34
|
)
|
|
36
35
|
p = urlparse(metrics_url)
|
|
37
36
|
prom_yml = render_template(
|
|
@@ -116,9 +115,7 @@ def up():
|
|
|
116
115
|
|
|
117
116
|
root = Path(".obs")
|
|
118
117
|
root.mkdir(exist_ok=True)
|
|
119
|
-
metrics_url = os.getenv(
|
|
120
|
-
"SVC_INFRA_METRICS_URL", "http://host.docker.internal:8000/metrics"
|
|
121
|
-
)
|
|
118
|
+
metrics_url = os.getenv("SVC_INFRA_METRICS_URL", "http://host.docker.internal:8000/metrics")
|
|
122
119
|
|
|
123
120
|
cloud_url = os.getenv("GRAFANA_CLOUD_URL", "").strip()
|
|
124
121
|
cloud_token = os.getenv("GRAFANA_CLOUD_TOKEN", "").strip()
|
|
@@ -4,9 +4,7 @@ import subprocess
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
-
app = typer.Typer(
|
|
8
|
-
no_args_is_help=True, add_completion=False, help="Generate SDKs from OpenAPI."
|
|
9
|
-
)
|
|
7
|
+
app = typer.Typer(no_args_is_help=True, add_completion=False, help="Generate SDKs from OpenAPI.")
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
def _echo(cmd: list[str]):
|
|
@@ -30,9 +28,7 @@ def _parse_bool(val: str | bool | None, default: bool = True) -> bool:
|
|
|
30
28
|
def sdk_ts(
|
|
31
29
|
openapi: str = typer.Argument(..., help="Path to OpenAPI JSON"),
|
|
32
30
|
outdir: str = typer.Option("sdk-ts", help="Output directory"),
|
|
33
|
-
dry_run: str = typer.Option(
|
|
34
|
-
"true", help="Print commands instead of running (true/false)"
|
|
35
|
-
),
|
|
31
|
+
dry_run: str = typer.Option("true", help="Print commands instead of running (true/false)"),
|
|
36
32
|
):
|
|
37
33
|
"""Generate a TypeScript SDK (openapi-typescript-codegen as default)."""
|
|
38
34
|
cmd = [
|
|
@@ -55,9 +51,7 @@ def sdk_py(
|
|
|
55
51
|
openapi: str = typer.Argument(..., help="Path to OpenAPI JSON"),
|
|
56
52
|
outdir: str = typer.Option("sdk-py", help="Output directory"),
|
|
57
53
|
package_name: str = typer.Option("client_sdk", help="Python package name"),
|
|
58
|
-
dry_run: str = typer.Option(
|
|
59
|
-
"true", help="Print commands instead of running (true/false)"
|
|
60
|
-
),
|
|
54
|
+
dry_run: str = typer.Option("true", help="Print commands instead of running (true/false)"),
|
|
61
55
|
):
|
|
62
56
|
"""Generate a Python SDK via openapi-generator-cli with "python" generator."""
|
|
63
57
|
cmd = [
|
|
@@ -84,12 +78,8 @@ def sdk_py(
|
|
|
84
78
|
@app.command("postman")
|
|
85
79
|
def sdk_postman(
|
|
86
80
|
openapi: str = typer.Argument(..., help="Path to OpenAPI JSON"),
|
|
87
|
-
out: str = typer.Option(
|
|
88
|
-
|
|
89
|
-
),
|
|
90
|
-
dry_run: str = typer.Option(
|
|
91
|
-
"true", help="Print commands instead of running (true/false)"
|
|
92
|
-
),
|
|
81
|
+
out: str = typer.Option("postman_collection.json", help="Output Postman collection"),
|
|
82
|
+
dry_run: str = typer.Option("true", help="Print commands instead of running (true/false)"),
|
|
93
83
|
):
|
|
94
84
|
"""Convert OpenAPI to a Postman collection via openapi-to-postmanv2."""
|
|
95
85
|
cmd = [
|
|
@@ -3,21 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import shutil
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import List, Optional
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def _has_poetry(root: Path) -> bool:
|
|
10
9
|
return (root / "pyproject.toml").exists() and bool(shutil.which("poetry"))
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
def candidate_cmds(root: Path, prog: str, argv:
|
|
12
|
+
def candidate_cmds(root: Path, prog: str, argv: list[str]) -> list[list[str]]:
|
|
14
13
|
"""
|
|
15
14
|
Return argv lists to try in order:
|
|
16
15
|
1) poetry run <prog> ...
|
|
17
16
|
2) <prog> ...
|
|
18
17
|
3) python -m <module> ...
|
|
19
18
|
"""
|
|
20
|
-
cmds:
|
|
19
|
+
cmds: list[list[str]] = []
|
|
21
20
|
if _has_poetry(root):
|
|
22
21
|
cmds.append(["poetry", "run", prog, *argv])
|
|
23
22
|
|
|
@@ -25,20 +24,18 @@ def candidate_cmds(root: Path, prog: str, argv: List[str]) -> List[List[str]]:
|
|
|
25
24
|
cmds.append([prog, *argv])
|
|
26
25
|
|
|
27
26
|
py = shutil.which("python3") or shutil.which("python") or "python"
|
|
28
|
-
module = (
|
|
29
|
-
prog.replace("-", "_") + ".cli_shim"
|
|
30
|
-
) # e.g., svc-infra -> svc_infra.cli_shim
|
|
27
|
+
module = prog.replace("-", "_") + ".cli_shim" # e.g., svc-infra -> svc_infra.cli_shim
|
|
31
28
|
cmds.append([py, "-m", module, *argv])
|
|
32
29
|
|
|
33
30
|
return cmds
|
|
34
31
|
|
|
35
32
|
|
|
36
|
-
async def run_from_root(root: Path, prog: str, argv:
|
|
33
|
+
async def run_from_root(root: Path, prog: str, argv: list[str]) -> str:
|
|
37
34
|
"""
|
|
38
35
|
cd to project root and run the first working candidate command.
|
|
39
36
|
Returns captured stdout+stderr text; raises on total failure.
|
|
40
37
|
"""
|
|
41
|
-
last_exc:
|
|
38
|
+
last_exc: BaseException | None = None
|
|
42
39
|
for cmd in candidate_cmds(root, prog, argv):
|
|
43
40
|
try:
|
|
44
41
|
proc = await asyncio.create_subprocess_exec(
|
|
@@ -56,6 +53,4 @@ async def run_from_root(root: Path, prog: str, argv: List[str]) -> str:
|
|
|
56
53
|
except Exception as e:
|
|
57
54
|
last_exc = e
|
|
58
55
|
continue
|
|
59
|
-
raise RuntimeError(
|
|
60
|
-
f"All runners failed in {root} for: {prog} {' '.join(argv)}"
|
|
61
|
-
) from last_exc
|
|
56
|
+
raise RuntimeError(f"All runners failed in {root} for: {prog} {' '.join(argv)}") from last_exc
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -12,7 +11,7 @@ from svc_infra.app.root import resolve_project_root
|
|
|
12
11
|
def pre_cli(app: typer.Typer) -> None:
|
|
13
12
|
@app.callback()
|
|
14
13
|
def _bootstrap(
|
|
15
|
-
env_file:
|
|
14
|
+
env_file: Path | None = typer.Option(
|
|
16
15
|
None,
|
|
17
16
|
"--env-file",
|
|
18
17
|
dir_okay=False,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Data lifecycle module for backup verification, retention, and GDPR erasure.
|
|
2
|
+
|
|
3
|
+
This module provides data lifecycle management primitives:
|
|
4
|
+
|
|
5
|
+
- **add_data_lifecycle**: FastAPI integration for auto-migration and fixtures
|
|
6
|
+
- **Backup**: Backup health verification utilities
|
|
7
|
+
- **Retention**: Data retention policies and purge execution
|
|
8
|
+
- **Erasure**: GDPR-compliant data erasure workflows
|
|
9
|
+
- **Fixtures**: Fixture loading with run-once semantics
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
from svc_infra.data import add_data_lifecycle, make_on_load_fixtures
|
|
14
|
+
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
|
|
17
|
+
# Enable auto-migration and fixture loading
|
|
18
|
+
add_data_lifecycle(
|
|
19
|
+
app,
|
|
20
|
+
auto_migrate=True,
|
|
21
|
+
on_load_fixtures=make_on_load_fixtures(load_seed_data),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Define retention policies
|
|
25
|
+
from svc_infra.data import RetentionPolicy, run_retention_purge
|
|
26
|
+
|
|
27
|
+
policies = [
|
|
28
|
+
RetentionPolicy(name="old_logs", model=AuditLog, older_than_days=90),
|
|
29
|
+
RetentionPolicy(name="expired_tokens", model=RefreshToken, older_than_days=30),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Run in a scheduled job
|
|
33
|
+
affected = await run_retention_purge(session, policies)
|
|
34
|
+
|
|
35
|
+
# GDPR erasure
|
|
36
|
+
from svc_infra.data import ErasurePlan, ErasureStep, run_erasure
|
|
37
|
+
|
|
38
|
+
plan = ErasurePlan(steps=[
|
|
39
|
+
ErasureStep(name="anonymize_user", run=anonymize_user_data),
|
|
40
|
+
ErasureStep(name="delete_logs", run=delete_user_logs),
|
|
41
|
+
])
|
|
42
|
+
await run_erasure(session, principal_id="user_123", plan=plan)
|
|
43
|
+
|
|
44
|
+
See Also:
|
|
45
|
+
- docs/data-lifecycle.md for detailed documentation
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
# FastAPI integration
|
|
51
|
+
from .add import add_data_lifecycle
|
|
52
|
+
|
|
53
|
+
# Backup verification
|
|
54
|
+
from .backup import BackupHealthReport, make_backup_verification_job, verify_backups
|
|
55
|
+
|
|
56
|
+
# GDPR erasure
|
|
57
|
+
from .erasure import ErasurePlan, ErasureStep, run_erasure
|
|
58
|
+
|
|
59
|
+
# Fixture loading
|
|
60
|
+
from .fixtures import make_on_load_fixtures, run_fixtures
|
|
61
|
+
|
|
62
|
+
# Retention policies
|
|
63
|
+
from .retention import RetentionPolicy, purge_policy, run_retention_purge
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
# FastAPI integration
|
|
67
|
+
"add_data_lifecycle",
|
|
68
|
+
# Backup
|
|
69
|
+
"BackupHealthReport",
|
|
70
|
+
"verify_backups",
|
|
71
|
+
"make_backup_verification_job",
|
|
72
|
+
# Retention
|
|
73
|
+
"RetentionPolicy",
|
|
74
|
+
"purge_policy",
|
|
75
|
+
"run_retention_purge",
|
|
76
|
+
# Erasure
|
|
77
|
+
"ErasureStep",
|
|
78
|
+
"ErasurePlan",
|
|
79
|
+
"run_erasure",
|
|
80
|
+
# Fixtures
|
|
81
|
+
"run_fixtures",
|
|
82
|
+
"make_on_load_fixtures",
|
|
83
|
+
]
|
svc_infra/data/add.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI
|
|
7
7
|
|
|
@@ -13,11 +13,11 @@ def add_data_lifecycle(
|
|
|
13
13
|
*,
|
|
14
14
|
auto_migrate: bool = True,
|
|
15
15
|
database_url: str | None = None,
|
|
16
|
-
discover_packages:
|
|
16
|
+
discover_packages: list[str] | None = None,
|
|
17
17
|
with_payments: bool | None = None,
|
|
18
|
-
on_load_fixtures:
|
|
19
|
-
retention_jobs:
|
|
20
|
-
erasure_job:
|
|
18
|
+
on_load_fixtures: Callable[[], None] | None = None,
|
|
19
|
+
retention_jobs: Iterable[Callable[[], None]] | None = None,
|
|
20
|
+
erasure_job: Callable[[str], None] | None = None,
|
|
21
21
|
) -> None:
|
|
22
22
|
"""
|
|
23
23
|
Wire data lifecycle conveniences:
|
svc_infra/data/backup.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from datetime import
|
|
5
|
-
from typing import Callable, Optional
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass(frozen=True)
|
|
9
9
|
class BackupHealthReport:
|
|
10
10
|
ok: bool
|
|
11
|
-
last_success:
|
|
12
|
-
retention_days:
|
|
11
|
+
last_success: datetime | None
|
|
12
|
+
retention_days: int | None
|
|
13
13
|
message: str = ""
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def verify_backups(
|
|
17
|
-
*, last_success:
|
|
17
|
+
*, last_success: datetime | None = None, retention_days: int | None = None
|
|
18
18
|
) -> BackupHealthReport:
|
|
19
19
|
"""Return a basic backup health report.
|
|
20
20
|
|
|
@@ -27,12 +27,10 @@ def verify_backups(
|
|
|
27
27
|
retention_days=retention_days,
|
|
28
28
|
message="no_backup_seen",
|
|
29
29
|
)
|
|
30
|
-
now = datetime.now(
|
|
30
|
+
now = datetime.now(UTC)
|
|
31
31
|
age_days = (now - last_success).total_seconds() / 86400.0
|
|
32
32
|
ok = retention_days is None or age_days <= max(1, retention_days)
|
|
33
|
-
return BackupHealthReport(
|
|
34
|
-
ok=ok, last_success=last_success, retention_days=retention_days
|
|
35
|
-
)
|
|
33
|
+
return BackupHealthReport(ok=ok, last_success=last_success, retention_days=retention_days)
|
|
36
34
|
|
|
37
35
|
|
|
38
36
|
__all__ = ["BackupHealthReport", "verify_backups"]
|
|
@@ -41,7 +39,7 @@ __all__ = ["BackupHealthReport", "verify_backups"]
|
|
|
41
39
|
def make_backup_verification_job(
|
|
42
40
|
checker: Callable[[], BackupHealthReport],
|
|
43
41
|
*,
|
|
44
|
-
on_report:
|
|
42
|
+
on_report: Callable[[BackupHealthReport], None] | None = None,
|
|
45
43
|
):
|
|
46
44
|
"""Return a callable suitable for scheduling in a job runner.
|
|
47
45
|
|
svc_infra/data/erasure.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Protocol
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class SqlSession(Protocol): # minimal protocol for tests/integration
|
|
@@ -25,7 +26,7 @@ async def run_erasure(
|
|
|
25
26
|
principal_id: str,
|
|
26
27
|
plan: ErasurePlan,
|
|
27
28
|
*,
|
|
28
|
-
on_audit:
|
|
29
|
+
on_audit: Callable[[str, dict[str, Any]], None] | None = None,
|
|
29
30
|
) -> int:
|
|
30
31
|
"""Run an erasure plan and optionally emit an audit event.
|
|
31
32
|
|
svc_infra/data/fixtures.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Awaitable, Callable, Iterable, Optional
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
async def run_fixtures(
|
|
9
9
|
loaders: Iterable[Callable[[], None | Awaitable[None]]],
|
|
10
10
|
*,
|
|
11
|
-
run_once_file:
|
|
11
|
+
run_once_file: str | None = None,
|
|
12
12
|
) -> None:
|
|
13
13
|
"""Run a sequence of fixture loaders (sync or async).
|
|
14
14
|
|
|
@@ -29,7 +29,7 @@ async def run_fixtures(
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def make_on_load_fixtures(
|
|
32
|
-
*loaders: Callable[[], None | Awaitable[None]], run_once_file:
|
|
32
|
+
*loaders: Callable[[], None | Awaitable[None]], run_once_file: str | None = None
|
|
33
33
|
) -> Callable[[], Awaitable[None]]:
|
|
34
34
|
"""Return an async callable suitable for add_data_lifecycle(on_load_fixtures=...)."""
|
|
35
35
|
|
svc_infra/data/retention.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from typing import Any, Protocol
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class SqlSession(Protocol): # minimal protocol for tests/integration
|
|
@@ -15,8 +16,8 @@ class RetentionPolicy:
|
|
|
15
16
|
name: str
|
|
16
17
|
model: Any # SQLAlchemy model or test double exposing columns
|
|
17
18
|
older_than_days: int
|
|
18
|
-
soft_delete_field:
|
|
19
|
-
extra_where:
|
|
19
|
+
soft_delete_field: str | None = "deleted_at"
|
|
20
|
+
extra_where: Sequence[Any] | None = None
|
|
20
21
|
hard_delete: bool = False
|
|
21
22
|
|
|
22
23
|
|
|
@@ -26,7 +27,7 @@ async def purge_policy(session: SqlSession, policy: RetentionPolicy) -> int:
|
|
|
26
27
|
If hard_delete is False and soft_delete_field exists on model, set timestamp; else DELETE.
|
|
27
28
|
Returns number of affected rows (best-effort; test doubles may return an int directly).
|
|
28
29
|
"""
|
|
29
|
-
cutoff = datetime.now(
|
|
30
|
+
cutoff = datetime.now(UTC) - timedelta(days=policy.older_than_days)
|
|
30
31
|
m = policy.model
|
|
31
32
|
where = list(policy.extra_where or [])
|
|
32
33
|
created_col = getattr(m, "created_at", None)
|
|
@@ -34,11 +35,7 @@ async def purge_policy(session: SqlSession, policy: RetentionPolicy) -> int:
|
|
|
34
35
|
where.append(created_col <= cutoff)
|
|
35
36
|
|
|
36
37
|
# Soft-delete path when available and requested
|
|
37
|
-
if (
|
|
38
|
-
not policy.hard_delete
|
|
39
|
-
and policy.soft_delete_field
|
|
40
|
-
and hasattr(m, policy.soft_delete_field)
|
|
41
|
-
):
|
|
38
|
+
if not policy.hard_delete and policy.soft_delete_field and hasattr(m, policy.soft_delete_field):
|
|
42
39
|
stmt = m.update().where(*where).values({policy.soft_delete_field: cutoff})
|
|
43
40
|
res = await session.execute(stmt)
|
|
44
41
|
return getattr(res, "rowcount", 0)
|
|
@@ -49,9 +46,7 @@ async def purge_policy(session: SqlSession, policy: RetentionPolicy) -> int:
|
|
|
49
46
|
return getattr(res, "rowcount", 0)
|
|
50
47
|
|
|
51
48
|
|
|
52
|
-
async def run_retention_purge(
|
|
53
|
-
session: SqlSession, policies: Iterable[RetentionPolicy]
|
|
54
|
-
) -> int:
|
|
49
|
+
async def run_retention_purge(session: SqlSession, policies: Iterable[RetentionPolicy]) -> int:
|
|
55
50
|
total = 0
|
|
56
51
|
for p in policies:
|
|
57
52
|
total += await purge_policy(session, p)
|
svc_infra/db/crud_schema.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, cast
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, ConfigDict, create_model
|
|
7
8
|
|
|
@@ -35,10 +36,10 @@ def _opt(t: type[Any]) -> tuple[Any, Any]:
|
|
|
35
36
|
def make_crud_schemas_from_specs(
|
|
36
37
|
*,
|
|
37
38
|
specs: Sequence[FieldSpec],
|
|
38
|
-
read_name:
|
|
39
|
-
create_name:
|
|
40
|
-
update_name:
|
|
41
|
-
json_encoders:
|
|
39
|
+
read_name: str | None,
|
|
40
|
+
create_name: str | None,
|
|
41
|
+
update_name: str | None,
|
|
42
|
+
json_encoders: dict[type[Any], Any] | None = None,
|
|
42
43
|
) -> tuple[type[BaseModel], type[BaseModel], type[BaseModel]]:
|
|
43
44
|
ann_read: dict[str, tuple[Any, Any]] = {}
|
|
44
45
|
ann_create: dict[str, tuple[Any, Any]] = {}
|
|
@@ -60,9 +61,9 @@ def make_crud_schemas_from_specs(
|
|
|
60
61
|
if not s.exclude_from_update:
|
|
61
62
|
ann_update[s.name] = _opt(s.typ)
|
|
62
63
|
|
|
63
|
-
Read = create_model(read_name or "Read", **cast(dict[str, Any], ann_read))
|
|
64
|
-
Create = create_model(create_name or "Create", **cast(dict[str, Any], ann_create))
|
|
65
|
-
Update = create_model(update_name or "Update", **cast(dict[str, Any], ann_update))
|
|
64
|
+
Read = create_model(read_name or "Read", **cast("dict[str, Any]", ann_read))
|
|
65
|
+
Create = create_model(create_name or "Create", **cast("dict[str, Any]", ann_create))
|
|
66
|
+
Update = create_model(update_name or "Update", **cast("dict[str, Any]", ann_update))
|
|
66
67
|
|
|
67
68
|
cfg = ConfigDict(from_attributes=True)
|
|
68
69
|
if json_encoders:
|
svc_infra/db/nosql/__init__.py
CHANGED
svc_infra/db/nosql/constants.py
CHANGED
svc_infra/db/nosql/core.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, cast
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
@@ -47,7 +48,7 @@ async def _apply_indexes(
|
|
|
47
48
|
if not indexes:
|
|
48
49
|
return []
|
|
49
50
|
result = await db[collection].create_indexes(list(indexes))
|
|
50
|
-
return cast(list[str], result)
|
|
51
|
+
return cast("list[str]", result)
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
# collection + doc used to "lock" the chosen DB name for this app
|
|
@@ -61,9 +62,7 @@ async def assert_db_locked(
|
|
|
61
62
|
registry = db.client.get_database(_REG_DB)
|
|
62
63
|
await registry[_REG_COLL].create_index("service_id", unique=True)
|
|
63
64
|
|
|
64
|
-
doc = await registry[_REG_COLL].find_one(
|
|
65
|
-
{"service_id": service_id}, projection={"db_name": 1}
|
|
66
|
-
)
|
|
65
|
+
doc = await registry[_REG_COLL].find_one({"service_id": service_id}, projection={"db_name": 1})
|
|
67
66
|
if doc is None:
|
|
68
67
|
await registry[_REG_COLL].insert_one(
|
|
69
68
|
{"service_id": service_id, "db_name": expected_db_name}
|
|
@@ -106,13 +105,9 @@ async def prepare_mongo(
|
|
|
106
105
|
|
|
107
106
|
expected_db = get_mongo_dbname_from_env(required=True)
|
|
108
107
|
if db.name != expected_db:
|
|
109
|
-
raise RuntimeError(
|
|
110
|
-
f"Connected to Mongo DB '{db.name}', but env says '{expected_db}'."
|
|
111
|
-
)
|
|
108
|
+
raise RuntimeError(f"Connected to Mongo DB '{db.name}', but env says '{expected_db}'.")
|
|
112
109
|
|
|
113
|
-
await assert_db_locked(
|
|
114
|
-
db, expected_db, service_id=service_id, allow_rebind=allow_rebind
|
|
115
|
-
)
|
|
110
|
+
await assert_db_locked(db, expected_db, service_id=service_id, allow_rebind=allow_rebind)
|
|
116
111
|
|
|
117
112
|
# collections
|
|
118
113
|
colls = [r.resolved_collection() for r in resources]
|
|
@@ -128,9 +123,7 @@ async def prepare_mongo(
|
|
|
128
123
|
names = await _apply_indexes(db, collection=coll, indexes=idx_models)
|
|
129
124
|
created_idx[coll] = names
|
|
130
125
|
|
|
131
|
-
return PrepareResult(
|
|
132
|
-
ok=True, created_collections=created_colls, created_indexes=created_idx
|
|
133
|
-
)
|
|
126
|
+
return PrepareResult(ok=True, created_collections=created_colls, created_indexes=created_idx)
|
|
134
127
|
|
|
135
128
|
|
|
136
129
|
def setup_and_prepare(
|
svc_infra/db/nosql/indexes.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from pymongo import ASCENDING, DESCENDING, IndexModel
|
|
6
7
|
from pymongo.collation import Collation
|
|
@@ -17,12 +18,12 @@ from pymongo.collation import Collation
|
|
|
17
18
|
# "sparse": True/False,
|
|
18
19
|
# "background": True/False # ignored by MongoDB 6+, harmless to pass
|
|
19
20
|
# }
|
|
20
|
-
Alias =
|
|
21
|
-
KeySpec =
|
|
21
|
+
Alias = dict[str, Any]
|
|
22
|
+
KeySpec = str | tuple[str, int]
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def _normalize_keys(keys: Iterable[KeySpec]) ->
|
|
25
|
-
out:
|
|
25
|
+
def _normalize_keys(keys: Iterable[KeySpec]) -> list[tuple[str, int]]:
|
|
26
|
+
out: list[tuple[str, int]] = []
|
|
26
27
|
for k in keys:
|
|
27
28
|
if isinstance(k, tuple):
|
|
28
29
|
field, dir_val = k
|
|
@@ -36,20 +37,20 @@ def _normalize_keys(keys: Iterable[KeySpec]) -> List[Tuple[str, int]]:
|
|
|
36
37
|
return out
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def _normalize_collation(c:
|
|
40
|
+
def _normalize_collation(c: dict[str, Any] | None) -> Collation | None:
|
|
40
41
|
if not c:
|
|
41
42
|
return None
|
|
42
43
|
# common short form e.g. {"locale":"en","strength":2}
|
|
43
44
|
return Collation(**c)
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def normalize_index(idx:
|
|
47
|
+
def normalize_index(idx: IndexModel | Alias) -> IndexModel:
|
|
47
48
|
if isinstance(idx, IndexModel):
|
|
48
49
|
return idx
|
|
49
50
|
keys = _normalize_keys(idx.get("keys", []))
|
|
50
51
|
if not keys:
|
|
51
52
|
raise ValueError("Index alias requires 'keys'.")
|
|
52
|
-
kwargs:
|
|
53
|
+
kwargs: dict[str, Any] = {}
|
|
53
54
|
for k in (
|
|
54
55
|
"name",
|
|
55
56
|
"unique",
|
|
@@ -67,8 +68,8 @@ def normalize_index(idx: Union[IndexModel, Alias]) -> IndexModel:
|
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def normalize_indexes(
|
|
70
|
-
indexes:
|
|
71
|
-
) ->
|
|
71
|
+
indexes: Iterable[IndexModel | Alias] | None,
|
|
72
|
+
) -> list[IndexModel]:
|
|
72
73
|
if not indexes:
|
|
73
74
|
return []
|
|
74
75
|
return [normalize_index(i) for i in indexes]
|
svc_infra/db/nosql/management.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Optional,
|
|
3
|
+
from typing import Any, Optional, Union, get_args, get_origin
|
|
4
4
|
|
|
5
5
|
from bson import ObjectId
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -24,7 +24,7 @@ def _unwrap_union(annotation: Any) -> set[type]:
|
|
|
24
24
|
"""
|
|
25
25
|
origin = get_origin(annotation)
|
|
26
26
|
if origin is Union:
|
|
27
|
-
return {t for t in get_args(annotation) if t is not type(None)}
|
|
27
|
+
return {t for t in get_args(annotation) if t is not type(None)}
|
|
28
28
|
return {annotation} if annotation is not None else set()
|
|
29
29
|
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ def _is_objectid_like(annotation: Any) -> bool:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def make_document_crud_schemas(
|
|
40
|
-
document_model:
|
|
40
|
+
document_model: type[BaseModel],
|
|
41
41
|
*,
|
|
42
42
|
create_exclude: tuple[str, ...] = ("_id",),
|
|
43
43
|
read_name: str | None = None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
6
|
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
@@ -13,8 +13,8 @@ except ImportError: # pragma: no cover
|
|
|
13
13
|
|
|
14
14
|
from .settings import MongoSettings
|
|
15
15
|
|
|
16
|
-
_client:
|
|
17
|
-
_db:
|
|
16
|
+
_client: AsyncIOMotorClient | None = None
|
|
17
|
+
_db: AsyncIOMotorDatabase | None = None
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _require_motor() -> None:
|
|
@@ -6,12 +6,8 @@ from pydantic import AnyUrl, BaseModel, Field
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class MongoSettings(BaseModel):
|
|
9
|
-
url: AnyUrl = Field(
|
|
10
|
-
default_factory=lambda: os.getenv("MONGO_URL", "mongodb://localhost:27017")
|
|
11
|
-
) # type: ignore[assignment]
|
|
9
|
+
url: AnyUrl = Field(default_factory=lambda: os.getenv("MONGO_URL", "mongodb://localhost:27017")) # type: ignore[assignment]
|
|
12
10
|
db_name: str = Field(default_factory=lambda: os.getenv("MONGO_DB", ""))
|
|
13
|
-
appname: str = Field(
|
|
14
|
-
default_factory=lambda: os.getenv("MONGO_APPNAME", "svc-infra")
|
|
15
|
-
)
|
|
11
|
+
appname: str = Field(default_factory=lambda: os.getenv("MONGO_APPNAME", "svc-infra"))
|
|
16
12
|
min_pool_size: int = int(os.getenv("MONGO_MIN_POOL", "0"))
|
|
17
13
|
max_pool_size: int = int(os.getenv("MONGO_MAX_POOL", "100"))
|