svc-infra 0.1.600__tar.gz → 0.1.602__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.600 → svc_infra-0.1.602}/PKG-INFO +1 -1
- {svc_infra-0.1.600 → svc_infra-0.1.602}/pyproject.toml +2 -1
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/add.py +32 -13
- svc_infra-0.1.602/src/svc_infra/api/fastapi/db/sql/crud_router.py +292 -0
- svc_infra-0.1.602/src/svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
- svc_infra-0.1.602/src/svc_infra/api/fastapi/docs/add.py +60 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/ratelimit.py +41 -1
- svc_infra-0.1.602/src/svc_infra/api/fastapi/ops/add.py +65 -0
- svc_infra-0.1.602/src/svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra-0.1.602/src/svc_infra/api/fastapi/tenancy/context.py +112 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/__init__.py +2 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/__init__.py +2 -0
- svc_infra-0.1.602/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +82 -0
- svc_infra-0.1.602/src/svc_infra/data/add.py +59 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/repository.py +46 -9
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/resource.py +5 -0
- svc_infra-0.1.602/src/svc_infra/db/sql/tenant.py +79 -0
- svc_infra-0.1.602/src/svc_infra/dx/add.py +63 -0
- svc_infra-0.1.600/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -148
- svc_infra-0.1.600/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -66
- {svc_infra-0.1.600 → svc_infra-0.1.602}/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/api/fastapi/setup.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/jobs/worker.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/hibp.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/permissions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.602}/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.602"
|
|
4
4
|
description = "Infrastructure for building and deploying prod-ready services"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -126,6 +126,7 @@ markers = [
|
|
|
126
126
|
"concurrency: Idempotency and concurrency control tests",
|
|
127
127
|
"jobs: Background jobs and scheduling tests",
|
|
128
128
|
"webhooks: Webhooks framework tests",
|
|
129
|
+
"tenancy: Tenancy isolation and enforcement tests",
|
|
129
130
|
]
|
|
130
131
|
filterwarnings = [
|
|
131
132
|
"ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
|
|
@@ -10,7 +10,7 @@ from svc_infra.db.sql.management import make_crud_schemas
|
|
|
10
10
|
from svc_infra.db.sql.repository import SqlRepository
|
|
11
11
|
from svc_infra.db.sql.resource import SqlResource
|
|
12
12
|
|
|
13
|
-
from .crud_router import make_crud_router_plus_sql
|
|
13
|
+
from .crud_router import make_crud_router_plus_sql, make_tenant_crud_router_plus_sql
|
|
14
14
|
from .health import _make_db_health_router
|
|
15
15
|
from .session import dispose_session, initialize_session
|
|
16
16
|
|
|
@@ -37,18 +37,37 @@ def add_sql_resources(app: FastAPI, resources: Sequence[SqlResource]) -> None:
|
|
|
37
37
|
update_name=r.update_name,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
if r.tenant_field:
|
|
41
|
+
# wrap service factory/instance through tenant router
|
|
42
|
+
def _factory():
|
|
43
|
+
return svc
|
|
44
|
+
|
|
45
|
+
router = make_tenant_crud_router_plus_sql(
|
|
46
|
+
model=r.model,
|
|
47
|
+
service_factory=_factory,
|
|
48
|
+
read_schema=Read,
|
|
49
|
+
create_schema=Create,
|
|
50
|
+
update_schema=Update,
|
|
51
|
+
prefix=r.prefix,
|
|
52
|
+
tenant_field=r.tenant_field,
|
|
53
|
+
tags=r.tags,
|
|
54
|
+
search_fields=r.search_fields,
|
|
55
|
+
default_ordering=r.ordering_default,
|
|
56
|
+
allowed_order_fields=r.allowed_order_fields,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
router = make_crud_router_plus_sql(
|
|
60
|
+
model=r.model,
|
|
61
|
+
service=svc,
|
|
62
|
+
read_schema=Read,
|
|
63
|
+
create_schema=Create,
|
|
64
|
+
update_schema=Update,
|
|
65
|
+
prefix=r.prefix,
|
|
66
|
+
tags=r.tags,
|
|
67
|
+
search_fields=r.search_fields,
|
|
68
|
+
default_ordering=r.ordering_default,
|
|
69
|
+
allowed_order_fields=r.allowed_order_fields,
|
|
70
|
+
)
|
|
52
71
|
app.include_router(router)
|
|
53
72
|
|
|
54
73
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
from typing import Annotated, Any, Optional, Sequence, Type, TypeVar, cast
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from svc_infra.api.fastapi.db.http import (
|
|
7
|
+
LimitOffsetParams,
|
|
8
|
+
OrderParams,
|
|
9
|
+
Page,
|
|
10
|
+
SearchParams,
|
|
11
|
+
build_order_by,
|
|
12
|
+
dep_limit_offset,
|
|
13
|
+
dep_order,
|
|
14
|
+
dep_search,
|
|
15
|
+
)
|
|
16
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
17
|
+
from svc_infra.db.sql.service import SqlService
|
|
18
|
+
from svc_infra.db.sql.tenant import TenantSqlService
|
|
19
|
+
|
|
20
|
+
from ...tenancy.context import TenantId
|
|
21
|
+
from .session import SqlSessionDep
|
|
22
|
+
|
|
23
|
+
CreateModel = TypeVar("CreateModel", bound=BaseModel)
|
|
24
|
+
ReadModel = TypeVar("ReadModel", bound=BaseModel)
|
|
25
|
+
UpdateModel = TypeVar("UpdateModel", bound=BaseModel)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_crud_router_plus_sql(
|
|
29
|
+
*,
|
|
30
|
+
model: type[Any],
|
|
31
|
+
service: SqlService,
|
|
32
|
+
read_schema: Type[ReadModel],
|
|
33
|
+
create_schema: Type[CreateModel],
|
|
34
|
+
update_schema: Type[UpdateModel],
|
|
35
|
+
prefix: str,
|
|
36
|
+
tags: list[str] | None = None,
|
|
37
|
+
search_fields: Optional[Sequence[str]] = None,
|
|
38
|
+
default_ordering: Optional[str] = None,
|
|
39
|
+
allowed_order_fields: Optional[list[str]] = None,
|
|
40
|
+
mount_under_db_prefix: bool = True,
|
|
41
|
+
) -> APIRouter:
|
|
42
|
+
router_prefix = ("/_sql" + prefix) if mount_under_db_prefix else prefix
|
|
43
|
+
router = public_router(
|
|
44
|
+
prefix=router_prefix,
|
|
45
|
+
tags=tags or [prefix.strip("/")],
|
|
46
|
+
redirect_slashes=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
|
|
50
|
+
if not order_spec:
|
|
51
|
+
return []
|
|
52
|
+
pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
|
|
53
|
+
fields: list[str] = []
|
|
54
|
+
for p in pieces:
|
|
55
|
+
name = p[1:] if p.startswith("-") else p
|
|
56
|
+
if allowed_order_fields and name not in (allowed_order_fields or []):
|
|
57
|
+
continue
|
|
58
|
+
fields.append(p)
|
|
59
|
+
return fields
|
|
60
|
+
|
|
61
|
+
# -------- LIST --------
|
|
62
|
+
@router.get(
|
|
63
|
+
"",
|
|
64
|
+
response_model=cast(Any, Page[Any]),
|
|
65
|
+
description=f"List items of type {model.__name__}",
|
|
66
|
+
)
|
|
67
|
+
async def list_items(
|
|
68
|
+
lp: Annotated[LimitOffsetParams, Depends(dep_limit_offset)],
|
|
69
|
+
op: Annotated[OrderParams, Depends(dep_order)],
|
|
70
|
+
sp: Annotated[SearchParams, Depends(dep_search)],
|
|
71
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
72
|
+
):
|
|
73
|
+
order_spec = op.order_by or default_ordering
|
|
74
|
+
order_fields = _parse_ordering_to_fields(order_spec)
|
|
75
|
+
order_by = build_order_by(model, order_fields)
|
|
76
|
+
|
|
77
|
+
if sp.q:
|
|
78
|
+
fields = [
|
|
79
|
+
f.strip()
|
|
80
|
+
for f in (sp.fields or (",".join(search_fields or []) or "")).split(",")
|
|
81
|
+
if f.strip()
|
|
82
|
+
]
|
|
83
|
+
items = await service.search(
|
|
84
|
+
session, q=sp.q, fields=fields, limit=lp.limit, offset=lp.offset, order_by=order_by
|
|
85
|
+
)
|
|
86
|
+
total = await service.count_filtered(session, q=sp.q, fields=fields)
|
|
87
|
+
else:
|
|
88
|
+
items = await service.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
|
|
89
|
+
total = await service.count(session)
|
|
90
|
+
return Page[read_schema].from_items(
|
|
91
|
+
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# -------- GET by id --------
|
|
95
|
+
@router.get(
|
|
96
|
+
"/{item_id}",
|
|
97
|
+
response_model=cast(Any, Any),
|
|
98
|
+
description=f"Get item of type {model.__name__}",
|
|
99
|
+
)
|
|
100
|
+
async def get_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
|
|
101
|
+
row = await service.get(session, item_id)
|
|
102
|
+
if not row:
|
|
103
|
+
raise HTTPException(404, "Not found")
|
|
104
|
+
return row
|
|
105
|
+
|
|
106
|
+
# -------- CREATE --------
|
|
107
|
+
@router.post(
|
|
108
|
+
"",
|
|
109
|
+
response_model=cast(Any, Any),
|
|
110
|
+
status_code=201,
|
|
111
|
+
description=f"Create item of type {model.__name__}",
|
|
112
|
+
)
|
|
113
|
+
async def create_item(
|
|
114
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
115
|
+
payload: Any = Body(...),
|
|
116
|
+
):
|
|
117
|
+
if isinstance(payload, BaseModel):
|
|
118
|
+
data = payload.model_dump(exclude_unset=True)
|
|
119
|
+
elif isinstance(payload, dict):
|
|
120
|
+
data = payload
|
|
121
|
+
else:
|
|
122
|
+
raise HTTPException(422, "invalid_payload")
|
|
123
|
+
return await service.create(session, data)
|
|
124
|
+
|
|
125
|
+
# -------- UPDATE --------
|
|
126
|
+
@router.patch(
|
|
127
|
+
"/{item_id}",
|
|
128
|
+
response_model=cast(Any, Any),
|
|
129
|
+
description=f"Update item of type {model.__name__}",
|
|
130
|
+
)
|
|
131
|
+
async def update_item(
|
|
132
|
+
item_id: Any,
|
|
133
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
134
|
+
payload: Any = Body(...),
|
|
135
|
+
):
|
|
136
|
+
if isinstance(payload, BaseModel):
|
|
137
|
+
data = payload.model_dump(exclude_unset=True)
|
|
138
|
+
elif isinstance(payload, dict):
|
|
139
|
+
data = payload
|
|
140
|
+
else:
|
|
141
|
+
raise HTTPException(422, "invalid_payload")
|
|
142
|
+
row = await service.update(session, item_id, data)
|
|
143
|
+
if not row:
|
|
144
|
+
raise HTTPException(404, "Not found")
|
|
145
|
+
return row
|
|
146
|
+
|
|
147
|
+
# -------- DELETE --------
|
|
148
|
+
@router.delete(
|
|
149
|
+
"/{item_id}", status_code=204, description=f"Delete item of type {model.__name__}"
|
|
150
|
+
)
|
|
151
|
+
async def delete_item(item_id: Any, session: SqlSessionDep): # type: ignore[name-defined]
|
|
152
|
+
ok = await service.delete(session, item_id)
|
|
153
|
+
if not ok:
|
|
154
|
+
raise HTTPException(404, "Not found")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
return router
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def make_tenant_crud_router_plus_sql(
|
|
161
|
+
*,
|
|
162
|
+
model: type[Any],
|
|
163
|
+
service_factory: callable, # factory that returns a SqlService (will be wrapped)
|
|
164
|
+
read_schema: Type[ReadModel],
|
|
165
|
+
create_schema: Type[CreateModel],
|
|
166
|
+
update_schema: Type[UpdateModel],
|
|
167
|
+
prefix: str,
|
|
168
|
+
tenant_field: str = "tenant_id",
|
|
169
|
+
tags: list[str] | None = None,
|
|
170
|
+
search_fields: Optional[Sequence[str]] = None,
|
|
171
|
+
default_ordering: Optional[str] = None,
|
|
172
|
+
allowed_order_fields: Optional[list[str]] = None,
|
|
173
|
+
mount_under_db_prefix: bool = True,
|
|
174
|
+
) -> APIRouter:
|
|
175
|
+
"""Like make_crud_router_plus_sql, but requires TenantId and scopes all operations."""
|
|
176
|
+
router_prefix = ("/_sql" + prefix) if mount_under_db_prefix else prefix
|
|
177
|
+
router = public_router(
|
|
178
|
+
prefix=router_prefix,
|
|
179
|
+
tags=tags or [prefix.strip("/")],
|
|
180
|
+
redirect_slashes=False,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _parse_ordering_to_fields(order_spec: Optional[str]) -> list[str]:
|
|
184
|
+
if not order_spec:
|
|
185
|
+
return []
|
|
186
|
+
pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
|
|
187
|
+
fields: list[str] = []
|
|
188
|
+
for p in pieces:
|
|
189
|
+
name = p[1:] if p.startswith("-") else p
|
|
190
|
+
if allowed_order_fields and name not in (allowed_order_fields or []):
|
|
191
|
+
continue
|
|
192
|
+
fields.append(p)
|
|
193
|
+
return fields
|
|
194
|
+
|
|
195
|
+
# create per-request service with tenant scoping
|
|
196
|
+
async def _svc(session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
|
|
197
|
+
base = service_factory # consumer-provided factory or instance
|
|
198
|
+
svc = base # assume already a SqlService by default
|
|
199
|
+
if callable(base):
|
|
200
|
+
svc = base # the consumer likely closed over repo
|
|
201
|
+
# if callable returns a service, call it now
|
|
202
|
+
try:
|
|
203
|
+
svc = base() # type: ignore[misc]
|
|
204
|
+
except TypeError:
|
|
205
|
+
svc = base # already instance
|
|
206
|
+
if not isinstance(svc, TenantSqlService):
|
|
207
|
+
svc = TenantSqlService(getattr(svc, "repo", svc), tenant_id=tenant_id, tenant_field=tenant_field) # type: ignore[arg-type]
|
|
208
|
+
return svc # type: ignore[return-value]
|
|
209
|
+
|
|
210
|
+
@router.get("", response_model=cast(Any, Page[Any]))
|
|
211
|
+
async def list_items(
|
|
212
|
+
lp: Annotated[LimitOffsetParams, Depends(dep_limit_offset)],
|
|
213
|
+
op: Annotated[OrderParams, Depends(dep_order)],
|
|
214
|
+
sp: Annotated[SearchParams, Depends(dep_search)],
|
|
215
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
216
|
+
tenant_id: TenantId,
|
|
217
|
+
):
|
|
218
|
+
svc = await _svc(session, tenant_id)
|
|
219
|
+
order_spec = op.order_by or default_ordering
|
|
220
|
+
order_fields = _parse_ordering_to_fields(order_spec)
|
|
221
|
+
order_by = build_order_by(model, order_fields)
|
|
222
|
+
if sp.q:
|
|
223
|
+
fields = [
|
|
224
|
+
f.strip()
|
|
225
|
+
for f in (sp.fields or (",".join(search_fields or []) or "")).split(",")
|
|
226
|
+
if f.strip()
|
|
227
|
+
]
|
|
228
|
+
items = await svc.search(
|
|
229
|
+
session, q=sp.q, fields=fields, limit=lp.limit, offset=lp.offset, order_by=order_by
|
|
230
|
+
)
|
|
231
|
+
total = await svc.count_filtered(session, q=sp.q, fields=fields)
|
|
232
|
+
else:
|
|
233
|
+
items = await svc.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
|
|
234
|
+
total = await svc.count(session)
|
|
235
|
+
return Page[read_schema].from_items(
|
|
236
|
+
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@router.get("/{item_id}", response_model=cast(Any, Any))
|
|
240
|
+
async def get_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
|
|
241
|
+
svc = await _svc(session, tenant_id)
|
|
242
|
+
row = await svc.get(session, item_id)
|
|
243
|
+
if not row:
|
|
244
|
+
raise HTTPException(404, "Not found")
|
|
245
|
+
return row
|
|
246
|
+
|
|
247
|
+
@router.post("", response_model=cast(Any, Any), status_code=201)
|
|
248
|
+
async def create_item(
|
|
249
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
250
|
+
tenant_id: TenantId,
|
|
251
|
+
payload: Any = Body(...),
|
|
252
|
+
):
|
|
253
|
+
svc = await _svc(session, tenant_id)
|
|
254
|
+
if isinstance(payload, BaseModel):
|
|
255
|
+
data = payload.model_dump(exclude_unset=True)
|
|
256
|
+
elif isinstance(payload, dict):
|
|
257
|
+
data = payload
|
|
258
|
+
else:
|
|
259
|
+
raise HTTPException(422, "invalid_payload")
|
|
260
|
+
return await svc.create(session, data)
|
|
261
|
+
|
|
262
|
+
@router.patch("/{item_id}", response_model=cast(Any, Any))
|
|
263
|
+
async def update_item(
|
|
264
|
+
item_id: Any,
|
|
265
|
+
session: SqlSessionDep, # type: ignore[name-defined]
|
|
266
|
+
tenant_id: TenantId,
|
|
267
|
+
payload: Any = Body(...),
|
|
268
|
+
):
|
|
269
|
+
svc = await _svc(session, tenant_id)
|
|
270
|
+
if isinstance(payload, BaseModel):
|
|
271
|
+
data = payload.model_dump(exclude_unset=True)
|
|
272
|
+
elif isinstance(payload, dict):
|
|
273
|
+
data = payload
|
|
274
|
+
else:
|
|
275
|
+
raise HTTPException(422, "invalid_payload")
|
|
276
|
+
row = await svc.update(session, item_id, data)
|
|
277
|
+
if not row:
|
|
278
|
+
raise HTTPException(404, "Not found")
|
|
279
|
+
return row
|
|
280
|
+
|
|
281
|
+
@router.delete("/{item_id}", status_code=204)
|
|
282
|
+
async def delete_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId): # type: ignore[name-defined]
|
|
283
|
+
svc = await _svc(session, tenant_id)
|
|
284
|
+
ok = await svc.delete(session, item_id)
|
|
285
|
+
if not ok:
|
|
286
|
+
raise HTTPException(404, "Not found")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
return router
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
__all__ = ["make_crud_router_plus_sql", "make_tenant_crud_router_plus_sql"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException
|
|
7
|
+
from starlette.requests import Request
|
|
8
|
+
|
|
9
|
+
from svc_infra.api.fastapi.middleware.ratelimit_store import InMemoryRateLimitStore, RateLimitStore
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from svc_infra.api.fastapi.tenancy.context import resolve_tenant_id as _resolve_tenant_id
|
|
13
|
+
except Exception: # pragma: no cover - minimal builds
|
|
14
|
+
_resolve_tenant_id = None # type: ignore
|
|
15
|
+
from svc_infra.obs.metrics import emit_rate_limited
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RateLimiter:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
limit: int,
|
|
23
|
+
window: int = 60,
|
|
24
|
+
key_fn: Callable = lambda r: "global",
|
|
25
|
+
limit_resolver: Optional[Callable[[Request, Optional[str]], Optional[int]]] = None,
|
|
26
|
+
scope_by_tenant: bool = False,
|
|
27
|
+
store: RateLimitStore | None = None,
|
|
28
|
+
):
|
|
29
|
+
self.limit = limit
|
|
30
|
+
self.window = window
|
|
31
|
+
self.key_fn = key_fn
|
|
32
|
+
self._limit_resolver = limit_resolver
|
|
33
|
+
self.scope_by_tenant = scope_by_tenant
|
|
34
|
+
self.store = store or InMemoryRateLimitStore(limit=limit)
|
|
35
|
+
|
|
36
|
+
async def __call__(self, request: Request):
|
|
37
|
+
# Try resolving tenant when asked
|
|
38
|
+
tenant_id = None
|
|
39
|
+
if self.scope_by_tenant or self._limit_resolver:
|
|
40
|
+
try:
|
|
41
|
+
if _resolve_tenant_id is not None:
|
|
42
|
+
tenant_id = await _resolve_tenant_id(request)
|
|
43
|
+
except Exception:
|
|
44
|
+
tenant_id = None
|
|
45
|
+
|
|
46
|
+
key = self.key_fn(request)
|
|
47
|
+
if self.scope_by_tenant and tenant_id:
|
|
48
|
+
key = f"{key}:tenant:{tenant_id}"
|
|
49
|
+
|
|
50
|
+
eff_limit = self.limit
|
|
51
|
+
if self._limit_resolver:
|
|
52
|
+
try:
|
|
53
|
+
v = self._limit_resolver(request, tenant_id)
|
|
54
|
+
eff_limit = int(v) if v is not None else self.limit
|
|
55
|
+
except Exception:
|
|
56
|
+
eff_limit = self.limit
|
|
57
|
+
|
|
58
|
+
count, store_limit, reset = self.store.incr(str(key), self.window)
|
|
59
|
+
if count > eff_limit:
|
|
60
|
+
retry = max(0, reset - int(time.time()))
|
|
61
|
+
try:
|
|
62
|
+
emit_rate_limited(str(key), eff_limit, retry)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
raise HTTPException(
|
|
66
|
+
status_code=429, detail="Rate limit exceeded", headers={"Retry-After": str(retry)}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["RateLimiter"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def rate_limiter(
|
|
74
|
+
*,
|
|
75
|
+
limit: int,
|
|
76
|
+
window: int = 60,
|
|
77
|
+
key_fn: Callable = lambda r: "global",
|
|
78
|
+
limit_resolver: Optional[Callable[[Request, Optional[str]], Optional[int]]] = None,
|
|
79
|
+
scope_by_tenant: bool = False,
|
|
80
|
+
store: RateLimitStore | None = None,
|
|
81
|
+
):
|
|
82
|
+
store_ = store or InMemoryRateLimitStore(limit=limit)
|
|
83
|
+
|
|
84
|
+
async def dep(request: Request):
|
|
85
|
+
tenant_id = None
|
|
86
|
+
if scope_by_tenant or limit_resolver:
|
|
87
|
+
try:
|
|
88
|
+
if _resolve_tenant_id is not None:
|
|
89
|
+
tenant_id = await _resolve_tenant_id(request)
|
|
90
|
+
except Exception:
|
|
91
|
+
tenant_id = None
|
|
92
|
+
|
|
93
|
+
key = key_fn(request)
|
|
94
|
+
if scope_by_tenant and tenant_id:
|
|
95
|
+
key = f"{key}:tenant:{tenant_id}"
|
|
96
|
+
|
|
97
|
+
eff_limit = limit
|
|
98
|
+
if limit_resolver:
|
|
99
|
+
try:
|
|
100
|
+
v = limit_resolver(request, tenant_id)
|
|
101
|
+
eff_limit = int(v) if v is not None else limit
|
|
102
|
+
except Exception:
|
|
103
|
+
eff_limit = limit
|
|
104
|
+
|
|
105
|
+
count, _store_limit, reset = store_.incr(str(key), window)
|
|
106
|
+
if count > eff_limit:
|
|
107
|
+
retry = max(0, reset - int(time.time()))
|
|
108
|
+
try:
|
|
109
|
+
emit_rate_limited(str(key), eff_limit, retry)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=429, detail="Rate limit exceeded", headers={"Retry-After": str(retry)}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return dep
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_docs(
|
|
11
|
+
app: FastAPI,
|
|
12
|
+
*,
|
|
13
|
+
redoc_url: str = "/redoc",
|
|
14
|
+
swagger_url: str = "/docs",
|
|
15
|
+
openapi_url: str = "/openapi.json",
|
|
16
|
+
export_openapi_to: Optional[str] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Enable docs endpoints and optionally export OpenAPI schema to disk on startup."""
|
|
19
|
+
# Configure FastAPI docs URLs
|
|
20
|
+
app.docs_url = swagger_url
|
|
21
|
+
app.redoc_url = redoc_url
|
|
22
|
+
app.openapi_url = openapi_url
|
|
23
|
+
|
|
24
|
+
if export_openapi_to:
|
|
25
|
+
export_path = Path(export_openapi_to)
|
|
26
|
+
|
|
27
|
+
@app.on_event("startup")
|
|
28
|
+
async def _export_spec() -> None: # noqa: ANN202
|
|
29
|
+
spec = app.openapi()
|
|
30
|
+
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
export_path.write_text(json.dumps(spec, indent=2))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def add_sdk_generation_stub(
|
|
35
|
+
app: FastAPI,
|
|
36
|
+
*,
|
|
37
|
+
on_generate: Optional[callable] = None,
|
|
38
|
+
openapi_path: str = "/openapi.json",
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Hook to add an SDK generation stub.
|
|
41
|
+
|
|
42
|
+
Provide `on_generate()` to run generation (e.g., openapi-generator). This is a stub only; we
|
|
43
|
+
don't ship a hard dependency. If `on_generate` is provided, we expose `/_docs/generate-sdk`.
|
|
44
|
+
"""
|
|
45
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
46
|
+
|
|
47
|
+
if not on_generate:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
router = public_router(prefix="/_docs", include_in_schema=False)
|
|
51
|
+
|
|
52
|
+
@router.post("/generate-sdk")
|
|
53
|
+
async def _generate() -> dict: # noqa: ANN201
|
|
54
|
+
on_generate()
|
|
55
|
+
return {"status": "ok"}
|
|
56
|
+
|
|
57
|
+
app.include_router(router)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["add_docs", "add_sdk_generation_stub"]
|
|
@@ -7,6 +7,12 @@ from svc_infra.obs.metrics import emit_rate_limited
|
|
|
7
7
|
|
|
8
8
|
from .ratelimit_store import InMemoryRateLimitStore, RateLimitStore
|
|
9
9
|
|
|
10
|
+
try:
|
|
11
|
+
# Optional import: tenancy may not be enabled in all apps
|
|
12
|
+
from svc_infra.api.fastapi.tenancy.context import resolve_tenant_id as _resolve_tenant_id
|
|
13
|
+
except Exception: # pragma: no cover - fallback for minimal builds
|
|
14
|
+
_resolve_tenant_id = None # type: ignore
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
|
|
12
18
|
def __init__(
|
|
@@ -15,18 +21,52 @@ class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
15
21
|
limit: int = 120,
|
|
16
22
|
window: int = 60,
|
|
17
23
|
key_fn=None,
|
|
24
|
+
*,
|
|
25
|
+
# When provided, dynamically computes a limit for the current request (e.g. per-tenant quotas)
|
|
26
|
+
# Signature: (request: Request, tenant_id: Optional[str]) -> int | None
|
|
27
|
+
limit_resolver=None,
|
|
28
|
+
# If True, automatically scopes the bucket key by tenant id when available
|
|
29
|
+
scope_by_tenant: bool = False,
|
|
18
30
|
store: RateLimitStore | None = None,
|
|
19
31
|
):
|
|
20
32
|
super().__init__(app)
|
|
21
33
|
self.limit, self.window = limit, window
|
|
22
34
|
self.key_fn = key_fn or (lambda r: r.headers.get("X-API-Key") or r.client.host)
|
|
35
|
+
self._limit_resolver = limit_resolver
|
|
36
|
+
self.scope_by_tenant = scope_by_tenant
|
|
23
37
|
self.store = store or InMemoryRateLimitStore(limit=limit)
|
|
24
38
|
|
|
25
39
|
async def dispatch(self, request, call_next):
|
|
40
|
+
# Resolve tenant when possible
|
|
41
|
+
tenant_id = None
|
|
42
|
+
if self.scope_by_tenant or self._limit_resolver:
|
|
43
|
+
try:
|
|
44
|
+
if _resolve_tenant_id is not None:
|
|
45
|
+
tenant_id = await _resolve_tenant_id(request)
|
|
46
|
+
except Exception:
|
|
47
|
+
tenant_id = None
|
|
48
|
+
|
|
26
49
|
key = self.key_fn(request)
|
|
50
|
+
if self.scope_by_tenant and tenant_id:
|
|
51
|
+
key = f"{key}:tenant:{tenant_id}"
|
|
52
|
+
|
|
53
|
+
# Allow dynamic limit overrides
|
|
54
|
+
eff_limit = self.limit
|
|
55
|
+
if self._limit_resolver:
|
|
56
|
+
try:
|
|
57
|
+
v = self._limit_resolver(request, tenant_id)
|
|
58
|
+
eff_limit = int(v) if v is not None else self.limit
|
|
59
|
+
except Exception:
|
|
60
|
+
eff_limit = self.limit
|
|
61
|
+
|
|
27
62
|
now = int(time.time())
|
|
28
63
|
# Increment counter in store
|
|
29
|
-
|
|
64
|
+
# Update store limit if it differs; stores capture configured limit internally
|
|
65
|
+
# For in-memory store, we can temporarily adjust per-request by swapping a new store instance
|
|
66
|
+
# but to keep API simple, we reuse store and clamp by eff_limit below.
|
|
67
|
+
count, store_limit, reset = self.store.incr(str(key), self.window)
|
|
68
|
+
# Enforce the effective limit selected for this request
|
|
69
|
+
limit = eff_limit
|
|
30
70
|
remaining = max(0, limit - count)
|
|
31
71
|
|
|
32
72
|
if remaining < 0: # defensive clamp
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_probes(
|
|
11
|
+
app: FastAPI,
|
|
12
|
+
*,
|
|
13
|
+
prefix: str = "/_ops",
|
|
14
|
+
include_in_schema: bool = False,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Mount basic liveness/readiness/startup probes under prefix."""
|
|
17
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
18
|
+
|
|
19
|
+
router = public_router(prefix=prefix, tags=["ops"], include_in_schema=include_in_schema)
|
|
20
|
+
|
|
21
|
+
@router.get("/live")
|
|
22
|
+
async def live() -> JSONResponse: # noqa: D401, ANN201
|
|
23
|
+
return JSONResponse({"status": "ok"})
|
|
24
|
+
|
|
25
|
+
@router.get("/ready")
|
|
26
|
+
async def ready() -> JSONResponse: # noqa: D401, ANN201
|
|
27
|
+
# In the future, add checks (DB ping, cache ping) via DI hooks.
|
|
28
|
+
return JSONResponse({"status": "ok"})
|
|
29
|
+
|
|
30
|
+
@router.get("/startup")
|
|
31
|
+
async def startup_probe() -> JSONResponse: # noqa: D401, ANN201
|
|
32
|
+
return JSONResponse({"status": "ok"})
|
|
33
|
+
|
|
34
|
+
app.include_router(router)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_maintenance_mode(app: FastAPI, *, env_var: str = "MAINTENANCE_MODE") -> None:
|
|
38
|
+
"""Enable a simple maintenance gate controlled by an env var.
|
|
39
|
+
|
|
40
|
+
When MAINTENANCE_MODE is truthy, all non-GET requests return 503.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@app.middleware("http")
|
|
44
|
+
async def _maintenance_gate(request: Request, call_next): # noqa: ANN001, ANN202
|
|
45
|
+
flag = str(os.getenv(env_var, "")).lower() in {"1", "true", "yes", "on"}
|
|
46
|
+
if flag and request.method not in {"GET", "HEAD", "OPTIONS"}:
|
|
47
|
+
return JSONResponse({"detail": "maintenance"}, status_code=503)
|
|
48
|
+
return await call_next(request)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def circuit_breaker_dependency(limit: int = 100, window_seconds: int = 60) -> Callable:
|
|
52
|
+
"""Return a dependency that can trip rejective errors based on external metrics.
|
|
53
|
+
|
|
54
|
+
This is a placeholder; callers can swap with a provider that tracks failures and opens the
|
|
55
|
+
breaker. Here, we read an env var to simulate an open breaker.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
async def _dep(_: Request) -> None: # noqa: D401, ANN202
|
|
59
|
+
if str(os.getenv("CIRCUIT_OPEN", "")).lower() in {"1", "true", "yes", "on"}:
|
|
60
|
+
raise HTTPException(status_code=503, detail="circuit open")
|
|
61
|
+
|
|
62
|
+
return _dep
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ["add_probes", "add_maintenance_mode", "circuit_breaker_dependency"]
|