svc-infra 0.1.599__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.599 → svc_infra-0.1.601}/PKG-INFO +1 -1
- {svc_infra-0.1.599 → svc_infra-0.1.601}/pyproject.toml +2 -1
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/gaurd.py +2 -2
- {svc_infra-0.1.599 → 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.599 → 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.599 → svc_infra-0.1.601}/src/svc_infra/cli/__init__.py +2 -0
- {svc_infra-0.1.599 → 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.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/repository.py +46 -9
- {svc_infra-0.1.599 → 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.599/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -148
- svc_infra-0.1.599/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -66
- {svc_infra-0.1.599 → svc_infra-0.1.601}/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/api/fastapi/setup.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/jobs/worker.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/hibp.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/permissions.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.599 → svc_infra-0.1.601}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.599 → 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.*",
|
|
@@ -12,6 +12,7 @@ from fastapi_users.password import PasswordHelper
|
|
|
12
12
|
from svc_infra.api.fastapi.auth._cookies import compute_cookie_params
|
|
13
13
|
from svc_infra.api.fastapi.auth.policy import AuthPolicy, DefaultAuthPolicy
|
|
14
14
|
from svc_infra.api.fastapi.auth.settings import get_auth_settings
|
|
15
|
+
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
15
16
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
16
17
|
|
|
17
18
|
_pwd = PasswordHelper()
|
|
@@ -66,19 +67,18 @@ def auth_session_router(
|
|
|
66
67
|
router = public_router()
|
|
67
68
|
policy = auth_policy or DefaultAuthPolicy(get_auth_settings())
|
|
68
69
|
|
|
69
|
-
from svc_infra.api.fastapi.db.sql import SqlSessionDep
|
|
70
70
|
from svc_infra.security.lockout import get_lockout_status, record_attempt
|
|
71
71
|
|
|
72
72
|
@router.post("/login", name="auth:jwt.login")
|
|
73
73
|
async def login(
|
|
74
74
|
request: Request,
|
|
75
|
+
session: SqlSessionDep,
|
|
75
76
|
username: str = Form(...),
|
|
76
77
|
password: str = Form(...),
|
|
77
78
|
scope: str = Form(""),
|
|
78
79
|
client_id: str | None = Form(None),
|
|
79
80
|
client_secret: str | None = Form(None),
|
|
80
81
|
user_manager=Depends(fapi.get_user_manager),
|
|
81
|
-
session: SqlSessionDep = Depends(),
|
|
82
82
|
):
|
|
83
83
|
strategy = auth_backend.get_strategy()
|
|
84
84
|
email = username.strip().lower()
|
|
@@ -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"]
|