svc-infra 0.1.603__tar.gz → 0.1.605__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.603 → svc_infra-0.1.605}/PKG-INFO +3 -1
- {svc_infra-0.1.603 → svc_infra-0.1.605}/README.md +2 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/pyproject.toml +3 -1
- svc_infra-0.1.605/src/svc_infra/api/fastapi/docs/add.py +160 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/mutators.py +111 -0
- svc_infra-0.1.605/src/svc_infra/billing/__init__.py +23 -0
- svc_infra-0.1.605/src/svc_infra/billing/models.py +131 -0
- svc_infra-0.1.605/src/svc_infra/billing/service.py +115 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/__init__.py +8 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/__init__.py +4 -0
- svc_infra-0.1.605/src/svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra-0.1.605/src/svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
- svc_infra-0.1.605/src/svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra-0.1.605/src/svc_infra/dx/changelog.py +74 -0
- svc_infra-0.1.605/src/svc_infra/dx/checks.py +67 -0
- svc_infra-0.1.605/src/svc_infra/obs/add.py +115 -0
- svc_infra-0.1.605/src/svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra-0.1.605/src/svc_infra/py.typed +0 -0
- svc_infra-0.1.603/src/svc_infra/api/fastapi/docs/add.py +0 -82
- svc_infra-0.1.603/src/svc_infra/obs/add.py +0 -68
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/ops/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/setup.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/tenancy/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/api/fastapi/tenancy/context.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/cli/foundation → svc_infra-0.1.605/src/svc_infra/cli/cmds/sdk}/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/db → svc_infra-0.1.605/src/svc_infra/cli/foundation}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/data/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/data/backup.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/data/erasure.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/data/fixtures.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/data/retention.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/db/nosql/mongo → svc_infra-0.1.605/src/svc_infra/db}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.603/src/svc_infra/db/nosql/mongo/templates → svc_infra-0.1.605/src/svc_infra/db/nosql/mongo}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/db/sql/templates/models_schemas → svc_infra-0.1.605/src/svc_infra/db/nosql/mongo/templates}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/repository.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/resource.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/db/sql/templates/setup → svc_infra-0.1.605/src/svc_infra/db/sql/templates/models_schemas}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.603/src/svc_infra/mcp → svc_infra-0.1.605/src/svc_infra/db/sql/templates/setup}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/tenant.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/dx/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/jobs/worker.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers → svc_infra-0.1.605/src/svc_infra/mcp}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/compose_cloud → svc_infra-0.1.605/src/svc_infra/obs/providers}/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/compose_cloud/templates → svc_infra-0.1.605/src/svc_infra/obs/providers/compose_cloud}/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/grafana → svc_infra-0.1.605/src/svc_infra/obs/providers/compose_cloud/templates}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/grafana/dashboards → svc_infra-0.1.605/src/svc_infra/obs/providers/grafana}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/grafana/templates → svc_infra-0.1.605/src/svc_infra/obs/providers/grafana/dashboards}/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/providers/grafana/templates/provisioning → svc_infra-0.1.605/src/svc_infra/obs/providers/grafana/templates}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/templates/sidecars → svc_infra-0.1.605/src/svc_infra/obs/providers/grafana/templates/provisioning}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/templates/sidecars/compose → svc_infra-0.1.605/src/svc_infra/obs/templates/sidecars}/__init__.py +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/templates/sidecars/fly → svc_infra-0.1.605/src/svc_infra/obs/templates/sidecars/compose}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/templates/sidecars/k8s → svc_infra-0.1.605/src/svc_infra/obs/templates/sidecars/fly}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.603/src/svc_infra/obs/templates/sidecars/railway → svc_infra-0.1.605/src/svc_infra/obs/templates/sidecars/k8s}/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- /svc_infra-0.1.603/src/svc_infra/py.typed → /svc_infra-0.1.605/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/hibp.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/permissions.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.603 → svc_infra-0.1.605}/src/svc_infra/webhooks/signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: svc-infra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.605
|
|
4
4
|
Summary: Infrastructure for building and deploying prod-ready services
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
|
|
@@ -91,8 +91,10 @@ svc-infra packages the shared building blocks we use to ship production FastAPI
|
|
|
91
91
|
| Jobs | JobQueue, scheduler, CLI worker | [Jobs quickstart](docs/jobs.md) |
|
|
92
92
|
| Cache | cashews decorators, namespace management, TTL helpers | [Cache guide](docs/cache.md) |
|
|
93
93
|
| Observability | Prometheus middleware, Grafana automation, OTEL hooks | [Observability guide](docs/observability.md) |
|
|
94
|
+
| Ops | Probes, breaker, SLOs & dashboards | [SLOs & Ops](docs/ops.md) |
|
|
94
95
|
| Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
|
|
95
96
|
| Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
|
|
97
|
+
| Contributing | Dev setup and quality gates | [Contributing guide](docs/contributing.md) |
|
|
96
98
|
| Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](docs/data-lifecycle.md) |
|
|
97
99
|
|
|
98
100
|
## Minimal FastAPI bootstrap
|
|
@@ -15,8 +15,10 @@ svc-infra packages the shared building blocks we use to ship production FastAPI
|
|
|
15
15
|
| Jobs | JobQueue, scheduler, CLI worker | [Jobs quickstart](docs/jobs.md) |
|
|
16
16
|
| Cache | cashews decorators, namespace management, TTL helpers | [Cache guide](docs/cache.md) |
|
|
17
17
|
| Observability | Prometheus middleware, Grafana automation, OTEL hooks | [Observability guide](docs/observability.md) |
|
|
18
|
+
| Ops | Probes, breaker, SLOs & dashboards | [SLOs & Ops](docs/ops.md) |
|
|
18
19
|
| Webhooks | Subscription store, signing, retry worker | [Webhooks framework](docs/webhooks.md) |
|
|
19
20
|
| Security | Password policy, lockout, signed cookies, headers | [Security hardening](docs/security.md) |
|
|
21
|
+
| Contributing | Dev setup and quality gates | [Contributing guide](docs/contributing.md) |
|
|
20
22
|
| Data Lifecycle | Fixtures, retention, erasure, backups | [Data lifecycle](docs/data-lifecycle.md) |
|
|
21
23
|
|
|
22
24
|
## Minimal FastAPI bootstrap
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "svc-infra"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.605"
|
|
4
4
|
description = "Infrastructure for building and deploying prod-ready services"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -128,6 +128,8 @@ markers = [
|
|
|
128
128
|
"webhooks: Webhooks framework tests",
|
|
129
129
|
"tenancy: Tenancy isolation and enforcement tests",
|
|
130
130
|
"data_lifecycle: Data lifecycle (fixtures, retention, erasure, backups)",
|
|
131
|
+
"ops: SLOs & Ops tests (probes, breaker, instrumentation)",
|
|
132
|
+
"dx: Developer experience and quality gates tests",
|
|
131
133
|
]
|
|
132
134
|
filterwarnings = [
|
|
133
135
|
"ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request
|
|
8
|
+
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
|
9
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
10
|
+
|
|
11
|
+
from .landing import CardSpec, DocTargets, render_index_html
|
|
12
|
+
from .scoped import DOC_SCOPES
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_docs(
|
|
16
|
+
app: FastAPI,
|
|
17
|
+
*,
|
|
18
|
+
redoc_url: str = "/redoc",
|
|
19
|
+
swagger_url: str = "/docs",
|
|
20
|
+
openapi_url: str = "/openapi.json",
|
|
21
|
+
export_openapi_to: Optional[str] = None,
|
|
22
|
+
# Landing page options
|
|
23
|
+
landing_url: str = "/",
|
|
24
|
+
include_landing: bool = True,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Enable docs endpoints and optionally export OpenAPI schema to disk on startup.
|
|
27
|
+
|
|
28
|
+
We mount docs and OpenAPI routes explicitly so this works even when configured post-init.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# OpenAPI JSON route
|
|
32
|
+
async def openapi_handler() -> JSONResponse: # noqa: ANN201
|
|
33
|
+
return JSONResponse(app.openapi())
|
|
34
|
+
|
|
35
|
+
app.add_api_route(openapi_url, openapi_handler, methods=["GET"], include_in_schema=False)
|
|
36
|
+
|
|
37
|
+
# Swagger UI route
|
|
38
|
+
async def swagger_ui(request: Request) -> HTMLResponse: # noqa: ANN201
|
|
39
|
+
resp = get_swagger_ui_html(openapi_url=openapi_url, title="API Docs")
|
|
40
|
+
theme = request.query_params.get("theme")
|
|
41
|
+
if theme == "dark":
|
|
42
|
+
return _with_dark_mode(resp)
|
|
43
|
+
return resp
|
|
44
|
+
|
|
45
|
+
app.add_api_route(swagger_url, swagger_ui, methods=["GET"], include_in_schema=False)
|
|
46
|
+
|
|
47
|
+
# Redoc route
|
|
48
|
+
async def redoc_ui(request: Request) -> HTMLResponse: # noqa: ANN201
|
|
49
|
+
resp = get_redoc_html(openapi_url=openapi_url, title="API ReDoc")
|
|
50
|
+
theme = request.query_params.get("theme")
|
|
51
|
+
if theme == "dark":
|
|
52
|
+
return _with_dark_mode(resp)
|
|
53
|
+
return resp
|
|
54
|
+
|
|
55
|
+
app.add_api_route(redoc_url, redoc_ui, methods=["GET"], include_in_schema=False)
|
|
56
|
+
|
|
57
|
+
# Optional export to disk on startup
|
|
58
|
+
if export_openapi_to:
|
|
59
|
+
export_path = Path(export_openapi_to)
|
|
60
|
+
|
|
61
|
+
async def _export_docs() -> None:
|
|
62
|
+
# Startup export
|
|
63
|
+
spec = app.openapi()
|
|
64
|
+
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
export_path.write_text(json.dumps(spec, indent=2))
|
|
66
|
+
|
|
67
|
+
app.add_event_handler("startup", _export_docs)
|
|
68
|
+
|
|
69
|
+
# Optional landing page with the same look/feel as setup_service_api
|
|
70
|
+
if include_landing:
|
|
71
|
+
# Avoid path collision; if landing_url is already taken for GET, fallback to "/_docs"
|
|
72
|
+
existing_paths = {
|
|
73
|
+
(getattr(r, "path", None) or getattr(r, "path_format", None))
|
|
74
|
+
for r in getattr(app, "routes", [])
|
|
75
|
+
if getattr(r, "methods", None) and "GET" in r.methods
|
|
76
|
+
}
|
|
77
|
+
landing_path = landing_url or "/"
|
|
78
|
+
if landing_path in existing_paths:
|
|
79
|
+
landing_path = "/_docs"
|
|
80
|
+
|
|
81
|
+
async def _landing() -> HTMLResponse: # noqa: ANN201
|
|
82
|
+
cards: list[CardSpec] = []
|
|
83
|
+
# Root docs card using the provided paths
|
|
84
|
+
cards.append(
|
|
85
|
+
CardSpec(
|
|
86
|
+
tag="",
|
|
87
|
+
docs=DocTargets(swagger=swagger_url, redoc=redoc_url, openapi_json=openapi_url),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
# Scoped docs (if any were registered via add_prefixed_docs)
|
|
91
|
+
for scope, swagger, redoc, openapi_json, _title in DOC_SCOPES:
|
|
92
|
+
cards.append(
|
|
93
|
+
CardSpec(
|
|
94
|
+
tag=scope.strip("/"),
|
|
95
|
+
docs=DocTargets(swagger=swagger, redoc=redoc, openapi_json=openapi_json),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
html = render_index_html(
|
|
99
|
+
service_name=app.title or "API", release=app.version or "", cards=cards
|
|
100
|
+
)
|
|
101
|
+
return HTMLResponse(html)
|
|
102
|
+
|
|
103
|
+
app.add_api_route(landing_path, _landing, methods=["GET"], include_in_schema=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _with_dark_mode(resp: HTMLResponse) -> HTMLResponse:
|
|
107
|
+
"""Return a copy of the HTMLResponse with a minimal dark-theme CSS injected.
|
|
108
|
+
|
|
109
|
+
We avoid depending on custom Swagger/ReDoc builds; this works by inlining a small CSS
|
|
110
|
+
block and toggling a `.dark` class on the body element.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
body = resp.body.decode("utf-8", errors="ignore")
|
|
114
|
+
except Exception: # pragma: no cover - very unlikely
|
|
115
|
+
return resp
|
|
116
|
+
|
|
117
|
+
css = _DARK_CSS
|
|
118
|
+
if "</head>" in body:
|
|
119
|
+
body = body.replace("</head>", f"<style>\n{css}\n</style></head>", 1)
|
|
120
|
+
# add class to body to allow stronger selectors
|
|
121
|
+
body = body.replace("<body>", '<body class="dark">', 1)
|
|
122
|
+
return HTMLResponse(content=body, status_code=resp.status_code, headers=dict(resp.headers))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_DARK_CSS = """
|
|
126
|
+
/* Minimal dark mode override for Swagger/ReDoc */
|
|
127
|
+
@media (prefers-color-scheme: dark) { :root { color-scheme: dark; } }
|
|
128
|
+
html.dark, body.dark { background: #0b0e14; color: #e0e6f1; }
|
|
129
|
+
#swagger, .redoc-wrap { background: transparent; }
|
|
130
|
+
a { color: #62aef7; }
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def add_sdk_generation_stub(
|
|
135
|
+
app: FastAPI,
|
|
136
|
+
*,
|
|
137
|
+
on_generate: Optional[callable] = None,
|
|
138
|
+
openapi_path: str = "/openapi.json",
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Hook to add an SDK generation stub.
|
|
141
|
+
|
|
142
|
+
Provide `on_generate()` to run generation (e.g., openapi-generator). This is a stub only; we
|
|
143
|
+
don't ship a hard dependency. If `on_generate` is provided, we expose `/_docs/generate-sdk`.
|
|
144
|
+
"""
|
|
145
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
146
|
+
|
|
147
|
+
if not on_generate:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
router = public_router(prefix="/_docs", include_in_schema=False)
|
|
151
|
+
|
|
152
|
+
@router.post("/generate-sdk")
|
|
153
|
+
async def _generate() -> dict: # noqa: ANN201
|
|
154
|
+
on_generate()
|
|
155
|
+
return {"status": "ok"}
|
|
156
|
+
|
|
157
|
+
app.include_router(router)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = ["add_docs", "add_sdk_generation_stub"]
|
|
@@ -1102,6 +1102,114 @@ def ensure_success_examples_mutator():
|
|
|
1102
1102
|
return m
|
|
1103
1103
|
|
|
1104
1104
|
|
|
1105
|
+
# --- NEW: attach minimal x-codeSamples for common operations ---
|
|
1106
|
+
def attach_code_samples_mutator():
|
|
1107
|
+
"""Attach minimal curl/httpie x-codeSamples for each operation if missing.
|
|
1108
|
+
|
|
1109
|
+
We avoid templating parameters; samples illustrate method and path only.
|
|
1110
|
+
"""
|
|
1111
|
+
|
|
1112
|
+
def m(schema: dict) -> dict:
|
|
1113
|
+
schema = dict(schema)
|
|
1114
|
+
servers = schema.get("servers") or [{"url": ""}]
|
|
1115
|
+
base = servers[0].get("url") or ""
|
|
1116
|
+
|
|
1117
|
+
for path, method, op in _iter_ops(schema):
|
|
1118
|
+
# Don't override existing samples
|
|
1119
|
+
if isinstance(op.get("x-codeSamples"), list) and op["x-codeSamples"]:
|
|
1120
|
+
continue
|
|
1121
|
+
url = f"{base}{path}"
|
|
1122
|
+
method_up = method.upper()
|
|
1123
|
+
samples = [
|
|
1124
|
+
{
|
|
1125
|
+
"lang": "bash",
|
|
1126
|
+
"label": "curl",
|
|
1127
|
+
"source": f"curl -X {method_up} '{url}'",
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
"lang": "bash",
|
|
1131
|
+
"label": "httpie",
|
|
1132
|
+
"source": f"http {method_up} '{url}'",
|
|
1133
|
+
},
|
|
1134
|
+
]
|
|
1135
|
+
op["x-codeSamples"] = samples
|
|
1136
|
+
return schema
|
|
1137
|
+
|
|
1138
|
+
return m
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
# --- NEW: ensure Problem+JSON examples exist for standard error responses ---
|
|
1142
|
+
def ensure_problem_examples_mutator():
|
|
1143
|
+
"""Add example objects for 4xx/5xx responses using Problem schema if absent."""
|
|
1144
|
+
|
|
1145
|
+
try:
|
|
1146
|
+
# Internal helper with sensible defaults
|
|
1147
|
+
from .conventions import _problem_example # type: ignore
|
|
1148
|
+
except Exception: # pragma: no cover - fallback
|
|
1149
|
+
|
|
1150
|
+
def _problem_example(**kw): # type: ignore
|
|
1151
|
+
base = {
|
|
1152
|
+
"type": "about:blank",
|
|
1153
|
+
"title": "Error",
|
|
1154
|
+
"status": 500,
|
|
1155
|
+
"detail": "An error occurred.",
|
|
1156
|
+
"instance": "/request/trace",
|
|
1157
|
+
"code": "INTERNAL_ERROR",
|
|
1158
|
+
}
|
|
1159
|
+
base.update(kw)
|
|
1160
|
+
return base
|
|
1161
|
+
|
|
1162
|
+
def m(schema: dict) -> dict:
|
|
1163
|
+
schema = dict(schema)
|
|
1164
|
+
for _, _, op in _iter_ops(schema):
|
|
1165
|
+
resps = op.get("responses") or {}
|
|
1166
|
+
for code, resp in resps.items():
|
|
1167
|
+
if not isinstance(resp, dict):
|
|
1168
|
+
continue
|
|
1169
|
+
try:
|
|
1170
|
+
ic = int(code)
|
|
1171
|
+
except Exception:
|
|
1172
|
+
continue
|
|
1173
|
+
if ic < 400:
|
|
1174
|
+
continue
|
|
1175
|
+
content = resp.setdefault("content", {})
|
|
1176
|
+
# prefer problem+json but also set application/json if present
|
|
1177
|
+
for mt in ("application/problem+json", "application/json"):
|
|
1178
|
+
mt_obj = content.get(mt)
|
|
1179
|
+
if mt_obj is None:
|
|
1180
|
+
# Create a basic media type referencing Problem schema when appropriate
|
|
1181
|
+
if mt == "application/problem+json":
|
|
1182
|
+
mt_obj = {"schema": {"$ref": "#/components/schemas/Problem"}}
|
|
1183
|
+
content[mt] = mt_obj
|
|
1184
|
+
else:
|
|
1185
|
+
continue
|
|
1186
|
+
if not isinstance(mt_obj, dict):
|
|
1187
|
+
continue
|
|
1188
|
+
if "example" in mt_obj or "examples" in mt_obj:
|
|
1189
|
+
continue
|
|
1190
|
+
mt_obj["example"] = _problem_example(status=ic)
|
|
1191
|
+
return schema
|
|
1192
|
+
|
|
1193
|
+
return m
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
# --- NEW: attach default tags from first path segment when missing ---
|
|
1197
|
+
def attach_default_tags_mutator():
|
|
1198
|
+
"""If an operation has no tags, tag it by its first path segment."""
|
|
1199
|
+
|
|
1200
|
+
def m(schema: dict) -> dict:
|
|
1201
|
+
schema = dict(schema)
|
|
1202
|
+
for path, _method, op in _iter_ops(schema):
|
|
1203
|
+
tags = op.get("tags")
|
|
1204
|
+
if tags:
|
|
1205
|
+
continue
|
|
1206
|
+
seg = path.strip("/").split("/", 1)[0] or "root"
|
|
1207
|
+
op["tags"] = [seg]
|
|
1208
|
+
return schema
|
|
1209
|
+
|
|
1210
|
+
return m
|
|
1211
|
+
|
|
1212
|
+
|
|
1105
1213
|
def dedupe_tags_mutator():
|
|
1106
1214
|
def m(schema: dict) -> dict:
|
|
1107
1215
|
schema = dict(schema)
|
|
@@ -1429,6 +1537,9 @@ def setup_mutators(
|
|
|
1429
1537
|
ensure_media_type_schemas_mutator(),
|
|
1430
1538
|
ensure_examples_for_json_mutator(),
|
|
1431
1539
|
ensure_success_examples_mutator(),
|
|
1540
|
+
attach_default_tags_mutator(),
|
|
1541
|
+
attach_code_samples_mutator(),
|
|
1542
|
+
ensure_problem_examples_mutator(),
|
|
1432
1543
|
ensure_media_examples_mutator(),
|
|
1433
1544
|
scrub_invalid_object_examples_mutator(),
|
|
1434
1545
|
normalize_no_content_204_mutator(),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
Invoice,
|
|
3
|
+
InvoiceLine,
|
|
4
|
+
Plan,
|
|
5
|
+
PlanEntitlement,
|
|
6
|
+
Price,
|
|
7
|
+
Subscription,
|
|
8
|
+
UsageAggregate,
|
|
9
|
+
UsageEvent,
|
|
10
|
+
)
|
|
11
|
+
from .service import BillingService
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"UsageEvent",
|
|
15
|
+
"UsageAggregate",
|
|
16
|
+
"Plan",
|
|
17
|
+
"PlanEntitlement",
|
|
18
|
+
"Subscription",
|
|
19
|
+
"Price",
|
|
20
|
+
"Invoice",
|
|
21
|
+
"InvoiceLine",
|
|
22
|
+
"BillingService",
|
|
23
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import JSON, DateTime, Index, Numeric, String, UniqueConstraint, text
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from svc_infra.db.sql.base import ModelBase
|
|
10
|
+
|
|
11
|
+
TENANT_ID_LEN = 64
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UsageEvent(ModelBase):
|
|
15
|
+
__tablename__ = "billing_usage_events"
|
|
16
|
+
|
|
17
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
18
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
19
|
+
metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
20
|
+
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
21
|
+
at_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
22
|
+
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
23
|
+
metadata_json: Mapped[dict] = mapped_column(JSON, default=dict)
|
|
24
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
25
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__table_args__ = (
|
|
29
|
+
UniqueConstraint("tenant_id", "metric", "idempotency_key", name="uq_usage_idem"),
|
|
30
|
+
Index("ix_usage_tenant_metric_ts", "tenant_id", "metric", "at_ts"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UsageAggregate(ModelBase):
|
|
35
|
+
__tablename__ = "billing_usage_aggregates"
|
|
36
|
+
|
|
37
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
38
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
39
|
+
metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
40
|
+
period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
41
|
+
granularity: Mapped[str] = mapped_column(String(8), nullable=False) # hour|day|month
|
|
42
|
+
total: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
43
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
44
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__table_args__ = (
|
|
48
|
+
UniqueConstraint("tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Plan(ModelBase):
|
|
53
|
+
__tablename__ = "billing_plans"
|
|
54
|
+
|
|
55
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
56
|
+
key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
57
|
+
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
58
|
+
description: Mapped[Optional[str]] = mapped_column(String(255))
|
|
59
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
60
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PlanEntitlement(ModelBase):
|
|
65
|
+
__tablename__ = "billing_plan_entitlements"
|
|
66
|
+
|
|
67
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
68
|
+
plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
69
|
+
key: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
70
|
+
limit_per_window: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
71
|
+
window: Mapped[str] = mapped_column(String(8), nullable=False) # day|month
|
|
72
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
73
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Subscription(ModelBase):
|
|
78
|
+
__tablename__ = "billing_subscriptions"
|
|
79
|
+
|
|
80
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
81
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
82
|
+
plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
83
|
+
effective_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
84
|
+
ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
85
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
86
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Price(ModelBase):
|
|
91
|
+
__tablename__ = "billing_prices"
|
|
92
|
+
|
|
93
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
94
|
+
key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
95
|
+
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
96
|
+
unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
|
|
97
|
+
metric: Mapped[Optional[str]] = mapped_column(String(64)) # null for fixed recurring
|
|
98
|
+
recurring_interval: Mapped[Optional[str]] = mapped_column(String(8)) # month|year
|
|
99
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
100
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Invoice(ModelBase):
|
|
105
|
+
__tablename__ = "billing_invoices"
|
|
106
|
+
|
|
107
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
108
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
109
|
+
period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
110
|
+
period_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
111
|
+
status: Mapped[str] = mapped_column(String(16), index=True, nullable=False)
|
|
112
|
+
total_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
113
|
+
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
114
|
+
provider_invoice_id: Mapped[Optional[str]] = mapped_column(String(128), index=True)
|
|
115
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
116
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InvoiceLine(ModelBase):
|
|
121
|
+
__tablename__ = "billing_invoice_lines"
|
|
122
|
+
|
|
123
|
+
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
124
|
+
invoice_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
125
|
+
price_id: Mapped[Optional[str]] = mapped_column(String(64), index=True)
|
|
126
|
+
metric: Mapped[Optional[str]] = mapped_column(String(64))
|
|
127
|
+
quantity: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
128
|
+
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
129
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
130
|
+
DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
|
|
131
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from .models import Invoice, InvoiceLine, UsageAggregate, UsageEvent
|
|
12
|
+
|
|
13
|
+
ProviderSyncHook = Callable[[Invoice, list[InvoiceLine]], None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class BillingService:
|
|
18
|
+
session: Session
|
|
19
|
+
tenant_id: str
|
|
20
|
+
provider_sync: Optional[ProviderSyncHook] = None
|
|
21
|
+
|
|
22
|
+
def record_usage(
|
|
23
|
+
self, *, metric: str, amount: int, at: datetime, idempotency_key: str, metadata: dict | None
|
|
24
|
+
) -> str:
|
|
25
|
+
# Ensure UTC
|
|
26
|
+
if at.tzinfo is None:
|
|
27
|
+
at = at.replace(tzinfo=timezone.utc)
|
|
28
|
+
evt = UsageEvent(
|
|
29
|
+
id=str(uuid.uuid4()),
|
|
30
|
+
tenant_id=self.tenant_id,
|
|
31
|
+
metric=metric,
|
|
32
|
+
amount=amount,
|
|
33
|
+
at_ts=at,
|
|
34
|
+
idempotency_key=idempotency_key,
|
|
35
|
+
metadata_json=metadata or {},
|
|
36
|
+
)
|
|
37
|
+
self.session.add(evt)
|
|
38
|
+
self.session.flush()
|
|
39
|
+
return evt.id
|
|
40
|
+
|
|
41
|
+
def aggregate_daily(self, *, metric: str, day_start: datetime) -> None:
|
|
42
|
+
# Compute [day_start, day_start+1d)
|
|
43
|
+
next_day = day_start.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
|
44
|
+
total = 0
|
|
45
|
+
rows = self.session.execute(
|
|
46
|
+
select(UsageEvent).where(
|
|
47
|
+
UsageEvent.tenant_id == self.tenant_id,
|
|
48
|
+
UsageEvent.metric == metric,
|
|
49
|
+
UsageEvent.at_ts >= day_start,
|
|
50
|
+
UsageEvent.at_ts < next_day,
|
|
51
|
+
)
|
|
52
|
+
).scalars()
|
|
53
|
+
for r in rows:
|
|
54
|
+
total += int(r.amount)
|
|
55
|
+
# upsert aggregate
|
|
56
|
+
agg = self.session.execute(
|
|
57
|
+
select(UsageAggregate).where(
|
|
58
|
+
UsageAggregate.tenant_id == self.tenant_id,
|
|
59
|
+
UsageAggregate.metric == metric,
|
|
60
|
+
UsageAggregate.period_start == day_start,
|
|
61
|
+
UsageAggregate.granularity == "day",
|
|
62
|
+
)
|
|
63
|
+
).scalar_one_or_none()
|
|
64
|
+
if agg:
|
|
65
|
+
agg.total = total
|
|
66
|
+
else:
|
|
67
|
+
self.session.add(
|
|
68
|
+
UsageAggregate(
|
|
69
|
+
id=str(uuid.uuid4()),
|
|
70
|
+
tenant_id=self.tenant_id,
|
|
71
|
+
metric=metric,
|
|
72
|
+
period_start=day_start,
|
|
73
|
+
granularity="day",
|
|
74
|
+
total=total,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def generate_monthly_invoice(
|
|
79
|
+
self, *, period_start: datetime, period_end: datetime, currency: str
|
|
80
|
+
) -> str:
|
|
81
|
+
# Minimal: sum all daily aggregates and produce one line
|
|
82
|
+
total = 0
|
|
83
|
+
rows = self.session.execute(
|
|
84
|
+
select(UsageAggregate).where(
|
|
85
|
+
UsageAggregate.tenant_id == self.tenant_id,
|
|
86
|
+
UsageAggregate.period_start >= period_start,
|
|
87
|
+
UsageAggregate.period_start < period_end,
|
|
88
|
+
UsageAggregate.granularity == "day",
|
|
89
|
+
)
|
|
90
|
+
).scalars()
|
|
91
|
+
for r in rows:
|
|
92
|
+
total += int(r.total)
|
|
93
|
+
inv = Invoice(
|
|
94
|
+
id=str(uuid.uuid4()),
|
|
95
|
+
tenant_id=self.tenant_id,
|
|
96
|
+
period_start=period_start,
|
|
97
|
+
period_end=period_end,
|
|
98
|
+
status="created",
|
|
99
|
+
total_amount=total,
|
|
100
|
+
currency=currency,
|
|
101
|
+
)
|
|
102
|
+
self.session.add(inv)
|
|
103
|
+
self.session.flush()
|
|
104
|
+
line = InvoiceLine(
|
|
105
|
+
id=str(uuid.uuid4()),
|
|
106
|
+
invoice_id=inv.id,
|
|
107
|
+
price_id=None,
|
|
108
|
+
metric=None,
|
|
109
|
+
quantity=1,
|
|
110
|
+
amount=total,
|
|
111
|
+
)
|
|
112
|
+
self.session.add(line)
|
|
113
|
+
if self.provider_sync:
|
|
114
|
+
self.provider_sync(inv, [line])
|
|
115
|
+
return inv.id
|
|
@@ -6,9 +6,11 @@ from svc_infra.cli.cmds import (
|
|
|
6
6
|
_HELP,
|
|
7
7
|
jobs_app,
|
|
8
8
|
register_alembic,
|
|
9
|
+
register_dx,
|
|
9
10
|
register_mongo,
|
|
10
11
|
register_mongo_scaffold,
|
|
11
12
|
register_obs,
|
|
13
|
+
register_sdk,
|
|
12
14
|
register_sql_export,
|
|
13
15
|
register_sql_scaffold,
|
|
14
16
|
)
|
|
@@ -29,9 +31,15 @@ register_mongo_scaffold(app)
|
|
|
29
31
|
# -- observability commands ---
|
|
30
32
|
register_obs(app)
|
|
31
33
|
|
|
34
|
+
# -- dx commands ---
|
|
35
|
+
register_dx(app)
|
|
36
|
+
|
|
32
37
|
# -- jobs commands ---
|
|
33
38
|
app.add_typer(jobs_app, name="jobs")
|
|
34
39
|
|
|
40
|
+
# -- sdk commands ---
|
|
41
|
+
register_sdk(app)
|
|
42
|
+
|
|
35
43
|
|
|
36
44
|
def main():
|
|
37
45
|
app()
|
|
@@ -5,8 +5,10 @@ from svc_infra.cli.cmds.db.nosql.mongo.mongo_scaffold_cmds import (
|
|
|
5
5
|
from svc_infra.cli.cmds.db.sql.alembic_cmds import register as register_alembic
|
|
6
6
|
from svc_infra.cli.cmds.db.sql.sql_export_cmds import register as register_sql_export
|
|
7
7
|
from svc_infra.cli.cmds.db.sql.sql_scaffold_cmds import register as register_sql_scaffold
|
|
8
|
+
from svc_infra.cli.cmds.dx import register_dx
|
|
8
9
|
from svc_infra.cli.cmds.jobs.jobs_cmds import app as jobs_app
|
|
9
10
|
from svc_infra.cli.cmds.obs.obs_cmds import register as register_obs
|
|
11
|
+
from svc_infra.cli.cmds.sdk.sdk_cmds import register as register_sdk
|
|
10
12
|
|
|
11
13
|
from .help import _HELP
|
|
12
14
|
|
|
@@ -18,5 +20,7 @@ __all__ = [
|
|
|
18
20
|
"register_mongo_scaffold",
|
|
19
21
|
"register_obs",
|
|
20
22
|
"jobs_app",
|
|
23
|
+
"register_sdk",
|
|
24
|
+
"register_dx",
|
|
21
25
|
"_HELP",
|
|
22
26
|
]
|