mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-29 04:53:59 +03:00
Merge branch 'release/4.3.9' into master
This commit is contained in:
commit
b0b8820ac1
|
@ -68,7 +68,7 @@ Key features of the ``Dependency Injector``:
|
|||
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
|
||||
See `Resource provider <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc.
|
||||
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc.
|
||||
See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly.
|
||||
See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
|
||||
|
|
79
docs/examples/fastapi.rst
Normal file
79
docs/examples/fastapi.rst
Normal file
|
@ -0,0 +1,79 @@
|
|||
.. _fastapi-example:
|
||||
|
||||
FastAPI example
|
||||
===============
|
||||
|
||||
.. meta::
|
||||
:keywords: Python,Dependency Injection,FastAPI,Example
|
||||
:description: This example demonstrates a usage of the FastAPI and Dependency Injector.
|
||||
|
||||
|
||||
This example shows how to use ``Dependency Injector`` with `FastAPI <https://fastapi.tiangolo.com/>`_.
|
||||
|
||||
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
|
||||
|
||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi>`_.
|
||||
|
||||
Application structure
|
||||
---------------------
|
||||
|
||||
Application has next structure:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./
|
||||
├── giphynavigator/
|
||||
│ ├── __init__.py
|
||||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── endpoints.py
|
||||
│ ├── giphy.py
|
||||
│ ├── services.py
|
||||
│ └── tests.py
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
Container
|
||||
---------
|
||||
|
||||
Declarative container is defined in ``giphynavigator/containers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/containers.py
|
||||
:language: python
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
Endpoint has a dependency on search service. There are also some config options that are used as default values.
|
||||
The dependencies are injected using :ref:`wiring` feature.
|
||||
|
||||
Listing of ``giphynavigator/endpoints.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/endpoints.py
|
||||
:language: python
|
||||
|
||||
Application factory
|
||||
-------------------
|
||||
Application factory creates container, wires it with the ``endpoints`` module, creates
|
||||
``FastAPI`` app, and setup routes.
|
||||
|
||||
Listing of ``giphynavigator/application.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/application.py
|
||||
:language: python
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests use :ref:`provider-overriding` feature to replace giphy client with a mock ``giphynavigator/tests.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 29,57,72
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi>`_.
|
||||
|
||||
.. disqus::
|
|
@ -17,5 +17,6 @@ Explore the examples to see the ``Dependency Injector`` in action.
|
|||
flask
|
||||
aiohttp
|
||||
sanic
|
||||
fastapi
|
||||
|
||||
.. disqus::
|
||||
|
|
|
@ -77,7 +77,7 @@ Key features of the ``Dependency Injector``:
|
|||
See :ref:`resource-provider`.
|
||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
|
||||
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||
|
|
|
@ -284,6 +284,7 @@ Choose one of the following as a next step:
|
|||
- :ref:`flask-example`
|
||||
- :ref:`aiohttp-example`
|
||||
- :ref:`sanic-example`
|
||||
- :ref:`fastapi-example`
|
||||
- Pass the tutorials:
|
||||
- :ref:`flask-tutorial`
|
||||
- :ref:`aiohttp-tutorial`
|
||||
|
|
|
@ -23,7 +23,7 @@ Key features of the ``Dependency Injector``:
|
|||
See :ref:`resource-provider`.
|
||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
|
||||
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||
|
|
|
@ -7,6 +7,10 @@ that were made in every particular version.
|
|||
From version 0.7.6 *Dependency Injector* framework strictly
|
||||
follows `Semantic versioning`_
|
||||
|
||||
4.3.9
|
||||
-----
|
||||
- Add ``FastAPI`` example.
|
||||
|
||||
4.3.8
|
||||
-----
|
||||
- Add a hotfix to support wiring for ``FastAPI`` endpoints.
|
||||
|
|
|
@ -201,5 +201,6 @@ Take a look at other application examples:
|
|||
- :ref:`flask-example`
|
||||
- :ref:`aiohttp-example`
|
||||
- :ref:`sanic-example`
|
||||
- :ref:`fastapi-example`
|
||||
|
||||
.. disqus::
|
||||
|
|
121
examples/miniapps/fastapi/README.rst
Normal file
121
examples/miniapps/fastapi/README.rst
Normal 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%
|
5
examples/miniapps/fastapi/config.yml
Normal file
5
examples/miniapps/fastapi/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
giphy:
|
||||
request_timeout: 10
|
||||
default:
|
||||
query: "Dependency Injector"
|
||||
limit: 10
|
1
examples/miniapps/fastapi/giphynavigator/__init__.py
Normal file
1
examples/miniapps/fastapi/giphynavigator/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Top-level package."""
|
21
examples/miniapps/fastapi/giphynavigator/application.py
Normal file
21
examples/miniapps/fastapi/giphynavigator/application.py
Normal 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()
|
21
examples/miniapps/fastapi/giphynavigator/containers.py
Normal file
21
examples/miniapps/fastapi/giphynavigator/containers.py
Normal 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,
|
||||
)
|
18
examples/miniapps/fastapi/giphynavigator/endpoints.py
Normal file
18
examples/miniapps/fastapi/giphynavigator/endpoints.py
Normal 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,
|
||||
}
|
26
examples/miniapps/fastapi/giphynavigator/giphy.py
Normal file
26
examples/miniapps/fastapi/giphynavigator/giphy.py
Normal 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()
|
18
examples/miniapps/fastapi/giphynavigator/services.py
Normal file
18
examples/miniapps/fastapi/giphynavigator/services.py
Normal 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']]
|
78
examples/miniapps/fastapi/giphynavigator/tests.py
Normal file
78
examples/miniapps/fastapi/giphynavigator/tests.py
Normal 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()
|
8
examples/miniapps/fastapi/requirements.txt
Normal file
8
examples/miniapps/fastapi/requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
dependency-injector
|
||||
fastapi
|
||||
uvicorn
|
||||
aiohttp
|
||||
httpx
|
||||
pyyaml
|
||||
pytest-asyncio
|
||||
pytest-cov
|
|
@ -1,6 +1,6 @@
|
|||
"""Top-level package."""
|
||||
|
||||
__version__ = '4.3.8'
|
||||
__version__ = '4.3.9'
|
||||
"""Version number.
|
||||
|
||||
:type: str
|
||||
|
|
Loading…
Reference in New Issue
Block a user