diff --git a/examples/miniapps/fastapi/README.rst b/examples/miniapps/fastapi/README.rst new file mode 100644 index 00000000..7e99ab8b --- /dev/null +++ b/examples/miniapps/fastapi/README.rst @@ -0,0 +1,121 @@ +FastAPI + Dependency Injector Example +===================================== + +This is an `FastAPI `_ + +`Dependency Injector `_ example application. + +The example application is a REST API that searches for funny GIFs on the `Giphy `_. + +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 `_. + +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% diff --git a/examples/miniapps/fastapi/config.yml b/examples/miniapps/fastapi/config.yml new file mode 100644 index 00000000..d1276f8c --- /dev/null +++ b/examples/miniapps/fastapi/config.yml @@ -0,0 +1,5 @@ +giphy: + request_timeout: 10 +default: + query: "Dependency Injector" + limit: 10 diff --git a/examples/miniapps/fastapi/giphynavigator/__init__.py b/examples/miniapps/fastapi/giphynavigator/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/fastapi/giphynavigator/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/fastapi/giphynavigator/application.py b/examples/miniapps/fastapi/giphynavigator/application.py new file mode 100644 index 00000000..805f4868 --- /dev/null +++ b/examples/miniapps/fastapi/giphynavigator/application.py @@ -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() diff --git a/examples/miniapps/fastapi/giphynavigator/containers.py b/examples/miniapps/fastapi/giphynavigator/containers.py new file mode 100644 index 00000000..730c162e --- /dev/null +++ b/examples/miniapps/fastapi/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/fastapi/giphynavigator/endpoints.py b/examples/miniapps/fastapi/giphynavigator/endpoints.py new file mode 100644 index 00000000..e1a37716 --- /dev/null +++ b/examples/miniapps/fastapi/giphynavigator/endpoints.py @@ -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, + } diff --git a/examples/miniapps/fastapi/giphynavigator/giphy.py b/examples/miniapps/fastapi/giphynavigator/giphy.py new file mode 100644 index 00000000..22a5f6a4 --- /dev/null +++ b/examples/miniapps/fastapi/giphynavigator/giphy.py @@ -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() diff --git a/examples/miniapps/fastapi/giphynavigator/services.py b/examples/miniapps/fastapi/giphynavigator/services.py new file mode 100644 index 00000000..1c86e0d7 --- /dev/null +++ b/examples/miniapps/fastapi/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/fastapi/giphynavigator/tests.py b/examples/miniapps/fastapi/giphynavigator/tests.py new file mode 100644 index 00000000..4ae91493 --- /dev/null +++ b/examples/miniapps/fastapi/giphynavigator/tests.py @@ -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() diff --git a/examples/miniapps/fastapi/requirements.txt b/examples/miniapps/fastapi/requirements.txt new file mode 100644 index 00000000..6eddac10 --- /dev/null +++ b/examples/miniapps/fastapi/requirements.txt @@ -0,0 +1,8 @@ +dependency-injector +fastapi +uvicorn +aiohttp +httpx +pyyaml +pytest-asyncio +pytest-cov