Source code for svcs.fastapi
# SPDX-FileCopyrightText: 2023 Hynek Schlawack <hs@ox.cx>
#
# SPDX-License-Identifier: MIT
from __future__ import annotations
import contextlib
import inspect
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Annotated, Any, TypeAlias, cast
import attrs
from fastapi import Depends, FastAPI, Request
import svcs
from svcs._core import _KEY_REGISTRY
if TYPE_CHECKING:
from fastapi.testclient import TestClient
else:
try:
from fastapi.testclient import TestClient
except (ImportError, RuntimeError): # pragma: no cover
TestClient = Any
AsyncGenLifespan: TypeAlias = Callable[
[FastAPI, svcs.Registry],
AsyncGenerator[dict[str, object] | None, None],
]
AsyncCMLifespan: TypeAlias = Callable[
[FastAPI, svcs.Registry],
contextlib.AbstractAsyncContextManager[dict[str, object] | None],
]
SomeLifespan: TypeAlias = AsyncGenLifespan | AsyncCMLifespan
[docs]
@attrs.define
class lifespan: # noqa: N801
"""
Make a FastAPI lifespan *svcs*-aware.
Makes sure that the registry is available to the decorated lifespan
function as a second parameter and that the registry is closed when the
application exists.
Async generators are automatically wrapped into an async context manager.
Args:
lifespan: The lifespan function to make *svcs*-aware.
"""
_lifespan: SomeLifespan
_state: dict[str, object] = attrs.field(factory=dict)
registry: svcs.Registry = attrs.field(factory=svcs.Registry)
@contextlib.asynccontextmanager
async def __call__(
self, app: FastAPI
) -> AsyncGenerator[dict[str, object], None]:
cm: AsyncCMLifespan
if inspect.isasyncgenfunction(self._lifespan):
cm = contextlib.asynccontextmanager(
cast(AsyncGenLifespan, self._lifespan)
)
else:
cm = cast(AsyncCMLifespan, self._lifespan)
# FastAPI enters merged lifespans app-first, but merges their
# states first-wins. Mirror that precedence: only the first
# svcs lifespan attaches its registry and detaches it on exit.
owns_app_state = not hasattr(app.state, _KEY_REGISTRY)
if owns_app_state:
setattr(app.state, _KEY_REGISTRY, self.registry)
try:
async with self.registry, cm(app, self.registry) as state:
self._state = state or {}
self._state[_KEY_REGISTRY] = self.registry
yield self._state
finally:
if owns_app_state:
delattr(app.state, _KEY_REGISTRY)
[docs]
def get_registry(app: FastAPI | TestClient) -> svcs.Registry:
"""
Get the registry that :class:`lifespan` has attached to *app*.
The registry is attached when the application starts, so this only works
on a running application.
Args:
app:
A FastAPI application with a *svcs*-aware lifespan, or a
:class:`fastapi.testclient.TestClient` wrapping one.
Raises:
LookupError: If no registry is attached to *app*.
.. versionadded:: 26.2.0
"""
try:
if not isinstance(app, FastAPI):
app = cast("FastAPI", app.app)
return getattr(app.state, _KEY_REGISTRY) # type: ignore[no-any-return]
except AttributeError:
msg = "No svcs registry on app."
raise LookupError(msg) from None
[docs]
async def container(request: Request) -> AsyncGenerator[svcs.Container, None]:
"""
A FastAPI `dependency
<https://fastapi.tiangolo.com/tutorial/dependencies/>`_ that provides you
with a request-scoped container.
Yields:
A :class:`svcs.Container` that is cleaned up after the request.
"""
async with svcs.Container(getattr(request.state, _KEY_REGISTRY)) as cont:
yield cont
DepContainer = Annotated[svcs.Container, Depends(container)]
"""
An alias for::
typing.Annotated[svcs.Container, fastapi.Depends(svcs.fastapi.container)]
This allows you write your view like::
@app.get("/")
async def view(services: svcs.fastapi.DepContainer):
...
"""
[docs]
async def registry(request: Request) -> svcs.Registry:
"""
A FastAPI `dependency
<https://fastapi.tiangolo.com/tutorial/dependencies/>`_ that provides you
with the application's registry.
.. versionadded:: 26.2.0
"""
return getattr(request.state, _KEY_REGISTRY) # type: ignore[no-any-return]
DepRegistry = Annotated[svcs.Registry, Depends(registry)]
"""
An alias for::
typing.Annotated[svcs.Registry, fastapi.Depends(svcs.fastapi.registry)]
This allows you write your view like::
@app.get("/")
async def view(registry: svcs.fastapi.DepRegistry):
...
.. versionadded:: 26.2.0
"""