Add FastAPI mini app

This commit is contained in:
Roman Mogylatov 2020-11-12 17:48:07 -05:00
parent af742b980a
commit 59f25241cc
10 changed files with 317 additions and 0 deletions

View File

@ -0,0 +1,121 @@
FastAPI + Dependency Injector Example
=====================================
This is an `FastAPI <https://fastapi.tiangolo.com/>`_ +
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application.
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
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
uvicorn giphynavigator.application:app --reload
The output should be something like:
.. code-block::
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [4795] using watchgod
INFO: Started server process [4797]
INFO: Waiting for application startup.
INFO: Application startup complete.
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 <https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key>`_.
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, 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/application.py 13 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/endpoints.py 5 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 38 0 100%
---------------------------------------------------
TOTAL 85 10 88%

View File

@ -0,0 +1,5 @@
giphy:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -0,0 +1 @@
"""Top-level package."""

View File

@ -0,0 +1,21 @@
"""Application module."""
from fastapi import FastAPI
from .containers import Container
from . import endpoints
def create_app() -> FastAPI:
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
container.wire(modules=[endpoints])
app = FastAPI()
app.container = container
app.add_api_route('/', endpoints.index)
return app
app = create_app()

View File

@ -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,
)

View File

@ -0,0 +1,18 @@
"""Endpoints module."""
from dependency_injector.wiring import Provide
from .containers import Container
async def index(
query: str = Provide[Container.config.default.query],
limit: int = Provide[Container.config.default.limit.as_int()],
search_service = Provide[Container.search_service],
):
gifs = await search_service.search(query, limit)
return {
'query': query,
'limit': limit,
'gifs': gifs,
}

View File

@ -0,0 +1,26 @@
"""Giphy client module."""
from aiohttp import ClientSession, ClientTimeout
class GiphyClient:
API_URL = 'https://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()

View File

@ -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']]

View File

@ -0,0 +1,78 @@
"""Tests module."""
from unittest import mock
import pytest
from httpx import AsyncClient
from giphynavigator.application import app
from giphynavigator.giphy import GiphyClient
@pytest.fixture
def client(event_loop):
client = AsyncClient(app=app, base_url='http://test')
yield client
event_loop.run_until_complete(client.aclose())
@pytest.mark.asyncio
async def test_index(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.container.giphy_client.override(giphy_client_mock):
response = await 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'},
],
}
@pytest.mark.asyncio
async def test_index_no_data(client):
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_code == 200
data = response.json()
assert data['gifs'] == []
@pytest.mark.asyncio
async def test_index_default_params(client):
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_code == 200
data = response.json()
assert data['query'] == app.container.config.default.query()
assert data['limit'] == app.container.config.default.limit()

View File

@ -0,0 +1,8 @@
dependency-injector
fastapi
uvicorn
aiohttp
httpx
pyyaml
pytest-asyncio
pytest-cov