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

Files changed (118) hide show
  1. svc_infra/api/fastapi/admin/__init__.py +3 -0
  2. svc_infra/api/fastapi/admin/add.py +231 -0
  3. svc_infra/api/fastapi/billing/router.py +64 -0
  4. svc_infra/api/fastapi/billing/setup.py +19 -0
  5. svc_infra/api/fastapi/db/sql/add.py +32 -13
  6. svc_infra/api/fastapi/db/sql/crud_router.py +178 -16
  7. svc_infra/api/fastapi/db/sql/session.py +16 -0
  8. svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
  9. svc_infra/api/fastapi/docs/add.py +160 -0
  10. svc_infra/api/fastapi/docs/landing.py +1 -1
  11. svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
  12. svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  13. svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
  14. svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
  15. svc_infra/api/fastapi/middleware/timeout.py +148 -0
  16. svc_infra/api/fastapi/openapi/mutators.py +114 -0
  17. svc_infra/api/fastapi/ops/add.py +73 -0
  18. svc_infra/api/fastapi/pagination.py +3 -1
  19. svc_infra/api/fastapi/routers/ping.py +1 -0
  20. svc_infra/api/fastapi/setup.py +11 -1
  21. svc_infra/api/fastapi/tenancy/add.py +19 -0
  22. svc_infra/api/fastapi/tenancy/context.py +112 -0
  23. svc_infra/app/README.md +5 -5
  24. svc_infra/billing/__init__.py +23 -0
  25. svc_infra/billing/async_service.py +147 -0
  26. svc_infra/billing/jobs.py +230 -0
  27. svc_infra/billing/models.py +131 -0
  28. svc_infra/billing/quotas.py +101 -0
  29. svc_infra/billing/schemas.py +33 -0
  30. svc_infra/billing/service.py +115 -0
  31. svc_infra/bundled_docs/README.md +5 -0
  32. svc_infra/bundled_docs/__init__.py +1 -0
  33. svc_infra/bundled_docs/getting-started.md +6 -0
  34. svc_infra/cache/__init__.py +4 -0
  35. svc_infra/cache/add.py +158 -0
  36. svc_infra/cache/backend.py +5 -2
  37. svc_infra/cache/decorators.py +19 -1
  38. svc_infra/cache/keys.py +24 -4
  39. svc_infra/cli/__init__.py +28 -8
  40. svc_infra/cli/cmds/__init__.py +8 -0
  41. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
  42. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
  43. svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
  44. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  45. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  46. svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
  47. svc_infra/cli/cmds/dx/__init__.py +12 -0
  48. svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  49. svc_infra/cli/cmds/help.py +4 -0
  50. svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
  51. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  52. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  53. svc_infra/data/add.py +61 -0
  54. svc_infra/data/backup.py +53 -0
  55. svc_infra/data/erasure.py +45 -0
  56. svc_infra/data/fixtures.py +40 -0
  57. svc_infra/data/retention.py +55 -0
  58. svc_infra/db/nosql/mongo/README.md +13 -13
  59. svc_infra/db/sql/repository.py +51 -11
  60. svc_infra/db/sql/resource.py +5 -0
  61. svc_infra/db/sql/templates/setup/env_async.py.tmpl +9 -1
  62. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +9 -2
  63. svc_infra/db/sql/tenant.py +79 -0
  64. svc_infra/db/sql/utils.py +18 -4
  65. svc_infra/docs/acceptance-matrix.md +71 -0
  66. svc_infra/docs/acceptance.md +44 -0
  67. svc_infra/docs/admin.md +425 -0
  68. svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
  69. svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
  70. svc_infra/docs/adr/0004-tenancy-model.md +42 -0
  71. svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
  72. svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
  73. svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
  74. svc_infra/docs/adr/0008-billing-primitives.md +143 -0
  75. svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
  76. svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  77. svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
  78. svc_infra/docs/api.md +59 -0
  79. svc_infra/docs/auth.md +11 -0
  80. svc_infra/docs/billing.md +190 -0
  81. svc_infra/docs/cache.md +76 -0
  82. svc_infra/docs/cli.md +74 -0
  83. svc_infra/docs/contributing.md +34 -0
  84. svc_infra/docs/data-lifecycle.md +52 -0
  85. svc_infra/docs/database.md +14 -0
  86. svc_infra/docs/docs-and-sdks.md +62 -0
  87. svc_infra/docs/environment.md +114 -0
  88. svc_infra/docs/getting-started.md +63 -0
  89. svc_infra/docs/idempotency.md +111 -0
  90. svc_infra/docs/jobs.md +67 -0
  91. svc_infra/docs/observability.md +16 -0
  92. svc_infra/docs/ops.md +37 -0
  93. svc_infra/docs/rate-limiting.md +125 -0
  94. svc_infra/docs/repo-review.md +48 -0
  95. svc_infra/docs/security.md +176 -0
  96. svc_infra/docs/tenancy.md +35 -0
  97. svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  98. svc_infra/docs/webhooks.md +112 -0
  99. svc_infra/dx/add.py +63 -0
  100. svc_infra/dx/changelog.py +74 -0
  101. svc_infra/dx/checks.py +67 -0
  102. svc_infra/http/__init__.py +13 -0
  103. svc_infra/http/client.py +72 -0
  104. svc_infra/jobs/builtins/webhook_delivery.py +14 -2
  105. svc_infra/jobs/queue.py +9 -1
  106. svc_infra/jobs/runner.py +75 -0
  107. svc_infra/jobs/worker.py +17 -1
  108. svc_infra/mcp/svc_infra_mcp.py +85 -28
  109. svc_infra/obs/add.py +54 -7
  110. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  111. svc_infra/security/headers.py +15 -2
  112. svc_infra/security/hibp.py +6 -2
  113. svc_infra/security/permissions.py +1 -0
  114. svc_infra/webhooks/service.py +10 -2
  115. {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/METADATA +40 -14
  116. {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/RECORD +118 -44
  117. {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/WHEEL +0 -0
  118. {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,76 @@
1
+ # Cache guide
2
+
3
+ The cache module wraps [cashews](https://github.com/Krukov/cashews) with decorators and namespace helpers so services can centralize key formats.
4
+
5
+ ```python
6
+ from svc_infra.cache import cache_read, cache_write, init_cache
7
+
8
+ init_cache() # uses CACHE_PREFIX / CACHE_VERSION
9
+
10
+ @cache_read(key="user:{user_id}", ttl=300)
11
+ async def get_user(user_id: int):
12
+ ...
13
+ ```
14
+
15
+ ### Environment
16
+
17
+ - `CACHE_PREFIX`, `CACHE_VERSION` – change the namespace alias used by the decorators. 【F:src/svc_infra/cache/README.md†L20-L173】
18
+ - `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG` – override canonical TTL buckets. 【F:src/svc_infra/cache/ttl.py†L26-L55】
19
+
20
+ ## Easy integration: add_cache
21
+
22
+ Use the one-liner helper to wire cache initialization into your ASGI app lifecycle with sensible defaults. This doesn’t replace the decorators; it standardizes init/readiness/shutdown and exposes a handle for convenience.
23
+
24
+ ```python
25
+ from fastapi import FastAPI
26
+ from svc_infra.cache import add_cache, cache_read, cache_write, resource
27
+
28
+ app = FastAPI()
29
+
30
+ # Wires startup (init + readiness) and shutdown (graceful close). Idempotent.
31
+ add_cache(app)
32
+
33
+ user = resource("user", "user_id")
34
+
35
+ @user.cache_read(suffix="profile", ttl=300)
36
+ async def get_user_profile(user_id: int):
37
+ ...
38
+
39
+ @user.cache_write()
40
+ async def update_user_profile(user_id: int, payload):
41
+ ...
42
+
43
+ # Optional: direct cache instance for advanced scenarios
44
+ # available after startup when using add_cache(app)
45
+ # app.state.cache -> cashews cache instance
46
+ ```
47
+
48
+ ### Env-driven defaults
49
+
50
+ - URL: `CACHE_URL` → `REDIS_URL` → `mem://`
51
+ - Prefix: `CACHE_PREFIX` (default `svc`)
52
+ - Version: `CACHE_VERSION` (default `v1`)
53
+
54
+ You can override explicitly:
55
+
56
+ ```python
57
+ add_cache(app, url="redis://localhost:6379/0", prefix="myapp", version="v2")
58
+ ```
59
+
60
+ ### Behavior
61
+
62
+ - Idempotent: multiple calls won’t duplicate handlers.
63
+ - Startup/shutdown hooks: registered when supported by the app; startup performs a readiness probe. Startup is optional for correctness, but recommended for production reliability.
64
+ - app.state exposure: by default, exposes `app.state.cache` to access the underlying cashews instance.
65
+
66
+ ### No-app usage
67
+
68
+ If you’re not wiring an app (e.g., a script), you can initialize without startup hooks:
69
+
70
+ ```python
71
+ from svc_infra.cache import add_cache
72
+
73
+ shutdown = add_cache(None) # immediate init (best-effort)
74
+ # ... do work ...
75
+ # call shutdown() is a no-op placeholder for symmetry
76
+ ```
svc_infra/docs/cli.md ADDED
@@ -0,0 +1,74 @@
1
+ # CLI Quick Reference
2
+
3
+ The `svc-infra` CLI wraps common database, observability, jobs, docs, and DX workflows using Typer.
4
+
5
+ - Entry points:
6
+ - Global: `svc-infra ...` (installed via Poetry scripts)
7
+ - Module: `python -m svc_infra.cli ...` (works in editable installs and containers)
8
+
9
+ ## Top-level help
10
+
11
+ Run:
12
+
13
+ ```
14
+ svc-infra --help
15
+ ```
16
+
17
+ You should see groups for SQL, Mongo, Observability, DX, Jobs, and SDK.
18
+
19
+ ## Database (Alembic) commands
20
+
21
+ - End-to-end setup and migrate (detects async from URL):
22
+ - Environment variables (commonly): `SQL_URL` or compose parts `DB_*`.
23
+
24
+ Example with SQLite for quick smoke tests:
25
+
26
+ ```
27
+ python -m svc_infra.cli sql setup-and-migrate --database-url sqlite+aiosqlite:///./accept.db \
28
+ --discover-packages "app.models" --with-payments false
29
+ ```
30
+
31
+ - Current revision, history, upgrade/downgrade:
32
+
33
+ ```
34
+ python -m svc_infra.cli sql current
35
+ python -m svc_infra.cli sql-history
36
+ python -m svc_infra.cli sql upgrade head
37
+ python -m svc_infra.cli sql downgrade -1
38
+ ```
39
+
40
+ - Seed fixtures/reference data with your callable:
41
+
42
+ ```
43
+ python -m svc_infra.cli sql seed path.to.module:seed_func
44
+ ```
45
+
46
+ Notes:
47
+ - The target must be in the format `module.path:callable`.
48
+ - If you previously referenced legacy test modules under `tests.db.*`, the CLI shims import to `tests.unit.db.*` when possible.
49
+
50
+ ## Jobs
51
+
52
+ Start the local jobs runner loop:
53
+
54
+ ```
55
+ svc-infra jobs run
56
+ ```
57
+
58
+ ## DX helpers
59
+
60
+ - Generate CI workflow and checks template:
61
+
62
+ ```
63
+ python -m svc_infra.cli dx ci --openapi openapi.json
64
+ ```
65
+
66
+ - Lint OpenAPI and Problem+JSON samples:
67
+
68
+ ```
69
+ python -m svc_infra.cli dx openapi openapi.json
70
+ ```
71
+
72
+ ## SDKs
73
+
74
+ Generate SDKs from OpenAPI (dry-run by default): see `docs/docs-and-sdks.md` for full examples.
@@ -0,0 +1,34 @@
1
+ # Contributing
2
+
3
+ Thanks for considering a contribution! This repo aims to provide production-ready primitives with clear gates.
4
+
5
+ ## Local setup
6
+
7
+ - Python 3.11–3.13
8
+ - Install via Poetry:
9
+ - `poetry install`
10
+ - `poetry run pre-commit install`
11
+
12
+ ## Quality gates (run before PR)
13
+
14
+ - Lint: `poetry run flake8 --select=E,F`
15
+ - Typecheck: `poetry run mypy src`
16
+ - Tests: `poetry run pytest -q -W error`
17
+ - OpenAPI lint (optional): `poetry run python -m svc_infra.cli dx openapi openapi.json`
18
+ - Migrations present (optional): `poetry run python -m svc_infra.cli dx migrations --project-root .`
19
+ - CI dry-run (optional): `poetry run python -m svc_infra.cli dx ci --openapi openapi.json`
20
+
21
+ ## Commit style
22
+
23
+ - Prefer Conventional Commits: `feat:`, `fix:`, `refactor:`, etc.
24
+ - Use changelog generator for releases:
25
+ - `poetry run python -m svc_infra.cli dx changelog 0.1.604 --commits-file commits.jsonl`
26
+
27
+ ## Release process
28
+
29
+ 1. Ensure all gates are green locally (see above).
30
+ 2. Update version in `pyproject.toml`.
31
+ 3. Export OpenAPI (if applicable) via docs helper.
32
+ 4. Generate changelog section and review.
33
+ 5. Merge to `main`. CI will run tests, lint, and typecheck.
34
+ 6. Tag and publish.
@@ -0,0 +1,52 @@
1
+ # Data Lifecycle
2
+
3
+ This guide covers fixtures (reference data), retention policies (soft/hard delete), GDPR erasure, and backup verification.
4
+
5
+ ## Quickstart
6
+
7
+ - Fixtures:
8
+ - Use `run_fixtures([...])` for ad-hoc loads.
9
+ - Or wire a one-time loader with `make_on_load_fixtures(fn, run_once_file)`, then `add_data_lifecycle(app, on_startup=[on_load])`.
10
+ - Retention:
11
+ - Define `RetentionPolicy(name, model, older_than_days, soft_delete_field|None, hard_delete=False)`.
12
+ - Execute manually with `await run_retention_purge(session, [policy,...])` or schedule via your jobs runner.
13
+ - Erasure:
14
+ - Compose an `ErasurePlan([ErasureStep(name, func), ...])` where functions accept `(session, principal_id)` and may be async.
15
+ - Run with `await run_erasure(session, principal_id, plan, on_audit=callable)`; `on_audit` receives `(event, context)`.
16
+ - Backups:
17
+ - `verify_backups(last_success: datetime|None, retention_days: int)` returns a `BackupHealthReport`.
18
+ - Wrap as a job: `make_backup_verification_job(checker, on_report=callback)`.
19
+
20
+ ## APIs
21
+
22
+ - `fixtures.py`:
23
+ - `run_fixtures(callables: Iterable[Callable]) -> Awaitable[None]`
24
+ - `make_on_load_fixtures(*fns, run_once_file: str | None = None) -> Callable[[], Awaitable[None]]`
25
+ - `retention.py`:
26
+ - `RetentionPolicy(name, model, older_than_days, soft_delete_field: str | None, hard_delete: bool = False)`
27
+ - `run_retention_purge(session, policies: Sequence[RetentionPolicy]) -> Awaitable[int]`
28
+ - `erasure.py`:
29
+ - `ErasureStep(name: str, func: Callable)`
30
+ - `ErasurePlan(steps: Sequence[ErasureStep])`
31
+ - `run_erasure(session, principal_id: str, plan: ErasurePlan, on_audit: Callable | None = None) -> Awaitable[int]`
32
+ - `backup.py`:
33
+ - `BackupHealthReport(ok: bool, last_success: datetime | None, reason: str | None)`
34
+ - `verify_backups(last_success: datetime | None = None, retention_days: int = 1) -> BackupHealthReport`
35
+ - `make_backup_verification_job(checker: Callable[[], BackupHealthReport], on_report: Callable[[BackupHealthReport], None] | None = None) -> Callable[[], BackupHealthReport]`
36
+
37
+ ## Scheduling
38
+
39
+ Use the jobs helpers to run retention and backup checks periodically. Example schedule JSON (via JOBS_SCHEDULE_JSON):
40
+
41
+ ```json
42
+ [
43
+ {"name": "retention-purge", "interval": "6h", "handler": "your.module:run_retention"},
44
+ {"name": "backup-verify", "interval": "12h", "handler": "your.module:verify_backups_job"}
45
+ ]
46
+ ```
47
+
48
+ ## Notes
49
+
50
+ - Soft delete expects a `deleted_at` column and optionally an `is_active` flag in repositories.
51
+ - `run_fixtures` and erasure steps support async functions seamlessly.
52
+ - `add_data_lifecycle` already awaits async fixture loaders and uses lifespan instead of deprecated startup events.
@@ -0,0 +1,14 @@
1
+ # Database guide
2
+
3
+ svc-infra exposes helpers for SQLAlchemy and Mongo so APIs get lifecycle management, migrations, and connection URLs from environment variables.
4
+
5
+ ## SQL
6
+
7
+ - `add_sql_db(app, url=None, dsn_env="SQL_URL")` wires the session and raises if the URL env is missing. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L55-L114】
8
+ - Build URLs piecemeal with `DB_DIALECT`, `DB_DRIVER`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_PARAMS`, or point at `SQL_URL_FILE`/`DB_PASSWORD_FILE`. 【F:src/svc_infra/db/sql/utils.py†L85-L206】
9
+ - Alembic templates respect overrides such as `ALEMBIC_DISCOVER_PACKAGES`, `ALEMBIC_INCLUDE_SCHEMAS`, and `ALEMBIC_SKIP_DROPS`. 【F:src/svc_infra/db/sql/utils.py†L274-L347】
10
+
11
+ ## Mongo
12
+
13
+ - `add_mongo_db(app, dsn_env="MONGO_URL")` validates the URL and optional db name. 【F:src/svc_infra/api/fastapi/db/nosql/mongo/add.py†L28-L53】
14
+ - Configure via `MONGO_URL`, `MONGO_DB`, `MONGO_APPNAME`, `MONGO_MIN_POOL`, `MONGO_MAX_POOL`, or point at `MONGO_URL_FILE`. 【F:src/svc_infra/db/nosql/mongo/settings.py†L9-L13】【F:src/svc_infra/db/nosql/utils.py†L56-L113】
@@ -0,0 +1,62 @@
1
+ # Docs & SDKs
2
+
3
+ This guide shows how to enable API docs, enrich your OpenAPI, and generate SDKs.
4
+
5
+ ## Enabling docs
6
+
7
+ - Use `setup_service_api(...)` for versioned apps; root docs are auto-mounted in local/dev.
8
+ - For standalone FastAPI apps, call `add_docs(app)` to mount:
9
+ - `/docs` (Swagger UI)
10
+ - `/redoc` (ReDoc)
11
+ - `/openapi.json` (OpenAPI schema)
12
+ - A landing page at `/` listing root and scoped docs (falls back to `/_docs` if `/` is taken).
13
+
14
+ Tip: Append `?theme=dark` to `/docs` or `/redoc` for a minimal dark mode.
15
+
16
+ ## OpenAPI enrichment
17
+
18
+ The OpenAPI pipeline adds helpful metadata automatically:
19
+ - `x-codeSamples` per operation (curl and httpie) using your server base URL.
20
+ - Problem+JSON examples on error responses (4xx/5xx) referencing the `Problem` schema.
21
+ - Existing success/media examples are preserved and normalized.
22
+
23
+ These mutators are applied for both root and versioned apps via `setup_mutators(...)`.
24
+
25
+ ## Exporting OpenAPI
26
+
27
+ `add_docs(app, export_openapi_to="openapi.json")` writes the schema to disk on startup.
28
+
29
+ ## Generate SDKs (CLI)
30
+
31
+ Use the CLI to generate SDKs from OpenAPI (defaults to dry-run, uses npx tools):
32
+
33
+ - TypeScript (openapi-typescript-codegen):
34
+ svc-infra sdk ts openapi.json --outdir sdk-ts --dry-run=false
35
+
36
+ - Python (openapi-generator):
37
+ svc-infra sdk py openapi.json --outdir sdk-py --package-name client_sdk --dry-run=false
38
+
39
+ - Postman collection:
40
+ svc-infra sdk postman openapi.json --out postman.json --dry-run=false
41
+
42
+ ## Quick curl examples
43
+
44
+ Replace URL and payload as needed; these align with x-codeSamples included in the schema.
45
+
46
+ - GET
47
+ curl -X GET 'http://localhost:8000/v1/projects'
48
+
49
+ - POST with JSON
50
+ curl -X POST 'http://localhost:8000/v1/projects' \
51
+ -H 'Content-Type: application/json' \
52
+ -d '{"name":"Example"}'
53
+
54
+ Notes:
55
+ - You need Node.js; the CLI calls `npx` for generator tools. Add them to your devDependencies for reproducibility.
56
+ - For CI, export OpenAPI to a path and run the CLI with `--dry-run=false`.
57
+
58
+ ## Troubleshooting
59
+
60
+ - Docs not visible at `/`? If your app already handles `/`, the landing page is mounted at `/_docs`.
61
+ - Dark mode not applying? Use `/docs?theme=dark` or `/redoc?theme=dark`.
62
+ - Missing Problem examples? Ensure your error handlers reference the `Problem` schema and that mutators run (they are wired by default in `setup_service_api`).
@@ -0,0 +1,114 @@
1
+ # Environment Reference
2
+
3
+ This guide consolidates every environment variable consumed by the svc-infra helpers in FastAPI, jobs, observability, security, and webhooks. Defaults shown below reflect the library's fallbacks when a variable is absent. Where a helper relies on `svc_infra.app.pick`, the note column calls out the environment-specific behavior.
4
+
5
+ ## FastAPI helpers
6
+
7
+ ### App bootstrap (`easy_service_app` / `setup_service_api`)
8
+
9
+ | Variable | Default | Consumed by | Notes |
10
+ | --- | --- | --- | --- |
11
+ | `ENABLE_LOGGING` | `true` | `EasyAppOptions.from_env()` | Disables `setup_logging` when set to false. |
12
+ | `LOG_LEVEL` | Auto (`INFO` in prod/test, `DEBUG` in dev/local via `pick()`) | `easy_service_app()` | Overrides the log level chosen by `svc_infra.app.pick`. |
13
+ | `LOG_FORMAT` | Auto (JSON in prod, plain elsewhere) | `easy_service_app()` | Explicit `json` or `plain` format overrides auto-detection. |
14
+ | `ENABLE_OBS` | `true` | `EasyAppOptions.from_env()` / `easy_service_app()` | Turns observability instrumentation on/off. |
15
+ | `METRICS_PATH` | `None` → falls back to Observability settings | `EasyAppOptions.from_env()` | Use to expose metrics at a non-default path. |
16
+ | `OBS_SKIP_PATHS` | `None` → defaults to metrics + health endpoints | `EasyAppOptions.from_env()` | Comma/space-separated list of paths skipped by Prometheus middleware. |
17
+ | `CORS_ALLOW_ORIGINS` | `""` (no origins) | `_setup_cors()` | Adds `CORSMiddleware` allow-list when non-empty. |
18
+
19
+ ### SQL helpers (`add_sql_db`, `setup_sql`)
20
+
21
+ | Variable | Default | Consumed by | Notes |
22
+ | --- | --- | --- | --- |
23
+ | `SQL_URL` (overridable via `dsn_env`) | _required_ | `add_sql_db()` / `setup_sql()` | Missing value raises `RuntimeError`; point at your primary database URL. |
24
+
25
+ ### Mongo helpers (`add_mongo_db`, `init_mongo`)
26
+
27
+ | Variable | Default | Consumed by | Notes |
28
+ | --- | --- | --- | --- |
29
+ | `MONGO_URL` / `MONGODB_URL` | `mongodb://localhost:27017` | `MongoSettings`, `add_mongo_db()` | Primary Mongo connection string; `_FILE` suffix or `MONGO_URL_FILE` allow secret mounts. |
30
+ | `MONGO_DB` / `MONGODB_DB` / `MONGO_DATABASE` | unset (optional) | `get_mongo_dbname_from_env()` | When set, verified against the connected database name. |
31
+ | `MONGO_APPNAME` | `svc-infra` | `MongoSettings` | Sets the Mongo client `appname`. |
32
+ | `MONGO_MIN_POOL` | `0` | `MongoSettings` | Minimum Motor/Mongo client pool size. |
33
+ | `MONGO_MAX_POOL` | `100` | `MongoSettings` | Maximum Motor/Mongo client pool size. |
34
+ | `MONGO_URL_FILE` | unset | `get_mongo_url_from_env()` | Alternate secret file path when not using `_FILE` suffix envs. |
35
+ | `/run/secrets/mongo_url` | unset | `get_mongo_url_from_env()` | Auto-mounted Docker/K8s secret fallback for the URL. |
36
+
37
+ ### Auth settings (`get_auth_settings` → `AuthSettings`)
38
+
39
+ Pydantic loads these with the `AUTH_` prefix and `__` as the nested delimiter.
40
+
41
+ | Variable | Default | Consumed by | Notes |
42
+ | --- | --- | --- | --- |
43
+ | `AUTH_JWT__SECRET` | _required when JWT auth enabled_ | `AuthSettings.jwt.secret` | Primary HS256 signing secret. |
44
+ | `AUTH_JWT__LIFETIME_SECONDS` | `604800` (7 days) | `AuthSettings.jwt.lifetime_seconds` | Adjusts refresh token lifetime. |
45
+ | `AUTH_JWT__OLD_SECRETS__*` | `[]` | `AuthSettings.jwt.old_secrets` | Accepted legacy secrets during rotation. |
46
+ | `AUTH_PASSWORD_CLIENTS__{n}__CLIENT_ID` | `[]` | `AuthSettings.password_clients[*].client_id` | Register password clients (list entries indexed by `{n}`). |
47
+ | `AUTH_PASSWORD_CLIENTS__{n}__CLIENT_SECRET` | `[]` | `AuthSettings.password_clients[*].client_secret` | Secret per password client. |
48
+ | `AUTH_REQUIRE_CLIENT_SECRET_ON_PASSWORD_LOGIN` | `false` | `AuthSettings.require_client_secret_on_password_login` | Enforces client secret on password grant. |
49
+ | `AUTH_MFA_DEFAULT_ENABLED_FOR_NEW_USERS` | `false` | `AuthSettings.mfa_default_enabled_for_new_users` | Enable TOTP by default on signup. |
50
+ | `AUTH_MFA_ENFORCE_FOR_ALL_USERS` | `false` | `AuthSettings.mfa_enforce_for_all_users` | Force MFA globally. |
51
+ | `AUTH_MFA_ENFORCE_FOR_TENANTS` | `[]` | `AuthSettings.mfa_enforce_for_tenants` | Tenant allow-list requiring MFA. |
52
+ | `AUTH_MFA_ISSUER` | `"svc-infra"` | `AuthSettings.mfa_issuer` | Label for TOTP apps. |
53
+ | `AUTH_MFA_PRE_TOKEN_LIFETIME_SECONDS` | `300` | `AuthSettings.mfa_pre_token_lifetime_seconds` | Lifespan of MFA pre-token. |
54
+ | `AUTH_MFA_RECOVERY_CODES` | `8` | `AuthSettings.mfa_recovery_codes` | Number of recovery codes issued. |
55
+ | `AUTH_MFA_RECOVERY_CODE_LENGTH` | `10` | `AuthSettings.mfa_recovery_code_length` | Digits per recovery code. |
56
+ | `AUTH_EMAIL_OTP_TTL_SECONDS` | `300` | `AuthSettings.email_otp_ttl_seconds` | Email OTP validity window. |
57
+ | `AUTH_EMAIL_OTP_COOLDOWN_SECONDS` | `60` | `AuthSettings.email_otp_cooldown_seconds` | Cooldown between OTP sends. |
58
+ | `AUTH_EMAIL_OTP_ATTEMPTS` | `5` | `AuthSettings.email_otp_attempts` | Maximum OTP attempts before lock. |
59
+ | `AUTH_SMTP_HOST` | `None` | `AuthSettings.smtp_host` | SMTP hostname (required for prod email). |
60
+ | `AUTH_SMTP_PORT` | `587` | `AuthSettings.smtp_port` | SMTP port. |
61
+ | `AUTH_SMTP_USERNAME` | `None` | `AuthSettings.smtp_username` | SMTP username. |
62
+ | `AUTH_SMTP_PASSWORD` | `None` | `AuthSettings.smtp_password` | SMTP password/secret. |
63
+ | `AUTH_SMTP_FROM` | `None` | `AuthSettings.smtp_from` | Default From address. |
64
+ | `AUTH_AUTO_VERIFY_IN_DEV` | `true` | `AuthSettings.auto_verify_in_dev` | Auto-confirms accounts outside prod. |
65
+ | `AUTH_GOOGLE_CLIENT_ID` | `None` | `AuthSettings.google_client_id` | Built-in Google OAuth client ID. |
66
+ | `AUTH_GOOGLE_CLIENT_SECRET` | `None` | `AuthSettings.google_client_secret` | Built-in Google OAuth secret. |
67
+ | `AUTH_GITHUB_CLIENT_ID` | `None` | `AuthSettings.github_client_id` | GitHub OAuth client ID. |
68
+ | `AUTH_GITHUB_CLIENT_SECRET` | `None` | `AuthSettings.github_client_secret` | GitHub OAuth secret. |
69
+ | `AUTH_MS_CLIENT_ID` | `None` | `AuthSettings.ms_client_id` | Microsoft OAuth client ID. |
70
+ | `AUTH_MS_CLIENT_SECRET` | `None` | `AuthSettings.ms_client_secret` | Microsoft OAuth secret. |
71
+ | `AUTH_MS_TENANT` | `None` | `AuthSettings.ms_tenant` | Microsoft tenant ID. |
72
+ | `AUTH_LI_CLIENT_ID` | `None` | `AuthSettings.li_client_id` | LinkedIn OAuth client ID. |
73
+ | `AUTH_LI_CLIENT_SECRET` | `None` | `AuthSettings.li_client_secret` | LinkedIn OAuth secret. |
74
+ | `AUTH_OIDC_PROVIDERS__{n}__NAME` | `[]` | `AuthSettings.oidc_providers[*].name` | Custom OIDC providers (list entries indexed by `{n}`). |
75
+ | `AUTH_OIDC_PROVIDERS__{n}__ISSUER` | `[]` | `AuthSettings.oidc_providers[*].issuer` | OIDC issuer URL. |
76
+ | `AUTH_OIDC_PROVIDERS__{n}__CLIENT_ID` | `[]` | `AuthSettings.oidc_providers[*].client_id` | OIDC client ID. |
77
+ | `AUTH_OIDC_PROVIDERS__{n}__CLIENT_SECRET` | `[]` | `AuthSettings.oidc_providers[*].client_secret` | OIDC client secret. |
78
+ | `AUTH_OIDC_PROVIDERS__{n}__SCOPE` | `"openid email profile"` | `AuthSettings.oidc_providers[*].scope` | Additional OIDC scopes. |
79
+ | `AUTH_POST_LOGIN_REDIRECT` | `http://localhost:3000/app` | `AuthSettings.post_login_redirect` | Default redirect after login. |
80
+ | `AUTH_REDIRECT_ALLOW_HOSTS_RAW` | `"localhost,127.0.0.1"` | `AuthSettings.redirect_allow_hosts_raw` | CSV/JSON allow-list for redirects. |
81
+ | `AUTH_SESSION_COOKIE_NAME` | `"svc_session"` | `AuthSettings.session_cookie_name` | Session cookie key. |
82
+ | `AUTH_AUTH_COOKIE_NAME` | `"svc_auth"` | `AuthSettings.auth_cookie_name` | Auth cookie key. |
83
+ | `AUTH_SESSION_COOKIE_SECURE` | `false` | `AuthSettings.session_cookie_secure` | Marks session cookie `Secure`. |
84
+ | `AUTH_SESSION_COOKIE_SAMESITE` | `"lax"` | `AuthSettings.session_cookie_samesite` | SameSite policy. |
85
+ | `AUTH_SESSION_COOKIE_DOMAIN` | `None` | `AuthSettings.session_cookie_domain` | Explicit cookie domain. |
86
+ | `AUTH_SESSION_COOKIE_MAX_AGE_SECONDS` | `14400` (4 hours) | `AuthSettings.session_cookie_max_age_seconds` | Session cookie lifetime. |
87
+
88
+ ## Jobs helpers
89
+
90
+ | Variable | Default | Consumed by | Notes |
91
+ | --- | --- | --- | --- |
92
+ | `JOBS_DRIVER` | `memory` | `JobsConfig`, `easy_jobs()` | Choose `redis` to activate Redis-backed queue. |
93
+ | `REDIS_URL` | `redis://localhost:6379/0` | `easy_jobs()` (Redis driver) | Redis connection string when `JOBS_DRIVER=redis`. |
94
+ | `JOBS_SCHEDULE_JSON` | unset | `schedule_from_env()` | JSON array of scheduler tasks (name, interval_seconds, target). |
95
+
96
+ ## Observability helpers
97
+
98
+ | Variable | Default | Consumed by | Notes |
99
+ | --- | --- | --- | --- |
100
+ | `METRICS_ENABLED` | `true` | `ObservabilitySettings` | Gate for Prometheus middleware registration. |
101
+ | `METRICS_PATH` | `/metrics` | `ObservabilitySettings`, `add_observability()` | Metrics endpoint path. |
102
+ | `METRICS_DEFAULT_BUCKETS` | `0.005,0.01,0.025,0.05,0.1,0.25,0.5,1.0,2.0,5.0,10.0` | `ObservabilitySettings` | Histogram buckets for request latency. |
103
+ | `SVC_INFRA_DISABLE_PROMETHEUS` | unset (`"1"` disables) | `metrics.asgi` | Skip Prometheus setup when toggled. |
104
+ | `SVC_INFRA_RATE_WINDOW` | unset | `cloud_dash.push_dashboards_from_pkg()` | Overrides `$__rate_interval` in dashboards. |
105
+ | `SVC_INFRA_DASHBOARD_REFRESH` | `5s` | `cloud_dash.push_dashboards_from_pkg()` | Grafana dashboard auto-refresh interval. |
106
+ | `SVC_INFRA_DASHBOARD_RANGE` | `now-6h` | `cloud_dash.push_dashboards_from_pkg()` | Default Grafana time range start. |
107
+
108
+ ## Security helpers
109
+
110
+ The primitives under `svc_infra.security` rely on configuration objects passed from application code; they do not read environment variables directly beyond the shared `AuthSettings` listed above.
111
+
112
+ ## Webhook helpers
113
+
114
+ Current webhook helpers (`fastapi.require_signature`, `InMemoryWebhookSubscriptions`, `WebhookService`) rely on dependency injection for secrets and stores and do not read environment variables directly.
@@ -0,0 +1,63 @@
1
+ # svc-infra
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/svc-infra.svg)](https://pypi.org/project/svc-infra/)
4
+ [![Docs](https://img.shields.io/badge/docs-reference-blue)](.)
5
+
6
+ svc-infra packages the shared building blocks we use to ship production FastAPI services fast—HTTP APIs with secure auth, durable persistence, background execution, cache, observability, and webhook plumbing that all share the same batteries-included defaults.
7
+
8
+ ## Helper index
9
+
10
+ | Area | What it covers | Guide |
11
+ | --- | --- | --- |
12
+ | Getting Started | Overview and entry points | [This page](getting-started.md) |
13
+ | Environment | Feature switches and env vars | [Environment](environment.md) |
14
+ | API | FastAPI bootstrap, middleware, docs wiring | [API guide](api.md) |
15
+ | Auth | Sessions, OAuth/OIDC, MFA, SMTP delivery | [Auth](auth.md) |
16
+ | Security | Password policy, lockout, signed cookies, headers | [Security](security.md) |
17
+ | Database | SQL + Mongo wiring, Alembic helpers, inbox/outbox patterns | [Database](database.md) |
18
+ | Tenancy | Multi-tenant boundaries and helpers | [Tenancy](tenancy.md) |
19
+ | Idempotency | Idempotent endpoints and middleware | [Idempotency](idempotency.md) |
20
+ | Rate Limiting | Middleware, dependency limiter, headers | [Rate limiting](rate-limiting.md) |
21
+ | Cache | cashews decorators, namespace management, TTL helpers | [Cache](cache.md) |
22
+ | Jobs | JobQueue, scheduler, CLI worker | [Jobs](jobs.md) |
23
+ | Observability | Prometheus, Grafana, OpenTelemetry | [Observability](observability.md) |
24
+ | Ops | Probes, breakers, SLOs & dashboards | [Ops](ops.md) |
25
+ | Webhooks | Subscription store, signing, retry worker | [Webhooks](webhooks.md) |
26
+ | CLI | Command groups for sql/mongo/obs/docs/dx/sdk/jobs | [CLI](cli.md) |
27
+ | Docs & SDKs | Publishing docs, generating SDKs | [Docs & SDKs](docs-and-sdks.md) |
28
+ | Acceptance | Acceptance harness and flows | [Acceptance](acceptance.md), [Matrix](acceptance-matrix.md) |
29
+ | Contributing | Dev setup and quality gates | [Contributing](contributing.md) |
30
+ | Repo Review | Checklist for releasing/PRs | [Repo review](repo-review.md) |
31
+ | Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](data-lifecycle.md) |
32
+
33
+ ## Minimal FastAPI bootstrap
34
+
35
+ ```python
36
+ from fastapi import Depends
37
+ from svc_infra.api.fastapi.ease import easy_service_app
38
+ from svc_infra.api.fastapi.db.sql.add import add_sql_db
39
+ from svc_infra.cache import init_cache
40
+ from svc_infra.jobs.easy import easy_jobs
41
+ from svc_infra.webhooks.fastapi import require_signature
42
+
43
+ app = easy_service_app(name="Billing", release="1.2.3")
44
+ add_sql_db(app) # reads SQL_URL / DB_* envs
45
+ init_cache() # honors CACHE_PREFIX / CACHE_VERSION
46
+ queue, scheduler = easy_jobs() # switches via JOBS_DRIVER / REDIS_URL
47
+
48
+ @app.post("/webhooks/billing")
49
+ async def handle_webhook(payload = Depends(require_signature(lambda: ["current", "next"]))):
50
+ queue.enqueue("process-billing-webhook", payload)
51
+ return {"status": "queued"}
52
+ ```
53
+
54
+ ## Environment switches
55
+
56
+ - **API** – toggle logging/observability and docs exposure with `ENABLE_LOGGING`, `LOG_LEVEL`, `LOG_FORMAT`, `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and `CORS_ALLOW_ORIGINS`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/api/fastapi/setup.py†L47-L88】
57
+ - **Auth** – configure JWT secrets, SMTP, cookies, and policy using the `AUTH_…` settings family (e.g., `AUTH_JWT__SECRET`, `AUTH_SMTP_HOST`, `AUTH_SESSION_COOKIE_SECURE`). 【F:src/svc_infra/api/fastapi/auth/settings.py†L23-L91】
58
+ - **Database** – set connection URLs or components via `SQL_URL`/`SQL_URL_FILE`, `DB_DIALECT`, `DB_HOST`, `DB_USER`, `DB_PASSWORD`, plus Mongo knobs like `MONGO_URL`, `MONGO_DB`, and `MONGO_URL_FILE`. 【F:src/svc_infra/api/fastapi/db/sql/add.py†L55-L114】【F:src/svc_infra/db/sql/utils.py†L85-L206】【F:src/svc_infra/db/nosql/mongo/settings.py†L9-L13】【F:src/svc_infra/db/nosql/utils.py†L56-L113】
59
+ - **Jobs** – choose the queue backend with `JOBS_DRIVER` and provide Redis via `REDIS_URL`; interval schedules can be declared with `JOBS_SCHEDULE_JSON`. 【F:src/svc_infra/jobs/easy.py†L11-L27】【F:src/svc_infra/docs/jobs.md†L11-L48】
60
+ - **Cache** – namespace keys and lifetimes through `CACHE_PREFIX`, `CACHE_VERSION`, and TTL overrides `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG`. 【F:src/svc_infra/cache/README.md†L20-L173】【F:src/svc_infra/cache/ttl.py†L26-L55】
61
+ - **Observability** – turn metrics on/off or adjust scrape paths with `ENABLE_OBS`, `METRICS_PATH`, `OBS_SKIP_PATHS`, and Prometheus/Grafana flags like `SVC_INFRA_DISABLE_PROMETHEUS`, `SVC_INFRA_RATE_WINDOW`, `SVC_INFRA_DASHBOARD_REFRESH`, `SVC_INFRA_DASHBOARD_RANGE`. 【F:src/svc_infra/api/fastapi/ease.py†L67-L111】【F:src/svc_infra/obs/metrics/asgi.py†L49-L206】【F:src/svc_infra/obs/cloud_dash.py†L85-L108】
62
+ - **Webhooks** – reuse the jobs envs (`JOBS_DRIVER`, `REDIS_URL`) for the delivery worker and queue configuration. 【F:src/svc_infra/docs/webhooks.md†L32-L53】
63
+ - **Security** – enforce password policy, MFA, and rotation with auth prefixes such as `AUTH_PASSWORD_MIN_LENGTH`, `AUTH_PASSWORD_REQUIRE_SYMBOL`, `AUTH_JWT__SECRET`, and `AUTH_JWT__OLD_SECRETS`. 【F:src/svc_infra/docs/security.md†L24-L70】
@@ -0,0 +1,111 @@
1
+ # Idempotency & Concurrency Controls
2
+
3
+ This guide explains how idempotency works in svc-infra and how to enable it for safe retries.
4
+
5
+ ## What it does
6
+ - Prevents duplicate processing of mutating requests (POST/PATCH/DELETE).
7
+ - Replays the previously successful response when the same `Idempotency-Key` is used.
8
+ - Detects conflicts when the same key is reused with a different request body, returning 409.
9
+
10
+ ## Middleware usage
11
+ ```python
12
+ from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
13
+
14
+ app.add_middleware(IdempotencyMiddleware) # default: in-memory store, 24h TTL
15
+ ```
16
+ - Header name: `Idempotency-Key` (configurable via `header_name`)
17
+ - TTL: defaults to 24 hours (`ttl_seconds`)
18
+
19
+ ### Redis store (recommended for multi-instance)
20
+ ```python
21
+ import redis
22
+ from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
23
+ from svc_infra.api.fastapi.middleware.idempotency_store import RedisIdempotencyStore
24
+
25
+ r = redis.Redis.from_url("redis://localhost:6379/0")
26
+ store = RedisIdempotencyStore(r, prefix="idmp")
27
+ app.add_middleware(IdempotencyMiddleware, store=store, ttl_seconds=24*3600)
28
+ ```
29
+
30
+ ## Per-route enforcement
31
+ If an endpoint must require idempotency always, add the dependency:
32
+ ```python
33
+ from fastapi import Depends
34
+ from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
35
+
36
+ @app.post("/payments/intents", dependencies=[Depends(require_idempotency_key)])
37
+ async def create_intent(...):
38
+ ...
39
+ ```
40
+
41
+ ## Semantics
42
+ - First request with a key:
43
+ - The middleware claims the key and records a hash of the request body.
44
+ - On success (2xx), the response envelope is cached until TTL.
45
+ - Replay with same key and same body:
46
+ - Returns the cached response with the original status and headers.
47
+ - Replay with same key but different body:
48
+ - Returns 409 Conflict (don’t reuse keys for different logical operations).
49
+
50
+ ## Testing
51
+ - Marker: `-m concurrency` selects concurrency tests in this repo.
52
+ - Scenarios covered:
53
+ - Successful first request and replay
54
+ - Conflict on mismatched payload reusing the same key
55
+
56
+ ## Notes and pitfalls
57
+ - Use a unique key per logical operation (e.g., `order-{id}-capture-1`).
58
+ - TTL should exceed your max retry horizon.
59
+ - For stronger guarantees in Redis, consider a Lua script to make the claim + response update atomic (future improvement).
60
+ - If you also use optimistic locking, surface 409 when `If-Match` version mismatches during updates.
61
+
62
+ ---
63
+
64
+ ## Optimistic Locking
65
+
66
+ Use the `If-Match` header and a version field on your models.
67
+
68
+ ```python
69
+ from svc_infra.api.fastapi.middleware.optimistic_lock import require_if_match, check_version_or_409
70
+
71
+ @app.patch("/resource/{rid}")
72
+ async def update_resource(rid: str, v: str = Depends(require_if_match)):
73
+ current = await repo.get(...)
74
+ check_version_or_409(lambda: current.version, v)
75
+ current.version += 1
76
+ await repo.save(current)
77
+ return {...}
78
+ ```
79
+
80
+ Pitfalls:
81
+ - Always bump the version on successful updates.
82
+ - Return 428 when `If-Match` is missing on mutating routes that require optimistic locking.
83
+ - Consider ETag headers for GETs to complement conditional requests.
84
+
85
+ ---
86
+
87
+ ## Outbox / Inbox
88
+
89
+ Outbox: record events/changes that must be delivered to external systems; a relay fetches and delivers reliably.
90
+
91
+ ```python
92
+ from svc_infra.db.outbox import InMemoryOutboxStore
93
+
94
+ ob = InMemoryOutboxStore()
95
+ msg = ob.enqueue("orders.created", {"order_id": 123})
96
+ nxt = ob.fetch_next(topics=["orders.created"]) # process
97
+ ob.mark_processed(nxt.id)
98
+ ```
99
+
100
+ Inbox: deduplicate external deliveries (e.g., webhook replays) with TTL.
101
+
102
+ ```python
103
+ from svc_infra.db.inbox import InMemoryInboxStore
104
+
105
+ ib = InMemoryInboxStore()
106
+ if not ib.mark_if_new("provider-evt-abc", ttl_seconds=86400):
107
+ return 200 # duplicate
108
+ ```
109
+
110
+ Notes:
111
+ - In-memory stores are for tests/local dev; implement SQL/Redis for production with row locks and `SKIP LOCKED` (or Lua) as needed.