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 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-28 18:38:25 -07:00
parent 5d43684c7a
commit 3b91452385
7 changed files with 24 additions and 29 deletions

View file

@ -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

View file

@ -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/)

View file

@ -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)]

View file

@ -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

View file

@ -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)]

View file

@ -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:
"""

View file

@ -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