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.
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/db/sql/add.py +32 -13
- svc_infra/api/fastapi/db/sql/crud_router.py +178 -16
- svc_infra/api/fastapi/db/sql/session.py +16 -0
- svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
- svc_infra/api/fastapi/docs/add.py +160 -0
- svc_infra/api/fastapi/docs/landing.py +1 -1
- svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
- svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
- svc_infra/api/fastapi/middleware/timeout.py +148 -0
- svc_infra/api/fastapi/openapi/mutators.py +114 -0
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +3 -1
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +11 -1
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/app/README.md +5 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +230 -0
- svc_infra/billing/models.py +131 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +33 -0
- svc_infra/billing/service.py +115 -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 +4 -0
- svc_infra/cache/add.py +158 -0
- svc_infra/cache/backend.py +5 -2
- svc_infra/cache/decorators.py +19 -1
- svc_infra/cache/keys.py +24 -4
- svc_infra/cli/__init__.py +28 -8
- svc_infra/cli/cmds/__init__.py +8 -0
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +53 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +40 -0
- svc_infra/data/retention.py +55 -0
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/sql/repository.py +51 -11
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +9 -1
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +9 -2
- svc_infra/db/sql/tenant.py +79 -0
- svc_infra/db/sql/utils.py +18 -4
- svc_infra/docs/acceptance-matrix.md +71 -0
- svc_infra/docs/acceptance.md +44 -0
- svc_infra/docs/admin.md +425 -0
- svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
- svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
- svc_infra/docs/adr/0004-tenancy-model.md +42 -0
- svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
- svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
- svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
- svc_infra/docs/adr/0008-billing-primitives.md +143 -0
- svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
- svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
- svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
- svc_infra/docs/api.md +59 -0
- svc_infra/docs/auth.md +11 -0
- svc_infra/docs/billing.md +190 -0
- svc_infra/docs/cache.md +76 -0
- svc_infra/docs/cli.md +74 -0
- svc_infra/docs/contributing.md +34 -0
- svc_infra/docs/data-lifecycle.md +52 -0
- svc_infra/docs/database.md +14 -0
- svc_infra/docs/docs-and-sdks.md +62 -0
- svc_infra/docs/environment.md +114 -0
- svc_infra/docs/getting-started.md +63 -0
- svc_infra/docs/idempotency.md +111 -0
- svc_infra/docs/jobs.md +67 -0
- svc_infra/docs/observability.md +16 -0
- svc_infra/docs/ops.md +37 -0
- svc_infra/docs/rate-limiting.md +125 -0
- svc_infra/docs/repo-review.md +48 -0
- svc_infra/docs/security.md +176 -0
- svc_infra/docs/tenancy.md +35 -0
- svc_infra/docs/timeouts-and-resource-limits.md +147 -0
- svc_infra/docs/webhooks.md +112 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +67 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +72 -0
- svc_infra/jobs/builtins/webhook_delivery.py +14 -2
- svc_infra/jobs/queue.py +9 -1
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/worker.py +17 -1
- svc_infra/mcp/svc_infra_mcp.py +85 -28
- svc_infra/obs/add.py +54 -7
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +6 -2
- svc_infra/security/permissions.py +1 -0
- svc_infra/webhooks/service.py +10 -2
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/METADATA +40 -14
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/RECORD +118 -44
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.600.dist-info → svc_infra-0.1.640.dist-info}/entry_points.txt +0 -0
svc_infra/docs/cache.md
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/svc-infra/)
|
|
4
|
+
[](.)
|
|
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.
|