svc-infra 0.1.601__py3-none-any.whl → 0.1.602__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.

@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from fastapi import FastAPI
8
+
9
+
10
+ def add_docs(
11
+ app: FastAPI,
12
+ *,
13
+ redoc_url: str = "/redoc",
14
+ swagger_url: str = "/docs",
15
+ openapi_url: str = "/openapi.json",
16
+ export_openapi_to: Optional[str] = None,
17
+ ) -> None:
18
+ """Enable docs endpoints and optionally export OpenAPI schema to disk on startup."""
19
+ # Configure FastAPI docs URLs
20
+ app.docs_url = swagger_url
21
+ app.redoc_url = redoc_url
22
+ app.openapi_url = openapi_url
23
+
24
+ if export_openapi_to:
25
+ export_path = Path(export_openapi_to)
26
+
27
+ @app.on_event("startup")
28
+ async def _export_spec() -> None: # noqa: ANN202
29
+ spec = app.openapi()
30
+ export_path.parent.mkdir(parents=True, exist_ok=True)
31
+ export_path.write_text(json.dumps(spec, indent=2))
32
+
33
+
34
+ def add_sdk_generation_stub(
35
+ app: FastAPI,
36
+ *,
37
+ on_generate: Optional[callable] = None,
38
+ openapi_path: str = "/openapi.json",
39
+ ) -> None:
40
+ """Hook to add an SDK generation stub.
41
+
42
+ Provide `on_generate()` to run generation (e.g., openapi-generator). This is a stub only; we
43
+ don't ship a hard dependency. If `on_generate` is provided, we expose `/_docs/generate-sdk`.
44
+ """
45
+ from svc_infra.api.fastapi.dual.public import public_router
46
+
47
+ if not on_generate:
48
+ return
49
+
50
+ router = public_router(prefix="/_docs", include_in_schema=False)
51
+
52
+ @router.post("/generate-sdk")
53
+ async def _generate() -> dict: # noqa: ANN201
54
+ on_generate()
55
+ return {"status": "ok"}
56
+
57
+ app.include_router(router)
58
+
59
+
60
+ __all__ = ["add_docs", "add_sdk_generation_stub"]
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Callable
5
+
6
+ from fastapi import FastAPI, HTTPException, Request
7
+ from starlette.responses import JSONResponse
8
+
9
+
10
+ def add_probes(
11
+ app: FastAPI,
12
+ *,
13
+ prefix: str = "/_ops",
14
+ include_in_schema: bool = False,
15
+ ) -> None:
16
+ """Mount basic liveness/readiness/startup probes under prefix."""
17
+ from svc_infra.api.fastapi.dual.public import public_router
18
+
19
+ router = public_router(prefix=prefix, tags=["ops"], include_in_schema=include_in_schema)
20
+
21
+ @router.get("/live")
22
+ async def live() -> JSONResponse: # noqa: D401, ANN201
23
+ return JSONResponse({"status": "ok"})
24
+
25
+ @router.get("/ready")
26
+ async def ready() -> JSONResponse: # noqa: D401, ANN201
27
+ # In the future, add checks (DB ping, cache ping) via DI hooks.
28
+ return JSONResponse({"status": "ok"})
29
+
30
+ @router.get("/startup")
31
+ async def startup_probe() -> JSONResponse: # noqa: D401, ANN201
32
+ return JSONResponse({"status": "ok"})
33
+
34
+ app.include_router(router)
35
+
36
+
37
+ def add_maintenance_mode(app: FastAPI, *, env_var: str = "MAINTENANCE_MODE") -> None:
38
+ """Enable a simple maintenance gate controlled by an env var.
39
+
40
+ When MAINTENANCE_MODE is truthy, all non-GET requests return 503.
41
+ """
42
+
43
+ @app.middleware("http")
44
+ async def _maintenance_gate(request: Request, call_next): # noqa: ANN001, ANN202
45
+ flag = str(os.getenv(env_var, "")).lower() in {"1", "true", "yes", "on"}
46
+ if flag and request.method not in {"GET", "HEAD", "OPTIONS"}:
47
+ return JSONResponse({"detail": "maintenance"}, status_code=503)
48
+ return await call_next(request)
49
+
50
+
51
+ def circuit_breaker_dependency(limit: int = 100, window_seconds: int = 60) -> Callable:
52
+ """Return a dependency that can trip rejective errors based on external metrics.
53
+
54
+ This is a placeholder; callers can swap with a provider that tracks failures and opens the
55
+ breaker. Here, we read an env var to simulate an open breaker.
56
+ """
57
+
58
+ async def _dep(_: Request) -> None: # noqa: D401, ANN202
59
+ if str(os.getenv("CIRCUIT_OPEN", "")).lower() in {"1", "true", "yes", "on"}:
60
+ raise HTTPException(status_code=503, detail="circuit open")
61
+
62
+ return _dep
63
+
64
+
65
+ __all__ = ["add_probes", "add_maintenance_mode", "circuit_breaker_dependency"]
svc_infra/data/add.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Iterable, Optional
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from svc_infra.cli.cmds.db.sql.alembic_cmds import cmd_setup_and_migrate
8
+
9
+
10
+ def add_data_lifecycle(
11
+ app: FastAPI,
12
+ *,
13
+ auto_migrate: bool = True,
14
+ database_url: str | None = None,
15
+ discover_packages: Optional[list[str]] = None,
16
+ with_payments: bool | None = None,
17
+ on_load_fixtures: Optional[Callable[[], None]] = None,
18
+ retention_jobs: Optional[Iterable[Callable[[], None]]] = None,
19
+ erasure_job: Optional[Callable[[str], None]] = None,
20
+ ) -> None:
21
+ """
22
+ Wire data lifecycle conveniences:
23
+
24
+ - auto_migrate: run end-to-end Alembic setup-and-migrate on startup (idempotent).
25
+ - on_load_fixtures: optional callback to load reference/fixture data once at startup.
26
+ - retention_jobs: optional list of callables to register purge tasks (scheduler integration is external).
27
+ - erasure_job: optional callable to trigger a GDPR erasure workflow for a given principal ID.
28
+
29
+ This helper is intentionally minimal: it coordinates existing building blocks
30
+ and offers extension points. Jobs should be scheduled using svc_infra.jobs helpers.
31
+ """
32
+
33
+ @app.on_event("startup")
34
+ async def _data_lifecycle_startup() -> None: # noqa: D401, ANN202
35
+ if auto_migrate:
36
+ # Use existing CLI function to perform end-to-end setup and migrate.
37
+ cmd_setup_and_migrate(
38
+ database_url=database_url,
39
+ overwrite_scaffold=False,
40
+ create_db_if_missing=True,
41
+ create_followup_revision=True,
42
+ initial_message="initial schema",
43
+ followup_message="autogen",
44
+ discover_packages=discover_packages,
45
+ with_payments=with_payments,
46
+ )
47
+
48
+ if on_load_fixtures:
49
+ # Run user-provided fixture loader (idempotent expected).
50
+ on_load_fixtures()
51
+
52
+ # Store optional jobs on app.state for external schedulers to discover/register.
53
+ if retention_jobs is not None:
54
+ app.state.data_retention_jobs = list(retention_jobs)
55
+ if erasure_job is not None:
56
+ app.state.data_erasure_job = erasure_job
57
+
58
+
59
+ __all__ = ["add_data_lifecycle"]
svc_infra/dx/add.py ADDED
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def write_ci_workflow(
7
+ *,
8
+ target_dir: str | Path,
9
+ name: str = "ci.yml",
10
+ python_version: str = "3.12",
11
+ ) -> Path:
12
+ """Write a minimal CI workflow file (GitHub Actions) with tests/lint/type steps."""
13
+ p = Path(target_dir) / ".github" / "workflows" / name
14
+ p.parent.mkdir(parents=True, exist_ok=True)
15
+ content = f"""
16
+ name: CI
17
+
18
+ on:
19
+ push:
20
+ branches: [ main ]
21
+ pull_request:
22
+
23
+ jobs:
24
+ build:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: '{python_version}'
31
+ - name: Install Poetry
32
+ run: pipx install poetry
33
+ - name: Install deps
34
+ run: poetry install
35
+ - name: Lint
36
+ run: poetry run flake8 --select=E,F
37
+ - name: Typecheck
38
+ run: poetry run mypy src
39
+ - name: Tests
40
+ run: poetry run pytest -q -W error
41
+ """
42
+ p.write_text(content.strip() + "\n")
43
+ return p
44
+
45
+
46
+ def write_openapi_lint_config(*, target_dir: str | Path, name: str = ".redocly.yaml") -> Path:
47
+ """Write a minimal OpenAPI lint config placeholder (Redocly)."""
48
+ p = Path(target_dir) / name
49
+ content = """
50
+ apis:
51
+ main:
52
+ root: openapi.json
53
+
54
+ rules:
55
+ operation-operationId: warn
56
+ no-unused-components: warn
57
+ security-defined: off
58
+ """
59
+ p.write_text(content.strip() + "\n")
60
+ return p
61
+
62
+
63
+ __all__ = ["write_ci_workflow", "write_openapi_lint_config"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.601
3
+ Version: 0.1.602
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -56,6 +56,7 @@ svc_infra/api/fastapi/db/sql/session.py,sha256=DUBqKTRJAX4fqRz9B-w9eD9SpzZ8EUS86
56
56
  svc_infra/api/fastapi/db/sql/users.py,sha256=68HGJgYVTEjKJm4-DPPC8-6nwXJoCukmgrYIIOHEUjs,5346
57
57
  svc_infra/api/fastapi/dependencies/ratelimit.py,sha256=DiOC-MJfqTtSydM6RAaeAsiXXL_6oZQoBLvRSpdWzs4,3794
58
58
  svc_infra/api/fastapi/docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
+ svc_infra/api/fastapi/docs/add.py,sha256=qbGO6GHW7tXH34njnGjqQM8Php_jT3iLVe_yxDPmkpU,1671
59
60
  svc_infra/api/fastapi/docs/landing.py,sha256=5JqJYCxQDCWy-BeDLfkv7OBlzWQKSGWUCYXQ51hojG8,4627
60
61
  svc_infra/api/fastapi/docs/scoped.py,sha256=AuN35Op-9fUvHQCLOBtRjd5eWSpB5C9EAW_7-Boxmfo,7540
61
62
  svc_infra/api/fastapi/dual/__init__.py,sha256=scHLcNFkGbgX_R21V702xnAv6GMCkQ4n7yUtNDNgliM,552
@@ -91,6 +92,7 @@ svc_infra/api/fastapi/openapi/mutators.py,sha256=KcWnJ3dsn_VdBG-x0ytddf_vwJ6u-84
91
92
  svc_infra/api/fastapi/openapi/pipeline.py,sha256=GAf-qzwmWlYbrAlPirr8w89fEO4-kFrhCoeMj-7mE44,646
92
93
  svc_infra/api/fastapi/openapi/responses.py,sha256=pBUoJd0lltBkQBJACS1Zd1wd975gbw6dYyMEqyueRuw,1093
93
94
  svc_infra/api/fastapi/openapi/security.py,sha256=U78KMwgc7FilFPLbIE2f6xrp74hq6TDFXpUMGRyK_bg,1248
95
+ svc_infra/api/fastapi/ops/add.py,sha256=39puYLuJdZuIBkpmMiHF6N8H4D-96TRtFdYidbzqndI,2311
94
96
  svc_infra/api/fastapi/pagination.py,sha256=770RbYyzgJotMA8gcc_y5zeAiP3em7fOHKuKDDlpOkI,10867
95
97
  svc_infra/api/fastapi/paths/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
98
  svc_infra/api/fastapi/paths/auth.py,sha256=hy9N0QFQnpv7dBOuHStui5eP9oIGHrawa0sADIVVD64,553
@@ -142,6 +144,7 @@ svc_infra/cli/cmds/obs/obs_cmds.py,sha256=fltUZu5fcnZdl0_JPJBIxIaA1Xqpw1BXE-SWBP
142
144
  svc_infra/cli/foundation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
145
  svc_infra/cli/foundation/runner.py,sha256=RbfjKwb3aHk1Y0MYU8xMpKRpIqRVMVr8GuL2EDZ6n38,1862
144
146
  svc_infra/cli/foundation/typer_bootstrap.py,sha256=KapgH1R-qON9FuYH1KYlVx_5sJvjmAGl25pB61XCpm4,985
147
+ svc_infra/data/add.py,sha256=GbTdPDDsHg9ahQuMlLn4M_YoBFcc16Mf20FeWv1p1sU,2252
145
148
  svc_infra/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
149
  svc_infra/db/crud_schema.py,sha256=-fv-Om1lHVt6lcNbie6A2kRcPex4SDByUPfks6SpmUc,2521
147
150
  svc_infra/db/inbox.py,sha256=drxLRLHaMRrCDgo_8wj12do80wDh5ssHV6LGkaM98no,1996
@@ -198,6 +201,7 @@ svc_infra/db/sql/uniq_hooks.py,sha256=6gCnO0_Y-rhB0p-VuY0mZ9m1u3haiLWI3Ns_iUTqF_
198
201
  svc_infra/db/sql/utils.py,sha256=nzuDcDhnVNehx5Y9BZLgxw8fvpfYbxTfXQsgnznVf4w,32862
199
202
  svc_infra/db/sql/versioning.py,sha256=okZu2ad5RAFXNLXJgGpcQvZ5bc6gPjRWzwiBT0rEJJw,400
200
203
  svc_infra/db/utils.py,sha256=aTD49VJSEu319kIWJ1uijUoP51co4lNJ3S0_tvuyGio,802
204
+ svc_infra/dx/add.py,sha256=FAnLGP0BPm_q_VCEcpUwfj-b0mEse988chh9DHeS7GU,1474
201
205
  svc_infra/jobs/builtins/outbox_processor.py,sha256=VZoehNyjdaV_MmV74WMcbZR6z9E3VFMtZC-pxEwK0x0,1247
202
206
  svc_infra/jobs/builtins/webhook_delivery.py,sha256=z_cl6YKwnduGjGaB8ZoUpKhFcEAhUZqqBma8v2FO1so,2982
203
207
  svc_infra/jobs/easy.py,sha256=eix-OxWeE3vdkY3GGNoYM0GAyOxc928SpiSzMkr9k0A,977
@@ -274,7 +278,7 @@ svc_infra/webhooks/fastapi.py,sha256=BCNvGNxukf6dC2a4i-6en-PrjBGV19YvCWOot5lXWsA
274
278
  svc_infra/webhooks/router.py,sha256=6JvAVPMEth_xxHX-IsIOcyMgHX7g1H0OVxVXKLuMp9w,1596
275
279
  svc_infra/webhooks/service.py,sha256=hWgiJRXKBwKunJOx91C7EcLUkotDtD3Xp0RT6vj2IC0,1797
276
280
  svc_infra/webhooks/signing.py,sha256=NCwdZzmravUe7HVIK_uXK0qqf12FG-_MVsgPvOw6lsM,784
277
- svc_infra-0.1.601.dist-info/METADATA,sha256=--fXg5XHvGu56tmJ-UArjNr8FjrvQ9d-75Bwww4i3bI,7839
278
- svc_infra-0.1.601.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
279
- svc_infra-0.1.601.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
280
- svc_infra-0.1.601.dist-info/RECORD,,
281
+ svc_infra-0.1.602.dist-info/METADATA,sha256=GarD5h35kMI5JWTb1QpqupgtKdpT8GIsv71yTKOEdgA,7839
282
+ svc_infra-0.1.602.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
283
+ svc_infra-0.1.602.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
284
+ svc_infra-0.1.602.dist-info/RECORD,,