ich bin der Schöpfer von Dependency Injector . Dies ist ein Abhängigkeitsinjektionsframework für Python.
Fortsetzung einer Reihe von Tutorials zur Verwendung des Abhängigkeitsinjektors zum Erstellen von Anwendungen.
In diesem Tutorial möchte ich zeigen, wie der Abhängigkeitsinjektor für die
aiohttpAnwendungsentwicklung verwendet wird .
Das Handbuch besteht aus folgenden Teilen:
- Was werden wir bauen?
- Vorbereitung der Umwelt
- Projektstruktur
- Abhängigkeiten installieren
- Minimale Anwendung
- Giphy API-Client
- Suchdienst
- Suche verbinden
- Ein bisschen Refactoring
- Tests hinzufügen
- Fazit
Das abgeschlossene Projekt finden Sie auf Github .
Um zu beginnen, müssen Sie haben:
- Python 3.5+
- Virtuelle Umgebung
Und es ist wünschenswert zu haben:
- Erste Entwicklungsfähigkeiten mit aiohttp
- Das Prinzip der Abhängigkeitsinjektion verstehen
Was werden wir bauen?
Wir werden eine REST-API-Anwendung erstellen, die auf Giphy nach lustigen Gifs sucht . Nennen wir es Giphy Navigator.
Wie funktioniert Giphy Navigator?
- Der Client sendet eine Anfrage, in der angegeben wird, wonach gesucht und wie viele Ergebnisse zurückgegeben werden sollen.
- Giphy Navigator gibt eine JSON-Antwort zurück.
- Die Antwort beinhaltet:
- Suchanfrage
- Anzahl der Ergebnisse
- GIF-URL-Liste
Beispielantwort:
{
"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"
}
]
}
Bereiten Sie die Umgebung vor
Beginnen wir mit der Vorbereitung der Umgebung.
Zunächst müssen wir einen Projektordner und eine virtuelle Umgebung erstellen:
mkdir giphynav-aiohttp-tutorial
cd giphynav-aiohttp-tutorial
python3 -m venv venv
Jetzt aktivieren wir die virtuelle Umgebung:
. venv/bin/activate
Die Umgebung ist fertig, jetzt beginnen wir mit der Projektstruktur.
Projektstruktur
In diesem Abschnitt organisieren wir die Struktur des Projekts.
Lassen Sie uns die folgende Struktur im aktuellen Ordner erstellen. Lassen Sie vorerst alle Dateien leer.
Anfangsstruktur:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Abhängigkeiten installieren
Es ist Zeit, die Abhängigkeiten zu installieren. Wir werden Pakete wie dieses verwenden:
dependency-injector- Abhängigkeitsinjektions-Frameworkaiohttp- Web-Frameworkaiohttp-devtools- Eine Hilfsbibliothek, die einen Server für die Live-Neustartentwicklung bereitstelltpyyaml- Bibliothek zum Parsen von YAML-Dateien, die zum Lesen der Konfiguration verwendet wirdpytest-aiohttp- Hilfsbibliothek zum Testen vonaiohttpAnwendungenpytest-cov- Hilfsbibliothek zur Messung der Codeabdeckung durch Tests
Fügen wir der Datei die folgenden Zeilen hinzu
requirements.txt:
dependency-injector
aiohttp
aiohttp-devtools
pyyaml
pytest-aiohttp
pytest-cov
Und im Terminal ausführen:
pip install -r requirements.txt
Zusätzlich installieren
httpie. Es ist ein Befehlszeilen-HTTP-Client. Wir werden
es verwenden, um die API manuell zu testen.
Lassen Sie uns im Terminal ausführen:
pip install httpie
Die Abhängigkeiten werden installiert. Jetzt erstellen wir eine minimale Anwendung.
Minimale Anwendung
In diesem Abschnitt erstellen wir eine minimale Anwendung. Es wird einen Endpunkt haben, der eine leere Antwort zurückgibt.
Lassen Sie uns bearbeiten
views.py:
"""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,
},
)
Fügen wir nun einen Container für Abhängigkeiten hinzu (im Folgenden nur einen Container). Der Container enthält alle Komponenten der Anwendung. Fügen wir die ersten beiden Komponenten hinzu. Dies ist eine
aiohttpAnwendung und Präsentation index.
Lassen Sie uns bearbeiten
containers.py:
"""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)
Jetzt müssen wir eine
aiohttpAnwendungsfactory erstellen . Es wird normalerweise genannt
create_app(). Es wird ein Container erstellt. Der Container wird zum Erstellen der aiohttpAnwendung verwendet. Der letzte Schritt besteht darin, das Routing einzurichten. Wir weisen index_viewdem Container eine Ansicht zu, um Anforderungen an das Stammverzeichnis "/"unserer Anwendung zu verarbeiten.
Lassen Sie uns bearbeiten
application.py:
"""Application module."""
from aiohttp import web
from .containers import ApplicationContainer
def create_app():
"""Create and return aiohttp application."""
container = ApplicationContainer()
app: web.Application = container.app()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
])
return app
Der Container ist das erste Objekt in der Anwendung. Es wird verwendet, um alle anderen Objekte abzurufen.
Jetzt können wir unsere Anwendung starten:
Führen Sie den Befehl im Terminal aus:
adev runserver giphynavigator/application.py --livereload
Die Ausgabe sollte folgendermaßen aussehen:
[18:52:59] Starting aux server at http://localhost:8001 ◆
[18:52:59] Starting dev server at http://localhost:8000 ●
Wir verwenden
httpie, um den Serverbetrieb zu überprüfen:
http http://127.0.0.1:8000/
Du wirst sehen:
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"
}
Die minimale Anwendung ist fertig. Verbinden wir die Giphy-API.
Giphy API-Client
In diesem Abschnitt werden wir unsere Anwendung in die Giphy-API integrieren. Wir werden unseren eigenen API-Client auf der Client-Seite erstellen
aiohttp.
Erstellen Sie eine leere Datei
giphy.pyim Paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
└── requirements.txt
und fügen Sie die folgenden Zeilen hinzu:
"""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()
Jetzt müssen wir den GiphyClient zum Container hinzufügen. GiphyClient hat zwei Abhängigkeiten, die beim Erstellen übergeben werden müssen: API-Schlüssel und Anforderungszeitlimit. Dazu müssen wir zwei neue Anbieter aus dem Modul verwenden
dependency_injector.providers:
- Der Anbieter
Factoryerstellt den GiphyClient. - Der Anbieter
Configurationsendet den API-Schlüssel und das Timeout an den GiphyClient.
Lassen Sie uns bearbeiten
containers.py:
"""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)
Wir haben die Konfigurationsparameter verwendet, bevor wir ihre Werte eingestellt haben. Dies ist das Prinzip, nach dem der Anbieter arbeitetConfiguration.
Zuerst verwenden wir, dann setzen wir die Werte.
Fügen wir nun die Konfigurationsdatei hinzu.
Wir werden YAML verwenden.
Erstellen Sie eine leere Datei
config.ymlim Stammverzeichnis des Projekts:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
Und füllen Sie es mit den folgenden Zeilen aus:
giphy:
request_timeout: 10
Wir werden eine Umgebungsvariable verwenden, um den API-Schlüssel zu übergeben
GIPHY_API_KEY .
Jetzt müssen wir bearbeiten
create_app(), um 2 Aktionen auszuführen, wenn die Anwendung gestartet wird:
- Konfiguration laden von
config.yml - Laden Sie den API-Schlüssel aus der Umgebungsvariablen
GIPHY_API_KEY
Bearbeiten
application.py:
"""Application module."""
from aiohttp import web
from .containers import ApplicationContainer
def create_app():
"""Create and return aiohttp 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
Jetzt müssen wir einen API-Schlüssel erstellen und ihn auf eine Umgebungsvariable setzen.
Verwenden Sie jetzt diesen Schlüssel, um keine Zeit damit zu verschwenden:
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
Befolgen Sie dieses Tutorial , um Ihren eigenen Giphy-API-Schlüssel zu erstellen .
Die Erstellung und Konfiguration der Giphy API-Clients ist abgeschlossen. Fahren wir mit dem Suchdienst fort.
Suchdienst
Es ist Zeit, einen Suchdienst hinzuzufügen
SearchService. Er wird:
- Suche
- Formatieren Sie die empfangene Antwort
SearchServicewird verwenden GiphyClient.
Erstellen Sie eine leere Datei
services.pyim Paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ └── views.py
├── venv/
└── requirements.txt
und fügen Sie die folgenden Zeilen hinzu:
"""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']]
Beim Erstellen müssen
SearchServiceSie übertragen GiphyClient. Wir werden dies angeben, wenn wir es SearchServicedem Container hinzufügen .
Lassen Sie uns bearbeiten
containers.py:
"""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)
Der Suchdienst ist jetzt
SearchServiceabgeschlossen. Im nächsten Abschnitt verbinden wir es mit unserer Ansicht.
Suche verbinden
Wir sind jetzt bereit für die Suche. Lassen Sie uns
SearchServicein indexSicht verwenden.
Bearbeiten
views.py:
"""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,
},
)
Ändern wir nun den Container, um die Abhängigkeit beim Aufruf
SearchServicean die Ansicht zu übergeben index.
Bearbeiten
containers.py:
"""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,
)
Stellen Sie sicher, dass die Anwendung ausgeführt wird oder ausgeführt wird:
adev runserver giphynavigator/application.py --livereload
und stellen Sie eine Anfrage an die API im Terminal:
http http://localhost:8000/ query=="wow,it works" limit==5
Du wirst sehen:
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"
},
],
"limit": 10,
"query": "wow,it works"
}
Die Suche funktioniert.
Ein bisschen Refactoring
Unsere Ansicht
indexenthält zwei fest codierte Werte:
- Standard-Suchbegriff
- Begrenzen Sie die Anzahl der Ergebnisse
Lassen Sie uns ein wenig umgestalten. Wir werden diese Werte in die Konfiguration übertragen.
Bearbeiten
views.py:
"""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,
},
)
Jetzt müssen diese Werte auf Abruf übergeben werden. Lassen Sie uns den Container aktualisieren.
Bearbeiten
containers.py:
"""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,
)
Jetzt aktualisieren wir die Konfigurationsdatei.
Bearbeiten
config.yml:
giphy:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
Das Refactoring ist abgeschlossen. Wir haben unsere Anwendung sauberer gemacht, indem wir fest codierte Werte in die Konfiguration verschoben haben.
Im nächsten Abschnitt werden wir einige Tests hinzufügen.
Tests hinzufügen
Es wäre schön, einige Tests hinzuzufügen. Machen wir das. Wir werden Pytest und Coverage verwenden .
Erstellen Sie eine leere Datei
tests.pyim Paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
└── requirements.txt
und fügen Sie die folgenden Zeilen hinzu:
"""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()
Beginnen wir nun mit dem Testen und überprüfen Sie die Abdeckung:
py.test giphynavigator/tests.py --cov=giphynavigator
Du wirst sehen:
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%
Beachten Sie, wie wirgiphy_clientmit der Methode durch Mock ersetzen.override(). Auf diese Weise können Sie den Rückgabewert eines beliebigen Anbieters überschreiben.
Die Arbeit ist erledigt. Lassen Sie uns nun zusammenfassen.
Fazit
Wir haben eine
aiohttpREST-API-Anwendung nach dem Prinzip der Abhängigkeitsinjektion erstellt. Wir haben Dependency Injector als Abhängigkeitsinjektions-Framework verwendet.
Der Vorteil, den Sie mit Dependency Injector erhalten, ist der Container.
Der Container beginnt sich auszuzahlen, wenn Sie die Struktur Ihrer Anwendung verstehen oder ändern müssen. Mit einem Container ist dies einfach, da sich alle Komponenten der Anwendung und ihre Abhängigkeiten an einem Ort befinden:
"""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,
)
Ein Container als Karte Ihrer Anwendung. Sie wissen immer, was von was abhängt.
Was weiter?
- Erfahren Sie mehr über Dependency Injector auf GitHub
- Lesen Sie die Dokumentation unter Lesen Sie die Dokumente
- Haben Sie eine Frage oder finden Sie einen Fehler? Öffnen Sie eine Ausgabe auf Github