Aiohttp tutorial (#271)

* Make a code style change to the giphynav-aiohttp app

* Make minimal punctuation changes for the flask tutorial

* Add parts of http tutorial

* Fix few issues in the flask tutorial

* Make some cosmetic changes to test data

* Fix typo in flask tutorial
This commit is contained in:
Roman Mogylatov 2020-07-29 22:16:02 -04:00 committed by GitHub
parent 577c7854da
commit adcea61657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 700 additions and 13 deletions

View File

@ -1,6 +1,692 @@
Aiohttp tutorial
================
Coming soon...
.. _aiohttp-tutorial:
This tutorials shows how to build ``Aiohttp`` REST API application following dependency injection
principle.
Start from the scratch or jump to the section:
.. contents::
:local:
:backlinks: none
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/giphynav-aiohttp>`_.
What are we going to build?
---------------------------
.. image:: https://media.giphy.com/media/apvx5lPCPsjN6/source.gif
We will build a REST API application that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
Let's call it Giphy Navigator.
How does Giphy Navigator work?
- Client sends a request specifying the search query and the number of results.
- Giphy Navigator returns a response in json format.
- The response contains:
- the search query
- the limit number
- the list of gif urls
Example response:
.. 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"
}
]
}
The task is naive and that's exactly what we need for the tutorial.
Prepare the environment
-----------------------
Let's create the environment for the project.
First we need to create a project folder and the virtual environment:
.. code-block:: bash
mkdir giphynav-aiohttp-tutorial
cd giphynav-aiohttp-tutorial
python3 -m venv venv
Now let's activate the virtual environment:
.. code-block:: bash
. venv/bin/activate
Environment is ready and now we're going to create the layout of the project.
Project layout
--------------
Create next structure in the current directory. All files should be empty. That's ok for now.
Initial project layout::
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Install the requirements
------------------------
Now it's time to install the project requirements. We will use next packages:
- ``dependency-injector`` - the dependency injection framework
- ``aiohttp`` - the web framework
- ``aiohttp-devtools`` - the helper library that will provide a development server with live
reloading
- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files
- ``pytest-aiohttp``- the helper library for the testing of the ``aiohttp`` application
- ``pytest-cov`` - the helper library for measuring the test coverage
Put next lines into the ``requirements.txt`` file:
.. code-block:: bash
dependency-injector
aiohttp
aiohttp-devtools
pyyaml
pytest-aiohttp
pytest-cov
and run next in the terminal:
.. code-block:: bash
pip install -r requirements.txt
Let's also install the ``httpie``. It is a user-friendly command-line HTTP client for the API era.
We will use it for the manual testing.
Run the command in the terminal:
.. code-block:: bash
pip install httpie
The requirements are setup. Now we will build a minimal application.
Minimal application
-------------------
In this section we will build a minimal application. It will have an endpoint that we can call.
The endpoint will answer in the right format and will have no data.
Edit ``views.py``:
.. code-block:: python
"""Views module."""
from aiohttp import web
async def index(request: web.Request) -> web.Response:
query = request.query.get('query', 'Dependency Injector')
limit = int(request.query.get('limit', 10))
gifs = []
return web.json_response(
{
'query': query,
'limit': limit,
'gifs': gifs,
},
)
Now let's create the main part of our application - the container. Container will keep all of the
application components and their dependencies. First two providers we need to add are
the ``aiohttp`` application provider and the view provider.
Put next into the ``containers.py``:
.. code-block:: python
"""Application containers module."""
from dependency_injector import containers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
index_view = aiohttp.View(views.index)
At the last we need to create the ``aiohttp`` application factory. It is traditionally called
``create_app()``. It will create the container. Then it will use the container to create
the ``aiohttp`` application. Last step is to configure the routing - we will assign
``index_view`` from the container to handle the requests to the root ``/`` of our REST API server.
Put next into the ``application.py``:
.. code-block:: python
"""Application module."""
from aiohttp import web
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
app: web.Application = container.app()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
])
return app
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Now we're ready to run our application
Do next in the terminal:
.. code-block:: bash
adev runserver giphynavigator/application.py --livereload
The output should be something like:
.. code-block:: bash
[18:52:59] Starting aux server at http://localhost:8001 ◆
[18:52:59] Starting dev server at http://localhost:8000 ●
Let's use ``httpie`` to check that it works:
.. code-block:: bash
http http://127.0.0.1:8000/
You should see:
.. code-block:: json
HTTP/1.1 200 OK
Content-Length: 844
Content-Type: application/json; charset=utf-8
Date: Wed, 29 Jul 2020 21:01:50 GMT
Server: Python/3.8 aiohttp/3.6.2
{
"gifs": [],
"limit": 10,
"query": "Dependency Injector"
}
Minimal application is ready. Let's connect our application with the Giphy API.
Giphy API client
----------------
In this section we will integrate our application with the Giphy API.
We will create our own API client using ``aiohttp`` client.
Create ``giphy.py`` module in the ``giphynavigator`` package:
.. code-block:: bash
:emphasize-lines: 6
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
└── requirements.txt
and put next into it:
.. code-block:: python
"""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."""
if not query:
return []
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()
Now we need to add ``GiphyClient`` into the container. The ``GiphyClient`` has two dependencies
that have to be injected: the API key and the request timeout. We will need to use two more
providers from the ``dependency_injector.providers`` module:
- ``Factory`` provider that will create the ``GiphyClient`` client.
- ``Configuration`` provider that will provide the API key and the request timeout.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 3,7,15,17-21
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, 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,
)
index_view = aiohttp.View(views.index)
.. note::
We have used the configuration value before it was defined. That's the principle how the
``Configuration`` provider works.
Use first, define later.
Now let's add the configuration file.
We will use YAML.
Create an empty file ``config.yml`` in the root root of the project:
.. code-block:: bash
:emphasize-lines: 9
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
and put next into it:
.. code-block:: yaml
giphy:
request_timeout: 10
We will use an environment variable ``GIPHY_API_KEY`` to provide the API key.
Now we need to edit ``create_app()`` to make two things when application starts:
- Load the configuration file the ``config.yml``.
- Load the API key from the ``GIPHY_API_KEY`` environment variable.
Edit ``application.py``:
.. code-block:: python
:emphasize-lines: 11-12
"""Application module."""
from aiohttp import web
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
app: web.Application = container.app()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
])
return app
Now we need to create an API key and set it to the environment variable.
As for now, dont worry, just take this one:
.. code-block:: bash
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
.. note::
To create your own Giphy API key follow this
`guide <https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key>`_.
The Giphy API client and the configuration setup is done. Let's proceed to the search service.
Search service
--------------
Now it's time to add the ``SearchService``. It will:
- Perform the search.
- Format result data.
``SearchService`` will use ``GiphyClient``.
Create ``services.py`` module in the ``giphynavigator`` package:
.. code-block:: bash
:emphasize-lines: 7
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ └── views.py
├── venv/
└── requirements.txt
and put next into it:
.. code-block:: python
"""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']]
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be injected.
Let's add ``SearchService`` to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 7,23-26
"""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)
The search service is ready. In the next section we're going to make it work.
Make the search work
--------------------
Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
Edit ``views.py``:
.. code-block:: python
:emphasize-lines: 5,8-11,15
"""Views module."""
from aiohttp import web
from .services import SearchService
async def index(
request: web.Request,
search_service: SearchService,
) -> web.Response:
query = request.query.get('query', 'Dependency Injector')
limit = int(request.query.get('limit', 10))
gifs = await search_service.search(query, limit)
return web.json_response(
{
'query': query,
'limit': limit,
'gifs': gifs,
},
)
Now let's inject the ``SearchService`` dependency into the ``index`` view.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 28-31
"""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,
)
Make sure the app is running or use:
.. code-block:: bash
adev runserver giphynavigator/application.py --livereload
and make a request to the API in the terminal:
.. code-block:: bash
http http://localhost:8000/ query=="wow,it works"
You should see:
.. code-block:: json
HTTP/1.1 200 OK
Content-Length: 850
Content-Type: application/json; charset=utf-8
Date: Wed, 29 Jul 2020 22:22:55 GMT
Server: Python/3.8 aiohttp/3.6.2
{
"gifs": [
{
"url": "https://giphy.com/gifs/discoverychannel-nugget-gold-rush-rick-ness-KGGPIlnC4hr4u2s3pY"
},
{
"url": "https://giphy.com/gifs/primevideoin-ll1hyBS2IrUPLE0E71"
},
{
"url": "https://giphy.com/gifs/jackman-works-jackmanworks-l4pTgQoCrmXq8Txlu"
},
{
"url": "https://giphy.com/gifs/cat-massage-at-work-l46CzMaOlJXAFuO3u"
},
{
"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"
}
.. image:: https://media.giphy.com/media/3oxHQCI8tKXoeW4IBq/source.gif
The search works!
Make some refactoring
---------------------
Tests
-----
Conclusion
----------
.. disqus::

View File

@ -3,7 +3,8 @@
Flask tutorial
==============
This tutorials shows how to build ``Flask`` application following dependency injection principle.
This tutorials shows how to build ``Flask`` application following the dependency injection
principle.
Start from the scratch or jump to the section:
@ -627,11 +628,11 @@ Github API client setup is done.
Search service
--------------
Now it's time to add ``SearchService``. It will:
Now it's time to add the ``SearchService``. It will:
- Perform the search.
- Fetch commit extra data for each result.
- Format result data
- Format result data.
``SearchService`` will use ``Github`` API client.
@ -827,8 +828,8 @@ Make some refactoring
Our ``index`` view has two hardcoded config values:
- Default search term
- Limit of the results
- Default search query
- Default results limit
Let's make some refactoring. We will move these values to the config.
@ -1075,7 +1076,7 @@ You should see:
.. note::
Take a look on the highlights in the ``tests.py``.
Take a look at the highlights in the ``tests.py``.
It emphasizes the overriding of the ``Github`` API client.
@ -1084,7 +1085,7 @@ Conclusion
We are done.
It this tutorial we've build ``Flask`` application following dependency injection principle.
In this tutorial we've build ``Flask`` application following the dependency injection principle.
We've used ``Dependency Injector`` as a dependency injection framework.
The main part of this application is the container. It keeps all the application components and

View File

@ -2,8 +2,8 @@
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services, views

View File

@ -22,8 +22,8 @@ async def test_index(client, app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = {
'data': [
{'url': 'https://giphy/gif1.gif'},
{'url': 'https://giphy/gif2.gif'},
{'url': 'https://giphy.com/gif1.gif'},
{'url': 'https://giphy.com/gif2.gif'},
],
}
@ -42,8 +42,8 @@ async def test_index(client, app):
'query': 'test',
'limit': 10,
'gifs': [
{'url': 'https://giphy/gif1.gif'},
{'url': 'https://giphy/gif2.gif'},
{'url': 'https://giphy.com/gif1.gif'},
{'url': 'https://giphy.com/gif2.gif'},
],
}