Sanic example

This example shows how to use Dependency Injector with Sanic.

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
│├──__main__.py
│├──application.py
│├──containers.py
│├──giphy.py
│├──handlers.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=[".handlers"])
 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,
 )

Handlers

Handler has dependencies on search service and some config options. The dependencies are injected using Wiring feature.

Listing of giphynavigator/handlers.py:

"""Handlers module."""
fromsanic.requestimport Request
fromsanic.responseimport HTTPResponse, json
fromdependency_injector.wiringimport inject, Provide
from.servicesimport SearchService
from.containersimport Container
@inject
async defindex(
 request: Request,
 search_service: SearchService = Provide[Container.search_service],
 default_query: str = Provide[Container.config.default.query],
 default_limit: int = Provide[Container.config.default.limit.as_int()],
) -> HTTPResponse:
 query = request.args.get("query", default_query)
 limit = int(request.args.get("limit", default_limit))
 gifs = await search_service.search(query, limit)
 return json(
 {
 "query": query,
 "limit": limit,
 "gifs": gifs,
 },
 )

Application factory

Application factory creates container, wires it with the handlers module, creates Sanic app and setup routes.

Listing of giphynavigator/application.py:

"""Application module."""
fromsanicimport Sanic
from.containersimport Container
from.import handlers
defcreate_app() -> Sanic:
"""Create and return Sanic application."""
 container = Container()
 container.config.giphy.api_key.from_env("GIPHY_API_KEY")
 app = Sanic("giphy-navigator")
 app.ctx.container = container
 app.add_route(handlers.index, "/")
 return app

Tests

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

"""Tests module."""
fromunittestimport mock
importpytest
fromsanicimport Sanic
fromgiphynavigator.applicationimport create_app
fromgiphynavigator.giphyimport GiphyClient
pytestmark = pytest.mark.asyncio
@pytest.fixture
defapp():
 Sanic.test_mode = True
 app = create_app()
 yield app
 app.ctx.container.unwire()
async deftest_index(app):
 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.ctx.container.giphy_client.override(giphy_client_mock):
 _, response = await app.asgi_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"},
 ],
 }
async deftest_index_no_data(app):
 giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
 giphy_client_mock.search.return_value = {
 "data": [],
 }
 with app.ctx.container.giphy_client.override(giphy_client_mock):
 _, response = await app.asgi_client.get("/")
 assert response.status_code == 200
 data = response.json
 assert data["gifs"] == []
async deftest_index_default_params(app):
 giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
 giphy_client_mock.search.return_value = {
 "data": [],
 }
 with app.ctx.container.giphy_client.override(giphy_client_mock):
 _, response = await app.asgi_client.get("/")
 assert response.status_code == 200
 data = response.json
 assert data["query"] == app.ctx.container.config.default.query()
 assert data["limit"] == app.ctx.container.config.default.limit()

Sources

Explore the sources on the Github.

Sponsor the project on GitHub:

[フレーム]