svc-infra 0.1.506__py3-none-any.whl → 0.1.654__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.
- svc_infra/apf_payments/README.md +732 -0
- svc_infra/apf_payments/alembic.py +11 -0
- svc_infra/apf_payments/models.py +339 -0
- svc_infra/apf_payments/provider/__init__.py +4 -0
- svc_infra/apf_payments/provider/aiydan.py +797 -0
- svc_infra/apf_payments/provider/base.py +270 -0
- svc_infra/apf_payments/provider/registry.py +31 -0
- svc_infra/apf_payments/provider/stripe.py +873 -0
- svc_infra/apf_payments/schemas.py +333 -0
- svc_infra/apf_payments/service.py +892 -0
- svc_infra/apf_payments/settings.py +67 -0
- svc_infra/api/fastapi/__init__.py +6 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- svc_infra/api/fastapi/apf_payments/router.py +1082 -0
- svc_infra/api/fastapi/apf_payments/setup.py +73 -0
- svc_infra/api/fastapi/auth/add.py +15 -6
- svc_infra/api/fastapi/auth/gaurd.py +67 -5
- svc_infra/api/fastapi/auth/mfa/router.py +18 -9
- svc_infra/api/fastapi/auth/routers/account.py +3 -2
- svc_infra/api/fastapi/auth/routers/apikey_router.py +11 -5
- svc_infra/api/fastapi/auth/routers/oauth_router.py +82 -37
- svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
- svc_infra/api/fastapi/auth/security.py +3 -1
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/auth/state.py +1 -1
- svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
- svc_infra/api/fastapi/db/sql/add.py +40 -18
- svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
- svc_infra/api/fastapi/db/sql/session.py +16 -0
- svc_infra/api/fastapi/db/sql/users.py +14 -2
- svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
- svc_infra/api/fastapi/docs/add.py +160 -0
- svc_infra/api/fastapi/docs/landing.py +1 -1
- svc_infra/api/fastapi/docs/scoped.py +254 -0
- svc_infra/api/fastapi/dual/dualize.py +38 -33
- svc_infra/api/fastapi/dual/router.py +48 -1
- svc_infra/api/fastapi/dx.py +3 -3
- svc_infra/api/fastapi/http/__init__.py +0 -0
- svc_infra/api/fastapi/http/concurrency.py +14 -0
- svc_infra/api/fastapi/http/conditional.py +33 -0
- svc_infra/api/fastapi/http/deprecation.py +21 -0
- 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/idempotency.py +116 -0
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +37 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +119 -0
- svc_infra/api/fastapi/middleware/ratelimit_store.py +84 -0
- svc_infra/api/fastapi/middleware/request_id.py +23 -0
- svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- svc_infra/api/fastapi/middleware/timeout.py +148 -0
- svc_infra/api/fastapi/openapi/mutators.py +768 -55
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +363 -0
- svc_infra/api/fastapi/paths/auth.py +14 -14
- svc_infra/api/fastapi/paths/prefix.py +0 -1
- svc_infra/api/fastapi/paths/user.py +1 -1
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +48 -15
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/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 +32 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +10 -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 +120 -14
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +5 -4
- 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/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +43 -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/inbox.py +67 -0
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/outbox.py +104 -0
- svc_infra/db/sql/apikey.py +1 -1
- svc_infra/db/sql/authref.py +61 -0
- svc_infra/db/sql/core.py +2 -2
- svc_infra/db/sql/repository.py +52 -12
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/scaffold.py +16 -4
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +199 -76
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +231 -79
- svc_infra/db/sql/tenant.py +79 -0
- svc_infra/db/sql/utils.py +18 -4
- svc_infra/db/sql/versioning.py +14 -0
- 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/versioned-integrations.md +146 -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/outbox_processor.py +38 -0
- svc_infra/jobs/builtins/webhook_delivery.py +90 -0
- svc_infra/jobs/easy.py +32 -0
- svc_infra/jobs/loader.py +45 -0
- svc_infra/jobs/queue.py +81 -0
- svc_infra/jobs/redis_queue.py +191 -0
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/scheduler.py +41 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/mcp/svc_infra_mcp.py +85 -28
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +54 -7
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +53 -0
- svc_infra/obs/metrics.py +52 -0
- svc_infra/security/add.py +201 -0
- svc_infra/security/audit.py +130 -0
- svc_infra/security/audit_service.py +73 -0
- svc_infra/security/headers.py +52 -0
- svc_infra/security/hibp.py +95 -0
- svc_infra/security/jwt_rotation.py +53 -0
- svc_infra/security/lockout.py +96 -0
- svc_infra/security/models.py +255 -0
- svc_infra/security/org_invites.py +128 -0
- svc_infra/security/passwords.py +77 -0
- svc_infra/security/permissions.py +149 -0
- svc_infra/security/session.py +98 -0
- svc_infra/security/signed_cookies.py +80 -0
- svc_infra/webhooks/__init__.py +16 -0
- svc_infra/webhooks/add.py +322 -0
- svc_infra/webhooks/fastapi.py +37 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +67 -0
- svc_infra/webhooks/signing.py +30 -0
- svc_infra-0.1.654.dist-info/METADATA +154 -0
- svc_infra-0.1.654.dist-info/RECORD +352 -0
- svc_infra/api/fastapi/deps.py +0 -3
- svc_infra-0.1.506.dist-info/METADATA +0 -78
- svc_infra-0.1.506.dist-info/RECORD +0 -213
- /svc_infra/{api/fastapi/schemas → apf_payments}/__init__.py +0 -0
- {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Billing Primitives
|
|
2
|
+
|
|
3
|
+
This module provides internal-first billing building blocks for services that need usage-based and subscription billing without coupling to a specific provider. It complements APF Payments (provider-facing) with portable primitives you can use regardless of Stripe/Aiydan/etc.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
- Usage ingestion with idempotency (UsageEvent)
|
|
8
|
+
- Windowed usage aggregation (UsageAggregate) — daily baseline
|
|
9
|
+
- Plan and entitlements registry (Plan, PlanEntitlement)
|
|
10
|
+
- Tenant subscriptions (Subscription)
|
|
11
|
+
- Price catalog for fixed/usage items (Price)
|
|
12
|
+
- Invoice and line items (Invoice, InvoiceLine)
|
|
13
|
+
- A small `BillingService` to record usage, aggregate, and generate monthly invoices
|
|
14
|
+
- Optional provider sync hook to mirror internal invoices/lines to your payment provider
|
|
15
|
+
|
|
16
|
+
## Data model (SQL)
|
|
17
|
+
|
|
18
|
+
Tables (v1):
|
|
19
|
+
- usage_events(id, tenant_id, metric, amount, at_ts, idempotency_key, metadata_json, created_at)
|
|
20
|
+
- Unique (tenant_id, metric, idempotency_key)
|
|
21
|
+
- usage_aggregates(id, tenant_id, metric, period_start, granularity, total, updated_at)
|
|
22
|
+
- Unique (tenant_id, metric, period_start, granularity)
|
|
23
|
+
- plans(id, key, name, description, created_at)
|
|
24
|
+
- plan_entitlements(id, plan_id, key, limit_per_window, window, created_at)
|
|
25
|
+
- subscriptions(id, tenant_id, plan_id, effective_at, ended_at, created_at)
|
|
26
|
+
- prices(id, key, currency, unit_amount, metric, recurring_interval, created_at)
|
|
27
|
+
- invoices(id, tenant_id, period_start, period_end, status, total_amount, currency, provider_invoice_id, created_at)
|
|
28
|
+
- invoice_lines(id, invoice_id, price_id, metric, quantity, amount, created_at)
|
|
29
|
+
|
|
30
|
+
See `src/svc_infra/billing/models.py` for full definitions.
|
|
31
|
+
|
|
32
|
+
## Quick start (Python)
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from sqlalchemy.orm import Session
|
|
37
|
+
from svc_infra.billing import BillingService
|
|
38
|
+
|
|
39
|
+
# session: SQLAlchemy Session (sync) targeting your DB
|
|
40
|
+
bs = BillingService(session=session, tenant_id="t_123")
|
|
41
|
+
|
|
42
|
+
# 1) Record usage (idempotent by (tenant, metric, idempotency_key))
|
|
43
|
+
evt_id = bs.record_usage(
|
|
44
|
+
metric="tokens", amount=42,
|
|
45
|
+
at=datetime.now(tz=timezone.utc),
|
|
46
|
+
idempotency_key="req-42",
|
|
47
|
+
metadata={"model": "gpt"},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 2) Aggregate for a day (baseline v1 granularity)
|
|
51
|
+
bs.aggregate_daily(metric="tokens", day_start=datetime(2025,1,1,tzinfo=timezone.utc))
|
|
52
|
+
|
|
53
|
+
# 3) Generate a monthly invoice (fixed+usage lines TBD)
|
|
54
|
+
inv_id = bs.generate_monthly_invoice(
|
|
55
|
+
period_start=datetime(2025,1,1,tzinfo=timezone.utc),
|
|
56
|
+
period_end=datetime(2025,2,1,tzinfo=timezone.utc),
|
|
57
|
+
currency="usd",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Optional: pass a provider sync hook if you want to mirror invoices/lines to Stripe/Aiydan:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from typing import Callable
|
|
65
|
+
from svc_infra.billing.models import Invoice, InvoiceLine
|
|
66
|
+
|
|
67
|
+
async def sync_to_provider(inv: Invoice, lines: list[InvoiceLine]):
|
|
68
|
+
# Map internal invoice/lines to provider calls here
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
bs = BillingService(session=session, tenant_id="t_123", provider_sync=sync_to_provider)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### FastAPI router (usage ingestion & aggregates)
|
|
75
|
+
|
|
76
|
+
Mount the router and start recording usage with idempotency:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fastapi import FastAPI
|
|
80
|
+
from svc_infra.api.fastapi.billing.setup import add_billing
|
|
81
|
+
from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
|
|
82
|
+
from svc_infra.api.fastapi.middleware.errors.handlers import register_error_handlers
|
|
83
|
+
|
|
84
|
+
app = FastAPI()
|
|
85
|
+
app.add_middleware(IdempotencyMiddleware, store={})
|
|
86
|
+
register_error_handlers(app)
|
|
87
|
+
add_billing(app) # mounts under /_billing
|
|
88
|
+
|
|
89
|
+
# POST /_billing/usage {metric, amount, at?, idempotency_key, metadata?} -> 202 {id}
|
|
90
|
+
# GET /_billing/usage?metric=tokens -> {items: [{period_start, granularity, metric, total}], next_cursor}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Quotas (soft/hard limits)
|
|
94
|
+
|
|
95
|
+
Protect your feature endpoints with a quota dependency based on internal plan entitlements and daily aggregates:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from fastapi import Depends
|
|
99
|
+
from svc_infra.billing.quotas import require_quota
|
|
100
|
+
|
|
101
|
+
@app.get("/generate-report", dependencies=[Depends(require_quota("reports", window="day", soft=False))])
|
|
102
|
+
async def generate_report():
|
|
103
|
+
return {"ok": True}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Relationship to APF Payments
|
|
107
|
+
|
|
108
|
+
- APF Payments is provider-facing: customers, intents, methods, products/prices, subscriptions, invoices, usage records via Stripe/Aiydan adapters and HTTP routers.
|
|
109
|
+
- Billing Primitives is provider-agnostic: an internal ledger of usage, plans/entitlements, and invoices that you can keep even if you change providers.
|
|
110
|
+
- You can use both: continue to use APF Payments for card/payments flows, and use Billing to meter custom features and create internal invoices; selectively sync them out later.
|
|
111
|
+
|
|
112
|
+
## Jobs and webhooks
|
|
113
|
+
|
|
114
|
+
Billing includes helpers to enqueue and process jobs and emit webhooks:
|
|
115
|
+
|
|
116
|
+
- Job names:
|
|
117
|
+
- `billing.aggregate_daily` payload: `{tenant_id, metric, day_start: ISO8601}`
|
|
118
|
+
- `billing.generate_monthly_invoice` payload: `{tenant_id, period_start: ISO8601, period_end: ISO8601, currency}`
|
|
119
|
+
- Emitted webhook topics:
|
|
120
|
+
- `billing.usage_aggregated` payload: `{tenant_id, metric, day_start, total}`
|
|
121
|
+
- `billing.invoice.created` payload: `{tenant_id, invoice_id, period_start, period_end, currency}`
|
|
122
|
+
|
|
123
|
+
Usage with the built-in queue/scheduler and webhooks outbox:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
127
|
+
from svc_infra.jobs.easy import easy_jobs
|
|
128
|
+
from svc_infra.webhooks.add import add_webhooks
|
|
129
|
+
from svc_infra.webhooks.service import WebhookService
|
|
130
|
+
from svc_infra.db.outbox import InMemoryOutboxStore
|
|
131
|
+
from svc_infra.webhooks.service import InMemoryWebhookSubscriptions
|
|
132
|
+
from svc_infra.billing.jobs import (
|
|
133
|
+
enqueue_aggregate_daily,
|
|
134
|
+
enqueue_generate_monthly_invoice,
|
|
135
|
+
make_billing_job_handler,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Create queue + scheduler
|
|
139
|
+
queue, scheduler = easy_jobs()
|
|
140
|
+
|
|
141
|
+
# Setup DB async session factory
|
|
142
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
143
|
+
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
144
|
+
|
|
145
|
+
# Setup webhooks (in-memory stores shown here)
|
|
146
|
+
outbox = InMemoryOutboxStore()
|
|
147
|
+
subs = InMemoryWebhookSubscriptions()
|
|
148
|
+
subs.add("billing.usage_aggregated", url="https://example.test/hook", secret="sekrit")
|
|
149
|
+
webhooks = WebhookService(outbox=outbox, subs=subs)
|
|
150
|
+
|
|
151
|
+
# Worker handler
|
|
152
|
+
handler = make_billing_job_handler(session_factory=SessionLocal, webhooks=webhooks)
|
|
153
|
+
|
|
154
|
+
# Enqueue example jobs
|
|
155
|
+
from datetime import datetime, timezone
|
|
156
|
+
enqueue_aggregate_daily(queue, tenant_id="t1", metric="tokens", day_start=datetime.now(timezone.utc))
|
|
157
|
+
enqueue_generate_monthly_invoice(
|
|
158
|
+
queue, tenant_id="t1", period_start=datetime(2025,1,1,tzinfo=timezone.utc), period_end=datetime(2025,2,1,tzinfo=timezone.utc), currency="usd"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# In your worker loop call process_one(queue, handler)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Roadmap (v1 scope)
|
|
165
|
+
|
|
166
|
+
- Router: `/_billing` endpoints for usage ingestion (idempotent), aggregate listing, plans/subscriptions read.
|
|
167
|
+
- Quotas: decorator/dependency to enforce per-plan limits (soft/hard, day/month windows).
|
|
168
|
+
- Jobs: integrate aggregation and invoice-generation with the scheduler; emit `billing.*` webhooks. (helpers available in `svc_infra.billing.jobs`) — Implemented.
|
|
169
|
+
- Provider sync: optional mapper to Stripe invoices/payment intents; reuse idempotency.
|
|
170
|
+
- Migrations: author initial Alembic migration for billing tables.
|
|
171
|
+
- Docs: examples for quotas and jobs; admin flows for plans and prices.
|
|
172
|
+
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
- See `tests/unit/billing/test_billing_service.py` for usage, aggregation, invoice basics, and idempotency uniqueness.
|
|
176
|
+
- Additions planned: router tests (ingest/list), quotas, job executions, webhook events.
|
|
177
|
+
|
|
178
|
+
## Security & Tenancy
|
|
179
|
+
|
|
180
|
+
- All records are tenant-scoped; ensure tenant_id is enforced in your service layer / router dependencies.
|
|
181
|
+
- Protect HTTP endpoints with RBAC permissions (e.g., billing.read, billing.write) if you expose them.
|
|
182
|
+
|
|
183
|
+
## Observability
|
|
184
|
+
|
|
185
|
+
Planned metrics (names may evolve):
|
|
186
|
+
- billing_usage_ingest_total
|
|
187
|
+
- billing_aggregate_duration_ms
|
|
188
|
+
- billing_invoice_generated_total
|
|
189
|
+
|
|
190
|
+
See ADR 0008 for design details.
|
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.
|