saya pencipta Dependency Injector . Ini adalah kerangka kerja injeksi ketergantungan untuk Python.
Melanjutkan rangkaian tutorial tentang penggunaan Dependency Injector untuk membangun aplikasi.
Dalam tutorial ini saya ingin menunjukkan kepada Anda bagaimana menggunakan Dependency Injector untuk pengembangan
aiohttpaplikasi.
Manual terdiri dari bagian-bagian berikut:
- Apa yang akan kita bangun?
- Mempersiapkan lingkungan
- Struktur proyek
- Menginstal dependensi
- Aplikasi minimal
- Klien Giphy API
- Layanan pencarian
- Hubungkan pencarian
- Sedikit refactoring
- Menambahkan tes
- Kesimpulan
Proyek yang telah selesai dapat ditemukan di Github .
Untuk memulai, Anda harus memiliki:
- Python 3.5+
- Lingkungan virtual
Dan diinginkan untuk memiliki:
- Keterampilan pengembangan awal dengan aiohttp
- Memahami prinsip injeksi ketergantungan
Apa yang akan kita bangun?
Kami akan membangun aplikasi REST API yang mencari gif lucu di Giphy . Sebut saja Giphy Navigator.
Bagaimana cara kerja Giphy Navigator?
- Klien mengirimkan permintaan yang menunjukkan apa yang harus dicari dan berapa banyak hasil yang akan dikembalikan.
- Giphy Navigator mengembalikan respons json.
- Jawabannya meliputi:
- permintaan pencarian
- jumlah hasil
- Daftar url GIF
Tanggapan sampel:
{
"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"
}
]
}
Persiapkan lingkungan
Mari kita mulai dengan mempersiapkan lingkungan.
Pertama-tama, kita perlu membuat folder proyek dan lingkungan virtual:
mkdir giphynav-aiohttp-tutorial
cd giphynav-aiohttp-tutorial
python3 -m venv venv
Sekarang mari kita aktifkan lingkungan virtual:
. venv/bin/activate
Lingkungan sudah siap, sekarang mari kita mulai dengan struktur proyek.
Struktur proyek
Pada bagian ini, kami akan mengatur struktur proyek.
Mari buat struktur berikut di folder saat ini. Biarkan semua file kosong untuk saat ini.
Struktur awal:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Menginstal dependensi
Saatnya menginstal dependensi. Kami akan menggunakan paket seperti ini:
dependency-injector- kerangka injeksi ketergantunganaiohttp- kerangka webaiohttp-devtools- perpustakaan pembantu yang menyediakan server untuk pengembangan boot ulang langsungpyyaml- perpustakaan untuk mem-parsing file YAML, digunakan untuk membaca konfigurasipytest-aiohttp- perpustakaan pembantu untuk mengujiaiohttpaplikasipytest-cov- Perpustakaan pembantu untuk mengukur cakupan kode dengan tes
Mari tambahkan baris berikut ke file
requirements.txt:
dependency-injector
aiohttp
aiohttp-devtools
pyyaml
pytest-aiohttp
pytest-cov
Dan jalankan di terminal:
pip install -r requirements.txt
Instal sebagai tambahan
httpie. Ini adalah klien HTTP baris perintah. Kami akan
menggunakannya untuk menguji API secara manual.
Mari kita jalankan di terminal:
pip install httpie
Dependensi diinstal. Sekarang mari buat aplikasi minimal.
Aplikasi minimal
Di bagian ini, kami akan membangun aplikasi minimal. Ini akan memiliki titik akhir yang akan mengembalikan respons kosong.
Mari edit
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,
},
)
Sekarang mari tambahkan wadah dependensi (selanjutnya hanya wadah). Penampung akan berisi semua komponen aplikasi. Mari tambahkan dua komponen pertama. Ini adalah
aiohttpaplikasi dan presentasi index.
Mari edit
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)
Sekarang kita perlu membuat pabrik
aiohttpaplikasi. Biasanya disebut
create_app(). Ini akan membuat wadah. Penampung akan digunakan untuk membuat aiohttpaplikasi. Langkah terakhir adalah menyiapkan perutean - kami akan menetapkan tampilan index_viewdari kontainer untuk menangani permintaan ke root "/"aplikasi kami.
Mari edit
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
Kontainer adalah objek pertama dalam aplikasi. Ini digunakan untuk mendapatkan semua objek lainnya.
Sekarang kita siap meluncurkan aplikasi kita:
Jalankan perintah di terminal:
adev runserver giphynavigator/application.py --livereload
Outputnya akan terlihat seperti ini:
[18:52:59] Starting aux server at http://localhost:8001 ◆
[18:52:59] Starting dev server at http://localhost:8000 ●
Kami menggunakan
httpieuntuk memeriksa operasi server:
http http://127.0.0.1:8000/
Kamu akan lihat:
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"
}
Aplikasi minimal sudah siap. Mari hubungkan API Giphy.
Klien Giphy API
Di bagian ini, kami akan mengintegrasikan aplikasi kami dengan Giphy API. Kami akan membuat klien API kami sendiri menggunakan sisi klien
aiohttp.
Buat file kosong
giphy.pydalam paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
└── requirements.txt
dan tambahkan baris berikut ke dalamnya:
"""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()
Sekarang kita perlu menambahkan GiphyClient ke wadah. GiphyClient memiliki dua dependensi yang harus diteruskan saat dibuat: kunci API dan waktu tunggu permintaan. Untuk melakukan ini, kita perlu menggunakan dua penyedia baru dari modul
dependency_injector.providers:
- Penyedia
Factoryakan membuat GiphyClient. - Penyedia
Configurationakan mengirimkan kunci API dan waktu tunggu ke GiphyClient.
Mari edit
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)
Kami menggunakan parameter konfigurasi sebelum menetapkan nilainya. Ini adalah prinsip yang digunakan oleh penyediaConfiguration.
Pertama kita gunakan, lalu kita atur nilainya.
Sekarang mari tambahkan file konfigurasi.
Kami akan menggunakan YAML.
Buat file kosong
config.ymldi root proyek:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
Dan isi dengan baris berikut:
giphy:
request_timeout: 10
Kami akan menggunakan variabel lingkungan untuk meneruskan kunci API
GIPHY_API_KEY .
Sekarang kita perlu mengedit
create_app()untuk melakukan 2 tindakan saat aplikasi dimulai:
- Muat konfigurasi dari
config.yml - Muat kunci API dari variabel lingkungan
GIPHY_API_KEY
Edit
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
Sekarang kita perlu membuat kunci API dan mengaturnya ke variabel lingkungan.
Agar tidak membuang waktu untuk ini, sekarang gunakan kunci ini:
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
Ikuti tutorial ini untuk membuat kunci API Giphy Anda sendiri .
Pembuatan klien Giphy API dan penginstalan konfigurasi selesai. Mari beralih ke layanan pencarian.
Layanan pencarian
Saatnya menambahkan layanan pencarian
SearchService. Dia akan:
- Cari
- Format menerima tanggapan
SearchServiceakan digunakan GiphyClient.
Buat file kosong
services.pydalam paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ └── views.py
├── venv/
└── requirements.txt
dan tambahkan baris berikut ke dalamnya:
"""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']]
Saat membuat,
SearchServiceAnda perlu mentransfer GiphyClient. Kami akan menunjukkan ini saat kami menambahkannya SearchServiceke penampung.
Mari edit
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)
Layanan pencarian sekarang
SearchServiceselesai. Di bagian selanjutnya, kami akan menghubungkannya ke tampilan kami.
Hubungkan pencarian
Kami sekarang siap untuk pencarian bekerja. Mari gunakan
SearchServicedalam indextampilan.
Edit
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,
},
)
Sekarang mari kita ubah wadah untuk meneruskan ketergantungan
SearchServiceke tampilan indexsaat dipanggil.
Edit
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,
)
Pastikan aplikasi sedang berjalan atau dijalankan:
adev runserver giphynavigator/application.py --livereload
dan buat permintaan ke API di terminal:
http http://localhost:8000/ query=="wow,it works" limit==5
Kamu akan lihat:
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"
}
Pencarian berhasil.
Sedikit refactoring
Tampilan kami
indexberisi dua nilai hardcode:
- Istilah pencarian default
- Batasi jumlah hasil
Mari kita lakukan sedikit refactoring. Kami akan mentransfer nilai-nilai ini ke dalam konfigurasi.
Edit
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,
},
)
Sekarang kita membutuhkan nilai-nilai ini untuk diteruskan saat dipanggil. Mari perbarui wadahnya.
Edit
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,
)
Sekarang mari perbarui file konfigurasi.
Edit
config.yml:
giphy:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
Refactoring selesai. Kami telah membuat aplikasi kami lebih bersih dengan memindahkan nilai hardcode ke dalam konfigurasi.
Di bagian selanjutnya, kami akan menambahkan beberapa tes.
Menambahkan tes
Akan menyenangkan menambahkan beberapa tes. Ayo lakukan. Kami akan menggunakan pytest dan cakupan .
Buat file kosong
tests.pydalam paket giphynavigator:
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
└── requirements.txt
dan tambahkan baris berikut ke dalamnya:
"""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()
Sekarang mari kita mulai menguji dan memeriksa cakupan:
py.test giphynavigator/tests.py --cov=giphynavigator
Kamu akan lihat:
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%
Perhatikan bagaimana kami menggantigiphy_clientdengan mock menggunakan metode ini.override(). Dengan cara ini, Anda bisa mengganti nilai kembalian dari penyedia mana pun.
Pekerjaan sudah selesai. Sekarang mari kita rangkum.
Kesimpulan
Kami telah membangun
aiohttpaplikasi REST API menggunakan prinsip injeksi ketergantungan. Kami menggunakan Dependency Injector sebagai kerangka kerja injeksi ketergantungan.
Keuntungan yang Anda dapatkan dengan Dependency Injector adalah wadah.
Penampung mulai terbayar saat Anda perlu memahami atau mengubah struktur aplikasi Anda. Dengan container, mudah karena semua komponen aplikasi dan dependensinya berada di satu tempat:
"""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,
)
Sebuah wadah sebagai peta aplikasi Anda. Anda selalu tahu apa yang tergantung pada apa.
Apa berikutnya?
- Pelajari lebih lanjut tentang Dependency Injector di GitHub
- Lihat dokumentasinya di Read the Docs
- Punya pertanyaan atau temukan bug? Buka masalah di Github