mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
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:
parent
5d43684c7a
commit
3b91452385
7 changed files with 24 additions and 29 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue