Aiohttp + Dependency Injector - tutorial injeksi ketergantungan

Hai,



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:



  1. Apa yang akan kita bangun?
  2. Mempersiapkan lingkungan
  3. Struktur proyek
  4. Menginstal dependensi
  5. Aplikasi minimal
  6. Klien Giphy API
  7. Layanan pencarian
  8. Hubungkan pencarian
  9. Sedikit refactoring
  10. Menambahkan tes
  11. 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 ketergantungan
  • aiohttp - kerangka web
  • aiohttp-devtools - perpustakaan pembantu yang menyediakan server untuk pengembangan boot ulang langsung
  • pyyaml - perpustakaan untuk mem-parsing file YAML, digunakan untuk membaca konfigurasi
  • pytest-aiohttp- perpustakaan pembantu untuk menguji aiohttpaplikasi
  • pytest-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 penyedia Configuration.



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 mengganti giphy_client dengan 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?






All Articles