From 3b91452385b8eca28e26f675c15b0a8a4899aa42 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Wed, 28 Oct 2026 18:38:25 -0700 Subject: [PATCH] refactor(proxy): name dependency aliases like FastAPI's SessionDep Rename DatabaseDependency, RepositoriesDependency and UserBannerAuditDependency to the *Dep form FastAPI's docs use, point the skill at the lifespan, background task and testing docs, and stop the dependency tests from asserting on their own test lifespan. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/database-access/SKILL.md | 2 +- .../database-access/references/python-fastapi.md | 16 ++++++++++------ litellm/proxy/deps.py | 6 +++--- litellm/proxy/proxy_server.py | 2 +- .../user_banner_dependencies.py | 2 +- .../ui_crud_endpoints/user_banner_endpoints.py | 10 +++++----- tests/unit/proxy/test_deps.py | 15 +++------------ 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.agents/skills/database-access/SKILL.md b/.agents/skills/database-access/SKILL.md index 5fd95cdc363..4d795a87f92 100644 --- a/.agents/skills/database-access/SKILL.md +++ b/.agents/skills/database-access/SKILL.md @@ -20,7 +20,7 @@ metadata: ## Framework wiring -- Python routes receive `RepositoriesDependency` and call domain operations such as `repositories.banners.get_banner()` +- Python routes receive `RepositoriesDep` and call domain operations such as `repositories.banners.get_banner()` - Use named FastAPI providers with `Annotated[..., Depends(...)]`, ordinary helpers and jobs receive explicit arguments - Preserve each route's no-database behavior, authorization precedence and background-task scheduling - Read [Python and FastAPI](references/python-fastapi.md) for provider examples, lifespan state and route verification diff --git a/.agents/skills/database-access/references/python-fastapi.md b/.agents/skills/database-access/references/python-fastapi.md index 26392ad9e8f..b579495baed 100644 --- a/.agents/skills/database-access/references/python-fastapi.md +++ b/.agents/skills/database-access/references/python-fastapi.md @@ -20,18 +20,18 @@ class Repositories: banners: UserBannerRepository -async def get_repositories(database: DatabaseDependency) -> Repositories | None: +async def get_repositories(database: DatabaseDep) -> Repositories | None: return None if database is None else Repositories(banners=UserBannerRepository(database.prisma)) -RepositoriesDependency: TypeAlias = Annotated[Repositories | None, Depends(get_repositories)] +RepositoriesDep: TypeAlias = Annotated[Repositories | None, Depends(get_repositories)] ``` - FastAPI supplies the container to routes and caches the provider within each request - Ordinary helpers and jobs receive explicit arguments, never unresolved `Depends` defaults ```python -async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: +async def get_user_banner(repositories: RepositoriesDep) -> UserBanner: if repositories is None: return UserBanner() return await repositories.banners.get_banner() @@ -41,11 +41,11 @@ async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: - Prisma startup owns connection setup, authentication refresh, health checks and migrations - `Database` wraps the initialized Prisma client, without opening another pool or holding fields per feature -- FastAPI lifespan enters `litellm.repositories.database.lifespan` and publishes its handle in request state +- The proxy lifespan registers `Database.aclose` on its exit stack and publishes the handle in request state - Starlette shallow-copies lifespan state into request state, which is distinct from `app.state` - The database module never imports a repository, legacy globals borrow its handle without owning shutdown - Drain requests, jobs, logging, spend and audit work before database teardown -- `litellm.repositories.database.lifespan` closes the handle on exit, including failures after entry +- The exit stack closes the handle on exit, including startup failures after the client is created - When adding native resources, initialize them in the serving process, respecting runtime fork restrictions - Await native shutdown instead of depending on Python garbage collection - Apply the [connection ownership rules](../SKILL.md#connection-ownership) when changing the owner @@ -81,7 +81,7 @@ async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: - After the existing lifecycle owner initializes the native handle, update the provider ```diff - async def get_repositories(database: DatabaseDependency) -> Repositories | None: + async def get_repositories(database: DatabaseDep) -> Repositories | None: - return None if database is None else Repositories(banners=UserBannerRepository(database.prisma)) + if database is None: + return None @@ -131,5 +131,9 @@ async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: ## Framework references +- [Lifespan events](https://fastapi.tiangolo.com/advanced/events/) and [Starlette lifespan state](https://starlette.dev/lifespan/) +- [`SessionDep`-style `Annotated` aliases](https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-session-dependency), which `DatabaseDep` and `RepositoriesDep` follow - [Dependency composition and caching](https://fastapi.tiangolo.com/tutorial/dependencies/sub-dependencies/) +- [Background tasks and dependencies with `yield`](https://fastapi.tiangolo.com/advanced/advanced-dependencies/#background-tasks-and-dependencies-with-yield-technical-details) - [Dependency overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/) +- [Testing lifespan](https://fastapi.tiangolo.com/advanced/testing-events/) diff --git a/litellm/proxy/deps.py b/litellm/proxy/deps.py index ccb5c990732..ae5519f02a7 100644 --- a/litellm/proxy/deps.py +++ b/litellm/proxy/deps.py @@ -21,11 +21,11 @@ async def get_database(request: Request) -> Database | None: raise DatabaseStateMissingError() -DatabaseDependency: TypeAlias = Annotated[Database | None, Depends(get_database)] +DatabaseDep: TypeAlias = Annotated[Database | None, Depends(get_database)] -async def get_repositories(database: DatabaseDependency) -> Repositories | None: +async def get_repositories(database: DatabaseDep) -> Repositories | None: return None if database is None else Repositories(banners=UserBannerRepository(database.prisma)) -RepositoriesDependency: TypeAlias = Annotated[Repositories | None, Depends(get_repositories)] +RepositoriesDep: TypeAlias = Annotated[Repositories | None, Depends(get_repositories)] diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4a21cd91e7c..7dd42518f0e 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1443,7 +1443,7 @@ def _own_database(resources: AsyncExitStack, client: PrismaClient | None) -> dat def _forget_prisma_client() -> None: - global prisma_client # noqa: PLW0603 # routes still read this global until they move to DatabaseDependency + global prisma_client # noqa: PLW0603 # routes still read this global until they move to DatabaseDep prisma_client = None # rebind-ok: a closed client must not survive into the next lifespan entry diff --git a/litellm/proxy/ui_crud_endpoints/user_banner_dependencies.py b/litellm/proxy/ui_crud_endpoints/user_banner_dependencies.py index 0b8f2afe80c..6d5b43504df 100644 --- a/litellm/proxy/ui_crud_endpoints/user_banner_dependencies.py +++ b/litellm/proxy/ui_crud_endpoints/user_banner_dependencies.py @@ -28,4 +28,4 @@ async def get_user_banner_audit() -> UserBannerAudit: return audit -UserBannerAuditDependency: TypeAlias = Annotated[UserBannerAudit, Depends(get_user_banner_audit)] +UserBannerAuditDep: TypeAlias = Annotated[UserBannerAudit, Depends(get_user_banner_audit)] diff --git a/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py b/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py index 42d930760dd..2934887252d 100644 --- a/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py @@ -7,9 +7,9 @@ from litellm._uuid import uuid4 from litellm.models.user_banner import UserBanner, UserBannerUpdate from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.deps import RepositoriesDependency +from litellm.proxy.deps import RepositoriesDep from litellm.proxy.ui_crud_endpoints.user_banner_dependencies import ( - UserBannerAuditDependency, + UserBannerAuditDep, ) from litellm.types.llms.base import LiteLLMBaseModel @@ -29,7 +29,7 @@ class UpdateUserBannerResponse(LiteLLMBaseModel): dependencies=[Depends(user_api_key_auth)], response_model=UserBanner, ) -async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: +async def get_user_banner(repositories: RepositoriesDep) -> UserBanner: """ Get the admin-published dashboard banner. Readable by any authenticated user; rendered on every dashboard page. @@ -48,8 +48,8 @@ async def get_user_banner(repositories: RepositoriesDependency) -> UserBanner: async def update_user_banner( banner_update: UserBannerUpdate, user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], - repositories: RepositoriesDependency, - audit: UserBannerAuditDependency, + repositories: RepositoriesDep, + audit: UserBannerAuditDep, background_tasks: BackgroundTasks, ) -> UpdateUserBannerResponse: """ diff --git a/tests/unit/proxy/test_deps.py b/tests/unit/proxy/test_deps.py index a9c94c142dc..da54d9eef5e 100644 --- a/tests/unit/proxy/test_deps.py +++ b/tests/unit/proxy/test_deps.py @@ -9,20 +9,19 @@ from fastapi.testclient import TestClient from starlette.requests import Request from litellm.models.user_banner import UserBanner -from litellm.proxy.deps import DatabaseStateMissingError, RepositoriesDependency, get_database +from litellm.proxy.deps import DatabaseStateMissingError, RepositoriesDep, get_database from litellm.repositories import database as database_runtime def _app(database: database_runtime.Database | None) -> FastAPI: @asynccontextmanager async def lifespan(app: FastAPI): - async with database_runtime.lifespan(database) as owned_database: - yield {"database": owned_database} + yield {"database": database} app: Final = FastAPI(lifespan=lifespan) @app.get("/banner") - async def banner(repositories: RepositoriesDependency) -> UserBanner: + async def banner(repositories: RepositoriesDep) -> UserBanner: return UserBanner() if repositories is None else await repositories.banners.get_banner() return app @@ -30,8 +29,6 @@ def _app(database: database_runtime.Database | None) -> FastAPI: def test_lifespan_database_is_shared_until_shutdown(): prisma: Final = MagicMock() - prisma.disconnect = AsyncMock() - prisma.stop_background_tasks = AsyncMock() prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None) app: Final = _app(database_runtime.Database(prisma=prisma)) with TestClient(app) as client: @@ -43,8 +40,6 @@ def test_lifespan_database_is_shared_until_shutdown(): client.get("/banner").json() == UserBanner(enabled=True, message="published", revision="revision").model_dump() ) - prisma.disconnect.assert_not_awaited() - prisma.disconnect.assert_awaited_once() def test_no_database_state_preserves_optional_repository(): @@ -56,8 +51,6 @@ def test_no_database_state_preserves_optional_repository(): def test_dependency_override_changes_next_request_without_changing_lifespan_owner(monkeypatch): prisma: Final = MagicMock() - prisma.disconnect = AsyncMock() - prisma.stop_background_tasks = AsyncMock() prisma.db.litellm_uisettings.find_unique = AsyncMock( return_value=SimpleNamespace(ui_settings={"message": "stored"}) ) @@ -66,8 +59,6 @@ def test_dependency_override_changes_next_request_without_changing_lifespan_owne assert client.get("/banner").json() == UserBanner(message="stored").model_dump() monkeypatch.setitem(app.dependency_overrides, get_database, lambda: None) assert client.get("/banner").json() == UserBanner().model_dump() - prisma.disconnect.assert_not_awaited() - prisma.disconnect.assert_awaited_once() @pytest.mark.asyncio