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."""
from dependency_injector import containers, providers
from . import giphy, services
class Container(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."""
from sanic.request import Request
from sanic.response import HTTPResponse, json
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
async def index(
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."""
from sanic import Sanic
from .containers import Container
from . import handlers
def create_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."""
from unittest import mock
import pytest
from sanic import Sanic
from giphynavigator.application import create_app
from giphynavigator.giphy import GiphyClient
@pytest.fixture
def app():
Sanic.test_mode = True
app = create_app()
yield app
app.ctx.container.unwire()
@pytest.fixture
def test_client(loop, app, sanic_client):
return loop.run_until_complete(sanic_client(app))
async def test_index(app, test_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.ctx.container.giphy_client.override(giphy_client_mock):
response = await test_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 def test_index_no_data(app, test_client):
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 test_client.get("/")
assert response.status_code == 200
data = response.json()
assert data["gifs"] == []
async def test_index_default_params(app, test_client):
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 test_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: |