svc-infra 0.1.628__tar.gz → 0.1.630__tar.gz
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-0.1.628 → svc_infra-0.1.630}/PKG-INFO +1 -1
- {svc_infra-0.1.628 → svc_infra-0.1.630}/pyproject.toml +3 -1
- svc_infra-0.1.630/src/svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra-0.1.630/src/svc_infra/api/fastapi/billing/setup.py +19 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/session.py +16 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +15 -0
- svc_infra-0.1.630/src/svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
- svc_infra-0.1.630/src/svc_infra/api/fastapi/middleware/timeout.py +144 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/setup.py +10 -0
- svc_infra-0.1.630/src/svc_infra/billing/async_service.py +147 -0
- svc_infra-0.1.630/src/svc_infra/billing/jobs.py +218 -0
- svc_infra-0.1.630/src/svc_infra/billing/quotas.py +101 -0
- svc_infra-0.1.630/src/svc_infra/billing/schemas.py +33 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +18 -3
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0008-billing-primitives.md +34 -0
- svc_infra-0.1.630/src/svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
- svc_infra-0.1.630/src/svc_infra/docs/billing.md +190 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/ops.md +4 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/rate-limiting.md +4 -0
- svc_infra-0.1.630/src/svc_infra/docs/timeouts-and-resource-limits.md +147 -0
- svc_infra-0.1.630/src/svc_infra/http/__init__.py +13 -0
- svc_infra-0.1.630/src/svc_infra/http/client.py +64 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/builtins/webhook_delivery.py +14 -2
- svc_infra-0.1.630/src/svc_infra/jobs/runner.py +75 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/worker.py +17 -1
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/hibp.py +6 -2
- {svc_infra-0.1.628 → svc_infra-0.1.630}/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/ops/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/tenancy/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/tenancy/context.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/billing/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/billing/models.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/billing/service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/bundled_docs/getting-started.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/docs/docs_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/dx/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/dx/dx_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/sdk/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/cmds/sdk/sdk_cmds.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/data/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/data/backup.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/data/erasure.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/data/fixtures.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/data/retention.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/repository.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/resource.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/tenant.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/acceptance-matrix.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/acceptance.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0003-webhooks-framework.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0004-tenancy-model.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0005-data-lifecycle.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0006-ops-slos-and-metrics.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0007-docs-and-sdks.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/adr/0009-acceptance-harness.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/api.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/auth.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/cache.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/cli.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/contributing.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/data-lifecycle.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/database.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/docs-and-sdks.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/environment.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/getting-started.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/idempotency.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/jobs.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/observability.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/repo-review.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/security.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/tenancy.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/docs/webhooks.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/dx/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/dx/changelog.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/dx/checks.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/grafana/dashboards/http-overview.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/permissions.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/webhooks/signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "svc-infra"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.630"
|
|
4
4
|
description = "Infrastructure for building and deploying prod-ready services"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -122,11 +122,13 @@ python_files = ["test_*.py", "*_test.py"]
|
|
|
122
122
|
python_classes = ["Test*",]
|
|
123
123
|
python_functions = ["test_*"]
|
|
124
124
|
markers = [
|
|
125
|
+
"acceptance: End-to-end acceptance tests running against the acceptance app or BASE_URL",
|
|
125
126
|
"security: Security and auth hardening tests",
|
|
126
127
|
"ratelimit: Rate limiting and abuse protection tests",
|
|
127
128
|
"concurrency: Idempotency and concurrency control tests",
|
|
128
129
|
"jobs: Background jobs and scheduling tests",
|
|
129
130
|
"webhooks: Webhooks framework tests",
|
|
131
|
+
"billing: Billing primitives tests",
|
|
130
132
|
"tenancy: Tenancy isolation and enforcement tests",
|
|
131
133
|
"data_lifecycle: Data lifecycle (fixtures, retention, erasure, backups)",
|
|
132
134
|
"ops: SLOs & Ops tests (probes, breaker, instrumentation)",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Response, status
|
|
7
|
+
|
|
8
|
+
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
9
|
+
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
10
|
+
from svc_infra.api.fastapi.tenancy.context import TenantId
|
|
11
|
+
from svc_infra.billing.async_service import AsyncBillingService
|
|
12
|
+
from svc_infra.billing.schemas import UsageAckOut, UsageAggregateRow, UsageAggregatesOut, UsageIn
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/_billing", tags=["Billing"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_service(tenant_id: TenantId, session: SqlSessionDep) -> AsyncBillingService:
|
|
18
|
+
return AsyncBillingService(session=session, tenant_id=tenant_id)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.post(
|
|
22
|
+
"/usage",
|
|
23
|
+
name="billing_record_usage",
|
|
24
|
+
status_code=status.HTTP_202_ACCEPTED,
|
|
25
|
+
response_model=UsageAckOut,
|
|
26
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
27
|
+
)
|
|
28
|
+
async def record_usage(
|
|
29
|
+
data: UsageIn, svc: Annotated[AsyncBillingService, Depends(get_service)], response: Response
|
|
30
|
+
):
|
|
31
|
+
at = data.at or datetime.now(tz=timezone.utc)
|
|
32
|
+
evt_id = await svc.record_usage(
|
|
33
|
+
metric=data.metric,
|
|
34
|
+
amount=int(data.amount),
|
|
35
|
+
at=at,
|
|
36
|
+
idempotency_key=data.idempotency_key,
|
|
37
|
+
metadata=data.metadata,
|
|
38
|
+
)
|
|
39
|
+
# For 202, no Location header is required, but we can surface the id in the body
|
|
40
|
+
return UsageAckOut(id=evt_id, accepted=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get(
|
|
44
|
+
"/usage",
|
|
45
|
+
name="billing_list_aggregates",
|
|
46
|
+
response_model=UsageAggregatesOut,
|
|
47
|
+
)
|
|
48
|
+
async def list_aggregates(
|
|
49
|
+
metric: str,
|
|
50
|
+
date_from: Optional[datetime] = None,
|
|
51
|
+
date_to: Optional[datetime] = None,
|
|
52
|
+
svc: Annotated[AsyncBillingService, Depends(get_service)] = None,
|
|
53
|
+
):
|
|
54
|
+
rows = await svc.list_daily_aggregates(metric=metric, date_from=date_from, date_to=date_to)
|
|
55
|
+
items = [
|
|
56
|
+
UsageAggregateRow(
|
|
57
|
+
period_start=r.period_start,
|
|
58
|
+
granularity=r.granularity,
|
|
59
|
+
metric=r.metric,
|
|
60
|
+
total=int(r.total),
|
|
61
|
+
)
|
|
62
|
+
for r in rows
|
|
63
|
+
]
|
|
64
|
+
return UsageAggregatesOut(items=items, next_cursor=None)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
from .router import router as billing_router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_billing(app: FastAPI, *, prefix: str = "/_billing") -> None:
|
|
9
|
+
# Mount under the chosen prefix; default is /_billing
|
|
10
|
+
if prefix and prefix != "/_billing":
|
|
11
|
+
# If a custom prefix is desired, clone router with new prefix
|
|
12
|
+
from fastapi import APIRouter
|
|
13
|
+
|
|
14
|
+
custom = APIRouter(prefix=prefix, tags=["Billing"])
|
|
15
|
+
for route in billing_router.routes:
|
|
16
|
+
custom.routes.append(route)
|
|
17
|
+
app.include_router(custom)
|
|
18
|
+
else:
|
|
19
|
+
app.include_router(billing_router)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
from typing import Annotated, AsyncIterator, Tuple
|
|
5
6
|
|
|
6
7
|
from fastapi import Depends
|
|
8
|
+
from sqlalchemy import text
|
|
7
9
|
from sqlalchemy.ext.asyncio import (
|
|
8
10
|
AsyncEngine,
|
|
9
11
|
AsyncSession,
|
|
@@ -53,6 +55,20 @@ async def get_session() -> AsyncIterator[AsyncSession]:
|
|
|
53
55
|
if _SessionLocal is None:
|
|
54
56
|
raise RuntimeError("Database not initialized. Call add_sql_db(app, ...) first.")
|
|
55
57
|
async with _SessionLocal() as session:
|
|
58
|
+
# Optional: set a per-transaction statement timeout for Postgres if configured
|
|
59
|
+
raw_ms = os.getenv("DB_STATEMENT_TIMEOUT_MS")
|
|
60
|
+
if raw_ms:
|
|
61
|
+
try:
|
|
62
|
+
ms = int(raw_ms)
|
|
63
|
+
if ms > 0:
|
|
64
|
+
try:
|
|
65
|
+
# SET LOCAL applies for the duration of the current transaction only
|
|
66
|
+
await session.execute(text("SET LOCAL statement_timeout = :ms"), {"ms": ms})
|
|
67
|
+
except Exception:
|
|
68
|
+
# Non-PG dialects (e.g., SQLite) will error; ignore silently
|
|
69
|
+
pass
|
|
70
|
+
except ValueError:
|
|
71
|
+
pass
|
|
56
72
|
try:
|
|
57
73
|
yield session
|
|
58
74
|
await session.commit()
|
{svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/errors/handlers.py
RENAMED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
import traceback
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
5
|
+
import httpx
|
|
5
6
|
from fastapi import Request
|
|
6
7
|
from fastapi.exceptions import HTTPException, RequestValidationError
|
|
7
8
|
from fastapi.responses import JSONResponse, Response
|
|
@@ -67,6 +68,20 @@ def problem_response(
|
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def register_error_handlers(app):
|
|
71
|
+
@app.exception_handler(httpx.TimeoutException)
|
|
72
|
+
async def handle_httpx_timeout(request: Request, exc: httpx.TimeoutException):
|
|
73
|
+
trace_id = _trace_id_from_request(request)
|
|
74
|
+
# Map outbound HTTP client timeouts to 504 Gateway Timeout
|
|
75
|
+
# Keep details generic in prod
|
|
76
|
+
return problem_response(
|
|
77
|
+
status=504,
|
|
78
|
+
title="Gateway Timeout",
|
|
79
|
+
detail=("Upstream request timed out." if IS_PROD else (str(exc) or "httpx timeout")),
|
|
80
|
+
code="GATEWAY_TIMEOUT",
|
|
81
|
+
instance=str(request.url),
|
|
82
|
+
trace_id=trace_id,
|
|
83
|
+
)
|
|
84
|
+
|
|
70
85
|
@app.exception_handler(FastApiException)
|
|
71
86
|
async def handle_app_exception(request: Request, exc: FastApiException):
|
|
72
87
|
trace_id = _trace_id_from_request(request)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from svc_infra.app.env import pick
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_grace_period_seconds() -> float:
|
|
18
|
+
default = pick(prod=20.0, nonprod=5.0)
|
|
19
|
+
raw = os.getenv("SHUTDOWN_GRACE_PERIOD_SECONDS")
|
|
20
|
+
if raw is None or raw == "":
|
|
21
|
+
return float(default)
|
|
22
|
+
try:
|
|
23
|
+
return float(raw)
|
|
24
|
+
except ValueError:
|
|
25
|
+
return float(default)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InflightTrackerMiddleware:
|
|
29
|
+
"""Tracks number of in-flight requests to support graceful shutdown drains."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, app: ASGIApp):
|
|
32
|
+
self.app = app
|
|
33
|
+
|
|
34
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
35
|
+
if scope.get("type") != "http":
|
|
36
|
+
await self.app(scope, receive, send)
|
|
37
|
+
return
|
|
38
|
+
state = scope.get("app").state # type: ignore[attr-defined]
|
|
39
|
+
state._inflight_requests = getattr(state, "_inflight_requests", 0) + 1
|
|
40
|
+
try:
|
|
41
|
+
await self.app(scope, receive, send)
|
|
42
|
+
finally:
|
|
43
|
+
state._inflight_requests = max(0, getattr(state, "_inflight_requests", 1) - 1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _wait_for_drain(app: FastAPI, grace: float) -> None:
|
|
47
|
+
interval = 0.1
|
|
48
|
+
waited = 0.0
|
|
49
|
+
while waited < grace:
|
|
50
|
+
inflight = int(getattr(app.state, "_inflight_requests", 0))
|
|
51
|
+
if inflight <= 0:
|
|
52
|
+
return
|
|
53
|
+
await asyncio.sleep(interval)
|
|
54
|
+
waited += interval
|
|
55
|
+
inflight = int(getattr(app.state, "_inflight_requests", 0))
|
|
56
|
+
if inflight > 0:
|
|
57
|
+
logger.warning(
|
|
58
|
+
"Graceful shutdown timeout: %s in-flight request(s) after %.2fs", inflight, waited
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def install_graceful_shutdown(app: FastAPI, *, grace_seconds: Optional[float] = None) -> None:
|
|
63
|
+
"""Install inflight tracking and lifespan hooks to wait for requests to drain.
|
|
64
|
+
|
|
65
|
+
- Adds InflightTrackerMiddleware
|
|
66
|
+
- Registers a lifespan handler that initializes state and waits up to grace_seconds on shutdown
|
|
67
|
+
"""
|
|
68
|
+
app.add_middleware(InflightTrackerMiddleware)
|
|
69
|
+
|
|
70
|
+
g = float(grace_seconds) if grace_seconds is not None else _get_grace_period_seconds()
|
|
71
|
+
|
|
72
|
+
# Preserve any existing lifespan and wrap it so our drain runs on shutdown.
|
|
73
|
+
previous_lifespan = getattr(app.router, "lifespan_context", None)
|
|
74
|
+
|
|
75
|
+
@asynccontextmanager
|
|
76
|
+
async def _lifespan(a: FastAPI): # noqa: ANN202
|
|
77
|
+
# Startup: initialize inflight counter
|
|
78
|
+
a.state._inflight_requests = 0
|
|
79
|
+
if previous_lifespan is not None:
|
|
80
|
+
async with previous_lifespan(a):
|
|
81
|
+
yield
|
|
82
|
+
else:
|
|
83
|
+
yield
|
|
84
|
+
# Shutdown: wait for in-flight requests to drain (up to grace period)
|
|
85
|
+
await _wait_for_drain(a, g)
|
|
86
|
+
|
|
87
|
+
app.router.lifespan_context = _lifespan
|
{svc_infra-0.1.628 → svc_infra-0.1.630}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py
RENAMED
|
@@ -16,14 +16,20 @@ class RateLimitStore(Protocol):
|
|
|
16
16
|
class InMemoryRateLimitStore:
|
|
17
17
|
def __init__(self, limit: int = 120):
|
|
18
18
|
self.limit = limit
|
|
19
|
-
|
|
19
|
+
# Track per-key rolling windows: key -> (count, window_start_epoch)
|
|
20
|
+
self._state: dict[str, tuple[int, float]] = {}
|
|
20
21
|
|
|
21
22
|
def incr(self, key: str, window: int) -> Tuple[int, int, int]:
|
|
22
|
-
now =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
now = time.time()
|
|
24
|
+
count, window_start = self._state.get(key, (0, now))
|
|
25
|
+
# If outside the rolling window, reset
|
|
26
|
+
if now >= window_start + window:
|
|
27
|
+
count = 1
|
|
28
|
+
window_start = now
|
|
29
|
+
else:
|
|
30
|
+
count += 1
|
|
31
|
+
self._state[key] = (count, window_start)
|
|
32
|
+
reset = int(window_start + window)
|
|
27
33
|
return count, self.limit, reset
|
|
28
34
|
|
|
29
35
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
8
|
+
|
|
9
|
+
from svc_infra.api.fastapi.middleware.errors.handlers import problem_response
|
|
10
|
+
from svc_infra.app.env import pick
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _env_int(name: str, default: int) -> int:
|
|
14
|
+
v = os.getenv(name)
|
|
15
|
+
if v is None:
|
|
16
|
+
return default
|
|
17
|
+
try:
|
|
18
|
+
return int(v)
|
|
19
|
+
except Exception:
|
|
20
|
+
return default
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
REQUEST_BODY_TIMEOUT_SECONDS: int = pick(
|
|
24
|
+
prod=_env_int("REQUEST_BODY_TIMEOUT_SECONDS", 15),
|
|
25
|
+
nonprod=_env_int("REQUEST_BODY_TIMEOUT_SECONDS", 30),
|
|
26
|
+
)
|
|
27
|
+
REQUEST_TIMEOUT_SECONDS: int = pick(
|
|
28
|
+
prod=_env_int("REQUEST_TIMEOUT_SECONDS", 30),
|
|
29
|
+
nonprod=_env_int("REQUEST_TIMEOUT_SECONDS", 15),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HandlerTimeoutMiddleware:
|
|
34
|
+
"""
|
|
35
|
+
Caps total handler execution time. If exceeded, returns 504 Problem+JSON.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, app: ASGIApp, timeout_seconds: int | None = None) -> None:
|
|
39
|
+
self.app = app
|
|
40
|
+
self.timeout_seconds = timeout_seconds or REQUEST_TIMEOUT_SECONDS
|
|
41
|
+
|
|
42
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
43
|
+
if scope.get("type") != "http":
|
|
44
|
+
await self.app(scope, receive, send)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
async def _call_next() -> None:
|
|
48
|
+
await self.app(scope, receive, send)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
await asyncio.wait_for(_call_next(), timeout=self.timeout_seconds)
|
|
52
|
+
except asyncio.TimeoutError:
|
|
53
|
+
# Build a minimal Request to extract headers and URL for trace info
|
|
54
|
+
request = Request(scope, receive=receive)
|
|
55
|
+
trace_id = None
|
|
56
|
+
for h in ("x-request-id", "x-correlation-id", "x-trace-id"):
|
|
57
|
+
v = request.headers.get(h)
|
|
58
|
+
if v:
|
|
59
|
+
trace_id = v
|
|
60
|
+
break
|
|
61
|
+
resp = problem_response(
|
|
62
|
+
status=504,
|
|
63
|
+
title="Gateway Timeout",
|
|
64
|
+
detail="The request took too long to complete.",
|
|
65
|
+
code="GATEWAY_TIMEOUT",
|
|
66
|
+
instance=str(request.url),
|
|
67
|
+
trace_id=trace_id,
|
|
68
|
+
)
|
|
69
|
+
await resp(scope, receive, send)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BodyReadTimeoutMiddleware:
|
|
73
|
+
"""
|
|
74
|
+
Enforces a timeout while reading the request body to mitigate slowloris.
|
|
75
|
+
If body read does not make progress within the timeout, returns 408 Problem+JSON.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, app: ASGIApp, timeout_seconds: int | None = None) -> None:
|
|
79
|
+
self.app = app
|
|
80
|
+
self.timeout_seconds = timeout_seconds or REQUEST_BODY_TIMEOUT_SECONDS
|
|
81
|
+
|
|
82
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
83
|
+
if scope.get("type") != "http":
|
|
84
|
+
await self.app(scope, receive, send)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Strategy: greedily drain the incoming request body here while enforcing
|
|
88
|
+
# per-receive timeout, then replay it to the downstream app from a buffer.
|
|
89
|
+
# This ensures we can detect slowloris-style uploads even if the app only
|
|
90
|
+
# reads the body later (after the server has finished buffering).
|
|
91
|
+
buffered = bytearray()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
while True:
|
|
95
|
+
message = await asyncio.wait_for(receive(), timeout=self.timeout_seconds)
|
|
96
|
+
|
|
97
|
+
mtype = message.get("type")
|
|
98
|
+
if mtype == "http.request":
|
|
99
|
+
chunk = message.get("body", b"") or b""
|
|
100
|
+
if chunk:
|
|
101
|
+
buffered.extend(chunk)
|
|
102
|
+
# Stop when server indicates no more body
|
|
103
|
+
if not message.get("more_body", False):
|
|
104
|
+
break
|
|
105
|
+
# else: continue reading remaining chunks with timeout
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if mtype == "http.disconnect": # client disconnected mid-upload
|
|
109
|
+
# Treat as end of body for the purposes of replay; downstream
|
|
110
|
+
# will see an empty body. No timeout response needed here.
|
|
111
|
+
break
|
|
112
|
+
# Ignore other message types and continue
|
|
113
|
+
except asyncio.TimeoutError:
|
|
114
|
+
# Timed out while waiting for the next body chunk → return 408
|
|
115
|
+
request = Request(scope, receive=receive)
|
|
116
|
+
trace_id = None
|
|
117
|
+
for h in ("x-request-id", "x-correlation-id", "x-trace-id"):
|
|
118
|
+
v = request.headers.get(h)
|
|
119
|
+
if v:
|
|
120
|
+
trace_id = v
|
|
121
|
+
break
|
|
122
|
+
resp = problem_response(
|
|
123
|
+
status=408,
|
|
124
|
+
title="Request Timeout",
|
|
125
|
+
detail="Timed out while reading request body.",
|
|
126
|
+
code="REQUEST_TIMEOUT",
|
|
127
|
+
instance=str(request.url),
|
|
128
|
+
trace_id=trace_id,
|
|
129
|
+
)
|
|
130
|
+
await resp(scope, receive, send)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Replay the drained body to the app as a single http.request message.
|
|
134
|
+
sent = False
|
|
135
|
+
|
|
136
|
+
async def _replay_receive() -> dict:
|
|
137
|
+
nonlocal sent
|
|
138
|
+
if not sent:
|
|
139
|
+
sent = True
|
|
140
|
+
return {"type": "http.request", "body": bytes(buffered), "more_body": False}
|
|
141
|
+
# Subsequent calls return an empty terminal body event
|
|
142
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
143
|
+
|
|
144
|
+
await self.app(scope, _replay_receive, send)
|
|
@@ -14,9 +14,14 @@ from svc_infra.api.fastapi.docs.landing import CardSpec, DocTargets, render_inde
|
|
|
14
14
|
from svc_infra.api.fastapi.docs.scoped import DOC_SCOPES
|
|
15
15
|
from svc_infra.api.fastapi.middleware.errors.catchall import CatchAllExceptionMiddleware
|
|
16
16
|
from svc_infra.api.fastapi.middleware.errors.handlers import register_error_handlers
|
|
17
|
+
from svc_infra.api.fastapi.middleware.graceful_shutdown import install_graceful_shutdown
|
|
17
18
|
from svc_infra.api.fastapi.middleware.idempotency import IdempotencyMiddleware
|
|
18
19
|
from svc_infra.api.fastapi.middleware.ratelimit import SimpleRateLimitMiddleware
|
|
19
20
|
from svc_infra.api.fastapi.middleware.request_id import RequestIdMiddleware
|
|
21
|
+
from svc_infra.api.fastapi.middleware.timeout import (
|
|
22
|
+
BodyReadTimeoutMiddleware,
|
|
23
|
+
HandlerTimeoutMiddleware,
|
|
24
|
+
)
|
|
20
25
|
from svc_infra.api.fastapi.openapi.models import APIVersionSpec, ServiceInfo
|
|
21
26
|
from svc_infra.api.fastapi.openapi.mutators import setup_mutators
|
|
22
27
|
from svc_infra.api.fastapi.openapi.pipeline import apply_mutators
|
|
@@ -79,11 +84,16 @@ def _setup_cors(app: FastAPI, public_cors_origins: list[str] | str | None = None
|
|
|
79
84
|
|
|
80
85
|
def _setup_middlewares(app: FastAPI):
|
|
81
86
|
app.add_middleware(RequestIdMiddleware)
|
|
87
|
+
# Timeouts: enforce body read timeout first, then total handler timeout
|
|
88
|
+
app.add_middleware(BodyReadTimeoutMiddleware)
|
|
89
|
+
app.add_middleware(HandlerTimeoutMiddleware)
|
|
82
90
|
app.add_middleware(CatchAllExceptionMiddleware)
|
|
83
91
|
app.add_middleware(IdempotencyMiddleware)
|
|
84
92
|
app.add_middleware(SimpleRateLimitMiddleware)
|
|
85
93
|
register_error_handlers(app)
|
|
86
94
|
_add_route_logger(app)
|
|
95
|
+
# Graceful shutdown: track in-flight and wait on shutdown
|
|
96
|
+
install_graceful_shutdown(app)
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
def _coerce_list(value: str | Iterable[str] | None) -> list[str]:
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import Optional, Sequence
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
|
|
10
|
+
from .models import Invoice, InvoiceLine, UsageAggregate, UsageEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncBillingService:
|
|
14
|
+
def __init__(self, session: AsyncSession, tenant_id: str):
|
|
15
|
+
self.session = session
|
|
16
|
+
self.tenant_id = tenant_id
|
|
17
|
+
|
|
18
|
+
async def record_usage(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
metric: str,
|
|
22
|
+
amount: int,
|
|
23
|
+
at: datetime,
|
|
24
|
+
idempotency_key: str,
|
|
25
|
+
metadata: dict | None,
|
|
26
|
+
) -> str:
|
|
27
|
+
if at.tzinfo is None:
|
|
28
|
+
at = at.replace(tzinfo=timezone.utc)
|
|
29
|
+
evt = UsageEvent(
|
|
30
|
+
id=str(uuid.uuid4()),
|
|
31
|
+
tenant_id=self.tenant_id,
|
|
32
|
+
metric=metric,
|
|
33
|
+
amount=amount,
|
|
34
|
+
at_ts=at,
|
|
35
|
+
idempotency_key=idempotency_key,
|
|
36
|
+
metadata_json=metadata or {},
|
|
37
|
+
)
|
|
38
|
+
self.session.add(evt)
|
|
39
|
+
await self.session.flush()
|
|
40
|
+
return evt.id
|
|
41
|
+
|
|
42
|
+
async def aggregate_daily(self, *, metric: str, day_start: datetime) -> int:
|
|
43
|
+
day_start = day_start.replace(
|
|
44
|
+
hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc
|
|
45
|
+
)
|
|
46
|
+
next_day = day_start + timedelta(days=1)
|
|
47
|
+
total = 0
|
|
48
|
+
rows: Sequence[UsageEvent] = (
|
|
49
|
+
(
|
|
50
|
+
await self.session.execute(
|
|
51
|
+
select(UsageEvent).where(
|
|
52
|
+
UsageEvent.tenant_id == self.tenant_id,
|
|
53
|
+
UsageEvent.metric == metric,
|
|
54
|
+
UsageEvent.at_ts >= day_start,
|
|
55
|
+
UsageEvent.at_ts < next_day,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
.scalars()
|
|
60
|
+
.all()
|
|
61
|
+
)
|
|
62
|
+
for r in rows:
|
|
63
|
+
total += int(r.amount)
|
|
64
|
+
|
|
65
|
+
agg = (
|
|
66
|
+
await self.session.execute(
|
|
67
|
+
select(UsageAggregate).where(
|
|
68
|
+
UsageAggregate.tenant_id == self.tenant_id,
|
|
69
|
+
UsageAggregate.metric == metric,
|
|
70
|
+
UsageAggregate.period_start == day_start,
|
|
71
|
+
UsageAggregate.granularity == "day",
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
).scalar_one_or_none()
|
|
75
|
+
if agg:
|
|
76
|
+
agg.total = total
|
|
77
|
+
else:
|
|
78
|
+
self.session.add(
|
|
79
|
+
UsageAggregate(
|
|
80
|
+
id=str(uuid.uuid4()),
|
|
81
|
+
tenant_id=self.tenant_id,
|
|
82
|
+
metric=metric,
|
|
83
|
+
period_start=day_start,
|
|
84
|
+
granularity="day",
|
|
85
|
+
total=total,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return total
|
|
89
|
+
|
|
90
|
+
async def list_daily_aggregates(
|
|
91
|
+
self, *, metric: str, date_from: Optional[datetime], date_to: Optional[datetime]
|
|
92
|
+
) -> list[UsageAggregate]:
|
|
93
|
+
q = select(UsageAggregate).where(
|
|
94
|
+
UsageAggregate.tenant_id == self.tenant_id,
|
|
95
|
+
UsageAggregate.metric == metric,
|
|
96
|
+
UsageAggregate.granularity == "day",
|
|
97
|
+
)
|
|
98
|
+
if date_from is not None:
|
|
99
|
+
q = q.where(UsageAggregate.period_start >= date_from)
|
|
100
|
+
if date_to is not None:
|
|
101
|
+
q = q.where(UsageAggregate.period_start < date_to)
|
|
102
|
+
rows: list[UsageAggregate] = (await self.session.execute(q)).scalars().all()
|
|
103
|
+
return rows
|
|
104
|
+
|
|
105
|
+
async def generate_monthly_invoice(
|
|
106
|
+
self, *, period_start: datetime, period_end: datetime, currency: str
|
|
107
|
+
) -> str:
|
|
108
|
+
total = 0
|
|
109
|
+
aggs: Sequence[UsageAggregate] = (
|
|
110
|
+
(
|
|
111
|
+
await self.session.execute(
|
|
112
|
+
select(UsageAggregate).where(
|
|
113
|
+
UsageAggregate.tenant_id == self.tenant_id,
|
|
114
|
+
UsageAggregate.period_start >= period_start,
|
|
115
|
+
UsageAggregate.period_start < period_end,
|
|
116
|
+
UsageAggregate.granularity == "day",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
.scalars()
|
|
121
|
+
.all()
|
|
122
|
+
)
|
|
123
|
+
for r in aggs:
|
|
124
|
+
total += int(r.total)
|
|
125
|
+
|
|
126
|
+
inv = Invoice(
|
|
127
|
+
id=str(uuid.uuid4()),
|
|
128
|
+
tenant_id=self.tenant_id,
|
|
129
|
+
period_start=period_start,
|
|
130
|
+
period_end=period_end,
|
|
131
|
+
status="created",
|
|
132
|
+
total_amount=total,
|
|
133
|
+
currency=currency,
|
|
134
|
+
)
|
|
135
|
+
self.session.add(inv)
|
|
136
|
+
await self.session.flush()
|
|
137
|
+
|
|
138
|
+
line = InvoiceLine(
|
|
139
|
+
id=str(uuid.uuid4()),
|
|
140
|
+
invoice_id=inv.id,
|
|
141
|
+
price_id=None,
|
|
142
|
+
metric=None,
|
|
143
|
+
quantity=1,
|
|
144
|
+
amount=total,
|
|
145
|
+
)
|
|
146
|
+
self.session.add(line)
|
|
147
|
+
return inv.id
|