From 2e928f7d47fbd99910dde1693a1a9b552f920ad5 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 29 Jul 2020 22:17:12 -0400 Subject: [PATCH] Add more tutorial sections --- docs/tutorials/aiohttp.rst | 308 ++++++++++++++++++++++++++++++++++--- 1 file changed, 290 insertions(+), 18 deletions(-) diff --git a/docs/tutorials/aiohttp.rst b/docs/tutorials/aiohttp.rst index 0a75e0b8..7b845ab5 100644 --- a/docs/tutorials/aiohttp.rst +++ b/docs/tutorials/aiohttp.rst @@ -3,8 +3,8 @@ Aiohttp tutorial .. _aiohttp-tutorial: -This tutorials shows how to build ``Aiohttp`` REST API application following dependency injection -principle. +This tutorials shows how to build an ``aiohttp`` REST API application following the dependency +injection principle. Start from the scratch or jump to the section: @@ -627,7 +627,7 @@ and make a request to the API in the terminal: .. code-block:: bash - http http://localhost:8000/ query=="wow,it works" + http http://localhost:8000/ query=="wow,it works" limit==5 You should see: @@ -656,21 +656,6 @@ You should see: { "url": "https://giphy.com/gifs/everwhatproductions-fun-christmas-3oxHQCI8tKXoeW4IBq" }, - { - "url": "https://giphy.com/gifs/spacestationgaming-love-wow-team-YST1F1J5g2yyLLvMJc" - }, - { - "url": "https://giphy.com/gifs/dollyparton-3xIVVMnZfG3KQ9v4Ye" - }, - { - "url": "https://giphy.com/gifs/greatbigstory-wow-omg-BLGlU7OWvFAFMoNjsM" - }, - { - "url": "https://giphy.com/gifs/soulpancake-wow-work-xUe4HVXTPi0wQ2OAJC" - }, - { - "url": "https://giphy.com/gifs/nickelodeon-nick-pull-ups-casagrandes-eK136cynbxuOVk0qzJ" - } ], "limit": 10, "query": "wow,it works" @@ -683,10 +668,297 @@ The search works! Make some refactoring --------------------- +Our ``index`` view has two hardcoded config values: + +- Default search query +- Default results limit + +Let's make some refactoring. We will move these values to the config. + +Edit ``views.py``: + +.. code-block:: python + :emphasize-lines: 11-12,14-15 + + """Views module.""" + + from aiohttp import web + + from .services import SearchService + + + async def index( + request: web.Request, + search_service: SearchService, + default_query: str, + default_limit: int, + ) -> web.Response: + query = request.query.get('query', default_query) + limit = int(request.query.get('limit', default_limit)) + + gifs = await search_service.search(query, limit) + + return web.json_response( + { + 'query': query, + 'limit': limit, + 'gifs': gifs, + }, + ) + +Now we need to inject these values. Let's update the container. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 31-32 + + """Application containers module.""" + + from dependency_injector import containers, providers + from dependency_injector.ext import aiohttp + from aiohttp import web + + from . import giphy, services, views + + + class ApplicationContainer(containers.DeclarativeContainer): + """Application container.""" + + app = aiohttp.Application(web.Application) + + 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, + ) + + index_view = aiohttp.View( + views.index, + search_service=search_service, + default_query=config.search.default_query, + default_limit=config.search.default_limit, + ) + +Finally let's update the config. + +Edit ``config.yml``: + +.. code-block:: yaml + :emphasize-lines: 3-5 + + giphy: + request_timeout: 10 + search: + default_query: "Dependency Injector" + default_limit: 10 + +The refactoring is done. We've made it cleaner - hardcoded values are now moved to the config. + +In the next section we will add some tests. + Tests ----- +It would be nice to add some tests. Let's do it. + +We will use `pytest `_ and +`coverage `_. + +Create ``tests.py`` module in the ``giphynavigator`` package: + +.. code-block:: bash + :emphasize-lines: 8 + + ./ + ├── giphynavigator/ + │ ├── __init__.py + │ ├── application.py + │ ├── containers.py + │ ├── giphy.py + │ ├── services.py + │ ├── tests.py + │ └── views.py + ├── venv/ + └── requirements.txt + +and put next into it: + +.. code-block:: python + :emphasize-lines: 30,57,71 + + """Tests module.""" + + from unittest import mock + + import pytest + + from giphynavigator.application import create_app + from giphynavigator.giphy import GiphyClient + + + @pytest.fixture + def app(): + return create_app() + + + @pytest.fixture + def client(app, aiohttp_client, loop): + return loop.run_until_complete(aiohttp_client(app)) + + + async def test_index(client, 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 client.get( + '/', + params={ + 'query': 'test', + 'limit': 10, + }, + ) + + assert response.status == 200 + data = await 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(client, 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 client.get('/') + + assert response.status == 200 + data = await response.json() + assert data['gifs'] == [] + + + async def test_index_default_params(client, 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 client.get('/') + + assert response.status == 200 + data = await response.json() + assert data['query'] == app.container.config.search.default_query() + assert data['limit'] == app.container.config.search.default_limit() + +Now let's run it and check the coverage: + +.. code-block:: bash + + py.test giphynavigator/tests.py --cov=giphynavigator + +You should see: + +.. code-block:: bash + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0 + 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 5 5 0% + giphynavigator/application.py 10 0 100% + giphynavigator/containers.py 10 0 100% + giphynavigator/giphy.py 16 11 31% + giphynavigator/services.py 9 1 89% + giphynavigator/tests.py 35 0 100% + giphynavigator/views.py 7 0 100% + --------------------------------------------------- + TOTAL 92 17 82% + +.. note:: + + Take a look at the highlights in the ``tests.py``. + + It emphasizes the overriding of the ``GiphyClient``. The real API call are mocked. + Conclusion ---------- +In this tutorial we've build an ``aiohttp`` REST API application following the dependency +injection principle. +We've used ``Dependency Injector`` as a dependency injection framework. + +The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff +when you need to understand or change your application structure. It's easy with the container, +cause you have everything in one place: + +.. code-block:: python + + """Application containers module.""" + + from dependency_injector import containers, providers + from dependency_injector.ext import aiohttp + from aiohttp import web + + from . import giphy, services, views + + + class ApplicationContainer(containers.DeclarativeContainer): + """Application container.""" + + app = aiohttp.Application(web.Application) + + 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, + ) + + index_view = aiohttp.View( + views.index, + search_service=search_service, + default_query=config.search.default_query, + default_limit=config.search.default_limit, + ) + +What's next? + +- Look at the other :ref:`tutorials`. +- Know more about the :ref:`providers`. +- Go to the :ref:`contents`. + .. disqus::