svc-infra 0.1.600__tar.gz → 0.1.601__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.601}/PKG-INFO +1 -1
- {svc_infra-0.1.600 → svc_infra-0.1.601}/pyproject.toml +2 -1
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/add.py +32 -13
- svc_infra-0.1.601/src/svc_infra/api/fastapi/db/sql/crud_router.py +292 -0
- svc_infra-0.1.601/src/svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/ratelimit.py +41 -1
- svc_infra-0.1.601/src/svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra-0.1.601/src/svc_infra/api/fastapi/tenancy/context.py +112 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/__init__.py +2 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/__init__.py +2 -0
- svc_infra-0.1.601/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +82 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/repository.py +46 -9
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/resource.py +5 -0
- svc_infra-0.1.601/src/svc_infra/db/sql/tenant.py +79 -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.601}/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/setup.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/jobs/worker.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/hibp.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/permissions.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.600 → svc_infra-0.1.601}/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.601"
|
|
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
|
|
@@ -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,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from .context import set_tenant_resolver
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_tenancy(app: FastAPI, *, resolver: Optional[Callable[..., Any]] = None) -> None:
|
|
11
|
+
"""Wire tenancy resolver for the application.
|
|
12
|
+
|
|
13
|
+
Provide a resolver(request, identity, header) -> Optional[str] to override
|
|
14
|
+
the default resolution. Pass None to clear a previous override.
|
|
15
|
+
"""
|
|
16
|
+
set_tenant_resolver(resolver)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["add_tenancy"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
try: # optional import; auth may not be used by all consumers
|
|
8
|
+
from svc_infra.api.fastapi.auth.security import OptionalIdentity
|
|
9
|
+
except Exception: # pragma: no cover - fallback for minimal builds
|
|
10
|
+
OptionalIdentity = None # type: ignore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_tenant_resolver: Optional[Callable[..., Any]] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_tenant_resolver(
|
|
17
|
+
fn: Optional[Callable[..., Any]],
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Set or clear a global override hook for tenant resolution.
|
|
20
|
+
|
|
21
|
+
The function receives (request, identity, tenant_header) and should return a tenant id
|
|
22
|
+
string or None to fall back to default logic.
|
|
23
|
+
"""
|
|
24
|
+
global _tenant_resolver
|
|
25
|
+
_tenant_resolver = fn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _maybe_await(x):
|
|
29
|
+
if callable(getattr(x, "__await__", None)):
|
|
30
|
+
return await x # type: ignore[misc]
|
|
31
|
+
return x
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def resolve_tenant_id(
|
|
35
|
+
request: Request,
|
|
36
|
+
tenant_header: Optional[str] = None,
|
|
37
|
+
identity: Any = Depends(OptionalIdentity) if OptionalIdentity else None, # type: ignore[arg-type]
|
|
38
|
+
) -> Optional[str]:
|
|
39
|
+
"""Resolve tenant id from override, identity, header, or request.state.
|
|
40
|
+
|
|
41
|
+
Order:
|
|
42
|
+
1) Global override hook (set_tenant_resolver)
|
|
43
|
+
2) Auth identity: user.tenant_id then api_key.tenant_id (if available)
|
|
44
|
+
3) X-Tenant-Id header
|
|
45
|
+
4) request.state.tenant_id
|
|
46
|
+
"""
|
|
47
|
+
# read header value if not provided directly (supports direct calls without DI)
|
|
48
|
+
if tenant_header is None:
|
|
49
|
+
try:
|
|
50
|
+
tenant_header = request.headers.get("X-Tenant-Id") # type: ignore[assignment]
|
|
51
|
+
except Exception:
|
|
52
|
+
tenant_header = None
|
|
53
|
+
|
|
54
|
+
# 1) global override
|
|
55
|
+
if _tenant_resolver is not None:
|
|
56
|
+
try:
|
|
57
|
+
v = _tenant_resolver(request, identity, tenant_header)
|
|
58
|
+
v2 = await _maybe_await(v)
|
|
59
|
+
if v2:
|
|
60
|
+
return str(v2)
|
|
61
|
+
except Exception:
|
|
62
|
+
# fall through to defaults
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# 2) from identity
|
|
66
|
+
try:
|
|
67
|
+
if identity and getattr(identity, "user", None) is not None:
|
|
68
|
+
tid = getattr(identity.user, "tenant_id", None)
|
|
69
|
+
if tid:
|
|
70
|
+
return str(tid)
|
|
71
|
+
if identity and getattr(identity, "api_key", None) is not None:
|
|
72
|
+
tid = getattr(identity.api_key, "tenant_id", None)
|
|
73
|
+
if tid:
|
|
74
|
+
return str(tid)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# 3) from header
|
|
79
|
+
if tenant_header and isinstance(tenant_header, str) and tenant_header.strip():
|
|
80
|
+
return tenant_header.strip()
|
|
81
|
+
|
|
82
|
+
# 4) request.state
|
|
83
|
+
try:
|
|
84
|
+
st_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
|
|
85
|
+
if st_tid:
|
|
86
|
+
return str(st_tid)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def require_tenant_id(
|
|
94
|
+
tenant_id: Optional[str] = Depends(resolve_tenant_id),
|
|
95
|
+
) -> str:
|
|
96
|
+
if not tenant_id:
|
|
97
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
98
|
+
return tenant_id
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# DX aliases
|
|
102
|
+
TenantId = Annotated[str, Depends(require_tenant_id)]
|
|
103
|
+
OptionalTenantId = Annotated[Optional[str], Depends(resolve_tenant_id)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = [
|
|
107
|
+
"TenantId",
|
|
108
|
+
"OptionalTenantId",
|
|
109
|
+
"resolve_tenant_id",
|
|
110
|
+
"require_tenant_id",
|
|
111
|
+
"set_tenant_resolver",
|
|
112
|
+
]
|