Sanic example

This example shows how to useDependencyInjector withSanic.

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

The source code is available on theGithub.

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 ingiphynavigator/containers.py:

"""Containers module."""fromdependency_injectorimportcontainers,providersfrom.importgiphy,servicesclassContainer(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 injectedusingWiring feature.

Listing ofgiphynavigator/handlers.py:

"""Handlers module."""fromsanic.requestimportRequestfromsanic.responseimportHTTPResponse,jsonfromdependency_injector.wiringimportinject,Providefrom.servicesimportSearchServicefrom.containersimportContainer@injectasyncdefindex(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=awaitsearch_service.search(query,limit)returnjson({"query":query,"limit":limit,"gifs":gifs,},)

Application factory

Application factory creates container, wires it with thehandlers module, createsSanic app and setup routes.

Listing ofgiphynavigator/application.py:

"""Application module."""fromsanicimportSanicfrom.containersimportContainerfrom.importhandlersdefcreate_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=containerapp.add_route(handlers.index,"/")returnapp

Tests

Tests useProvider overriding feature to replace giphy client with a mockgiphynavigator/tests.py:

"""Tests module."""fromunittestimportmockimportpytestfromsanicimportSanicfromgiphynavigator.applicationimportcreate_appfromgiphynavigator.giphyimportGiphyClientpytestmark=pytest.mark.asyncio@pytest.fixturedefapp():Sanic.test_mode=Trueapp=create_app()yieldappapp.ctx.container.unwire()asyncdeftest_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"},],}withapp.ctx.container.giphy_client.override(giphy_client_mock):_,response=awaitapp.asgi_client.get("/",params={"query":"test","limit":10,},)assertresponse.status_code==200data=response.jsonassertdata=={"query":"test","limit":10,"gifs":[{"url":"https://giphy.com/gif1.gif"},{"url":"https://giphy.com/gif2.gif"},],}asyncdeftest_index_no_data(app):giphy_client_mock=mock.AsyncMock(spec=GiphyClient)giphy_client_mock.search.return_value={"data":[],}withapp.ctx.container.giphy_client.override(giphy_client_mock):_,response=awaitapp.asgi_client.get("/")assertresponse.status_code==200data=response.jsonassertdata["gifs"]==[]asyncdeftest_index_default_params(app):giphy_client_mock=mock.AsyncMock(spec=GiphyClient)giphy_client_mock.search.return_value={"data":[],}withapp.ctx.container.giphy_client.override(giphy_client_mock):_,response=awaitapp.asgi_client.get("/")assertresponse.status_code==200data=response.jsonassertdata["query"]==app.ctx.container.config.default.query()assertdata["limit"]==app.ctx.container.config.default.limit()

Sources

Explore the sources on theGithub.

Sponsor the project on GitHub: