diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 3ca93d4c..64153c71 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -10,6 +10,7 @@ follows `Semantic versioning`_ 4.0.0 ----- - Add ``wiring`` feature. +- Add ``sanic`` example. - Update ``aiohttp`` example. - Update ``flask`` example. diff --git a/examples/miniapps/sanic/README.rst b/examples/miniapps/sanic/README.rst new file mode 100644 index 00000000..13d0ebdf --- /dev/null +++ b/examples/miniapps/sanic/README.rst @@ -0,0 +1,117 @@ +Dependency Injector + Sanic Example +===================================== + +Application ``giphynavigator`` is a `Sanic `_ + +`Dependency Injector `_ example application. + +Run +--- + +Create virtual environment: + +.. code-block:: bash + + virtualenv venv + . venv/bin/activate + +Install requirements: + +.. code-block:: bash + + pip install -r requirements.txt + +To run the application do: + +.. code-block:: bash + + export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0 + python -m giphynavigator + +The output should be something like: + +.. code-block:: + + [2020-09-23 18:16:31 -0400] [48258] [INFO] Goin' Fast @ http://0.0.0.0:8000 + [2020-09-23 18:16:31 -0400] [48258] [INFO] Starting worker [48258] + +After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``, +etc). You should see something like: + +.. code-block:: json + + { + "query": "Dependency Injector", + "limit": 10, + "gifs": [ + { + "url": "https://giphy.com/gifs/boxes-dependent-swbf2-6Eo7KzABxgJMY" + }, + { + "url": "https://giphy.com/gifs/depends-J56qCcOhk6hKE" + }, + { + "url": "https://giphy.com/gifs/web-series-ccstudios-bro-dependent-1lhU8KAVwmVVu" + }, + { + "url": "https://giphy.com/gifs/TheBoysTV-friends-friend-weneedeachother-XxR9qcIwcf5Jq404Sx" + }, + { + "url": "https://giphy.com/gifs/netflix-a-series-of-unfortunate-events-asoue-9rgeQXbwoK53pcxn7f" + }, + { + "url": "https://giphy.com/gifs/black-and-white-sad-skins-Hs4YzLs2zJuLu" + }, + { + "url": "https://giphy.com/gifs/always-there-for-you-i-am-here-PlayjhCco9jHBYrd9w" + }, + { + "url": "https://giphy.com/gifs/stream-famous-dollar-YT2dvOByEwXCdoYiA1" + }, + { + "url": "https://giphy.com/gifs/i-love-you-there-for-am-1BhGzgpZXYWwWMAGB1" + }, + { + "url": "https://giphy.com/gifs/life-like-twerk-9hlnWxjHqmH28" + } + ] + } + +.. note:: + + To create your own Giphy API key follow this + `guide `_. + +Test +---- + +This application comes with the unit tests. + +To run the tests do: + +.. code-block:: bash + + py.test giphynavigator/tests.py --cov=giphynavigator + +The output should be something like: + +.. code-block:: + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0, sanic-1.6.1 + collected 3 items + + giphynavigator/tests.py ... [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + --------------------------------------------------- + giphynavigator/__init__.py 0 0 100% + giphynavigator/__main__.py 4 4 0% + giphynavigator/application.py 12 0 100% + giphynavigator/containers.py 6 0 100% + giphynavigator/giphy.py 14 9 36% + giphynavigator/handlers.py 10 0 100% + giphynavigator/services.py 9 1 89% + giphynavigator/tests.py 34 0 100% + --------------------------------------------------- + TOTAL 89 14 84% diff --git a/examples/miniapps/sanic/config.yml b/examples/miniapps/sanic/config.yml new file mode 100644 index 00000000..d1276f8c --- /dev/null +++ b/examples/miniapps/sanic/config.yml @@ -0,0 +1,5 @@ +giphy: + request_timeout: 10 +default: + query: "Dependency Injector" + limit: 10 diff --git a/examples/miniapps/sanic/giphynavigator/__init__.py b/examples/miniapps/sanic/giphynavigator/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/sanic/giphynavigator/__main__.py b/examples/miniapps/sanic/giphynavigator/__main__.py new file mode 100644 index 00000000..5c7f8360 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/__main__.py @@ -0,0 +1,8 @@ +"""Main module.""" + +from .application import create_app + + +if __name__ == '__main__': + app = create_app() + app.run(host='0.0.0.0', port=8000, debug=True) diff --git a/examples/miniapps/sanic/giphynavigator/application.py b/examples/miniapps/sanic/giphynavigator/application.py new file mode 100644 index 00000000..bdf9ee33 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/application.py @@ -0,0 +1,22 @@ +"""Application module.""" + +from sanic import Sanic + +from .containers import Container +from . import handlers + + +def create_app(): + """Create and return aiohttp application.""" + container = Container() + container.config.from_yaml('config.yml') + container.config.giphy.api_key.from_env('GIPHY_API_KEY') + + container.wire(modules=[handlers]) + + app = Sanic('Giphy Navigator') + app.container = container + + app.add_route(handlers.index, '/') + + return app diff --git a/examples/miniapps/sanic/giphynavigator/containers.py b/examples/miniapps/sanic/giphynavigator/containers.py new file mode 100644 index 00000000..730c162e --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/containers.py @@ -0,0 +1,21 @@ +"""Containers module.""" + +from dependency_injector import containers, providers + +from . import giphy, services + + +class Container(containers.DeclarativeContainer): + + config = providers.Configuration() + + 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, + ) diff --git a/examples/miniapps/sanic/giphynavigator/giphy.py b/examples/miniapps/sanic/giphynavigator/giphy.py new file mode 100644 index 00000000..18d0f6b9 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/giphy.py @@ -0,0 +1,26 @@ +"""Giphy client module.""" + +from aiohttp import ClientSession, ClientTimeout + + +class GiphyClient: + + API_URL = 'http://api.giphy.com/v1' + + def __init__(self, api_key, timeout): + self._api_key = api_key + self._timeout = ClientTimeout(timeout) + + async def search(self, query, limit): + """Make search API call and return result.""" + url = f'{self.API_URL}/gifs/search' + params = { + 'q': query, + 'api_key': self._api_key, + 'limit': limit, + } + async with ClientSession(timeout=self._timeout) as session: + async with session.get(url, params=params) as response: + if response.status != 200: + response.raise_for_status() + return await response.json() diff --git a/examples/miniapps/sanic/giphynavigator/handlers.py b/examples/miniapps/sanic/giphynavigator/handlers.py new file mode 100644 index 00000000..6b319a25 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/handlers.py @@ -0,0 +1,28 @@ +"""Handlers module.""" + +from sanic.request import Request +from sanic.response import HTTPResponse, json +from dependency_injector.wiring import Provide + +from .services import SearchService +from .containers import Container + + +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, + }, + ) diff --git a/examples/miniapps/sanic/giphynavigator/services.py b/examples/miniapps/sanic/giphynavigator/services.py new file mode 100644 index 00000000..1c86e0d7 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/services.py @@ -0,0 +1,18 @@ +"""Services module.""" + +from .giphy import GiphyClient + + +class SearchService: + + def __init__(self, giphy_client: GiphyClient): + self._giphy_client = giphy_client + + async def search(self, query, limit): + """Search for gifs and return formatted data.""" + if not query: + return [] + + result = await self._giphy_client.search(query, limit) + + return [{'url': gif['url']} for gif in result['data']] diff --git a/examples/miniapps/sanic/giphynavigator/tests.py b/examples/miniapps/sanic/giphynavigator/tests.py new file mode 100644 index 00000000..c4a14256 --- /dev/null +++ b/examples/miniapps/sanic/giphynavigator/tests.py @@ -0,0 +1,74 @@ +"""Tests module.""" + +from unittest import mock + +import pytest + +from giphynavigator.application import create_app +from giphynavigator.giphy import GiphyClient + + +@pytest.fixture +def app(): + app = create_app() + yield app + app.container.unwire() + + +async def test_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.container.giphy_client.override(giphy_client_mock): + _, response = await app.asgi_client.get( + '/', + params={ + 'query': 'test', + 'limit': 10, + }, + ) + + assert response.status == 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): + 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 app.asgi_client.get('/') + + assert response.status == 200 + data = response.json() + assert data['gifs'] == [] + + +async def test_index_default_params(app): + 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 app.asgi_client.get('/') + + assert response.status == 200 + data = response.json() + assert data['query'] == app.container.config.default.query() + assert data['limit'] == app.container.config.default.limit() diff --git a/examples/miniapps/sanic/requirements.txt b/examples/miniapps/sanic/requirements.txt new file mode 100644 index 00000000..8f3a6e1a --- /dev/null +++ b/examples/miniapps/sanic/requirements.txt @@ -0,0 +1,5 @@ +dependency-injector +sanic +pyyaml +pytest-sanic +pytest-cov