FastAPI example

This example shows how to use Dependency Injector with FastAPI.

The example application is a REST API that searches for funny GIFs on the Giphy.

The source code is available on the Github.

Application structure

Application has next structure:

./
├──giphynavigator/
│├──__init__.py
│├──application.py
│├──containers.py
│├──endpoints.py
│├──giphy.py
│├──services.py
│└──tests.py
├──config.yml
└──requirements.txt

Container

Declarative container is defined in giphynavigator/containers.py:

"""Containers module."""
fromdependency_injectorimport containers, providers
from.import giphy, services
classContainer(containers.DeclarativeContainer):
 wiring_config = containers.WiringConfiguration(modules=[".endpoints"])
 config = providers.Configuration(yaml_files=["config.yml"])
 giphy_client = providers.Factory(
 giphy.GiphyClient,
 api_key=config.giphy.api_key,
 timeout=config.giphy.request_timeout,
 )
 search_service = providers.Factory(
 services.SearchService,
 giphy_client=giphy_client,
 )

Endpoints

Endpoint has a dependency on search service. There are also some config options that are used as default values. The dependencies are injected using Wiring feature.

Listing of giphynavigator/endpoints.py:

"""Endpoints module."""
fromtypingimport Annotated, List
fromfastapiimport APIRouter, Depends
frompydanticimport BaseModel
fromdependency_injector.wiringimport Provide, inject
from.containersimport Container
from.servicesimport SearchService
classGif(BaseModel):
 url: str
classResponse(BaseModel):
 query: str
 limit: int
 gifs: List[Gif]
router = APIRouter()
@router.get("/", response_model=Response)
@inject
async defindex(
 default_query: Annotated[str, Depends(Provide[Container.config.default.query])],
 default_limit: Annotated[
 int, Depends(Provide[Container.config.default.limit.as_int()])
 ],
 search_service: Annotated[
 SearchService, Depends(Provide[Container.search_service])
 ],
 query: str | None = None,
 limit: int | None = None,
):
 query = query or default_query
 limit = limit or default_limit
 gifs = await search_service.search(query, limit)
 return {
 "query": query,
 "limit": limit,
 "gifs": gifs,
 }

Application factory

Application factory creates container, wires it with the endpoints module, creates FastAPI app, and setup routes.

Listing of giphynavigator/application.py:

"""Application module."""
fromfastapiimport FastAPI
from.containersimport Container
from.import endpoints
defcreate_app() -> FastAPI:
 container = Container()
 container.config.giphy.api_key.from_env("GIPHY_API_KEY")
 app = FastAPI()
 app.container = container
 app.include_router(endpoints.router)
 return app
app = create_app()

Tests

Tests use Provider overriding feature to replace giphy client with a mock giphynavigator/tests.py:

"""Tests module."""
fromunittestimport mock
importpytest
importpytest_asyncio
fromhttpximport ASGITransport, AsyncClient
fromgiphynavigator.applicationimport app
fromgiphynavigator.giphyimport GiphyClient
@pytest_asyncio.fixture
async defclient():
 async with AsyncClient(
 transport=ASGITransport(app=app),
 base_url="http://test",
 ) as client:
 yield client
@pytest.mark.asyncio
async deftest_index(client):
 giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
 giphy_client_mock.search.return_value = {
 "data": [
 {"url": "https://giphy.com/gif1.gif"},
 {"url": "https://giphy.com/gif2.gif"},
 ],
 }
 with app.container.giphy_client.override(giphy_client_mock):
 response = await client.get(
 "/",
 params={
 "query": "test",
 "limit": 10,
 },
 )
 assert response.status_code == 200
 data = response.json()
 assert data == {
 "query": "test",
 "limit": 10,
 "gifs": [
 {"url": "https://giphy.com/gif1.gif"},
 {"url": "https://giphy.com/gif2.gif"},
 ],
 }
@pytest.mark.asyncio
async deftest_index_no_data(client):
 giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
 giphy_client_mock.search.return_value = {
 "data": [],
 }
 with app.container.giphy_client.override(giphy_client_mock):
 response = await client.get("/")
 assert response.status_code == 200
 data = response.json()
 assert data["gifs"] == []
@pytest.mark.asyncio
async deftest_index_default_params(client):
 giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
 giphy_client_mock.search.return_value = {
 "data": [],
 }
 with app.container.giphy_client.override(giphy_client_mock):
 response = await client.get("/")
 assert response.status_code == 200
 data = response.json()
 assert data["query"] == app.container.config.default.query()
 assert data["limit"] == app.container.config.default.limit()

Sources

Explore the sources on the Github.

Sponsor the project on GitHub:

[フレーム]