Flask + Dependency Injector - panduan injeksi ketergantungan

Hai,



saya pencipta Dependency Injector . Ini adalah kerangka kerja injeksi ketergantungan untuk Python.



Dalam tutorial ini, saya ingin menunjukkan bagaimana menggunakan Dependency Injector untuk mengembangkan aplikasi Flask.



Manual terdiri dari bagian-bagian berikut:



  1. Apa yang akan kita bangun?
  2. Persiapkan lingkungan
  3. Struktur proyek
  4. Halo Dunia!
  5. Termasuk gaya
  6. Menghubungkan Github
  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 Flask
  • Memahami prinsip injeksi ketergantungan


Apa yang akan kita bangun?



Kami akan membangun aplikasi yang membantu Anda mencari repositori di Github. Sebut saja Github Navigator.



Bagaimana cara kerja Github Navigator?



  • Pengguna membuka halaman web di mana dia diminta untuk memasukkan permintaan pencarian.
  • Pengguna memasukkan kueri dan menekan Enter.
  • Github Navigator mencari repositori yang cocok di Github.
  • Saat pencarian selesai, Github Navigator menunjukkan kepada pengguna halaman web dengan hasilnya.
  • Halaman hasil menunjukkan semua repositori yang ditemukan dan permintaan pencarian.
  • Untuk setiap repositori, pengguna melihat:

    • nama repositori
    • pemilik repositori
    • komit terakhir ke repositori
  • Pengguna dapat mengklik salah satu elemen untuk membuka halamannya di Github.






Persiapkan lingkungan



Pertama-tama, kita perlu membuat folder proyek dan lingkungan virtual:



mkdir ghnav-flask-tutorial
cd ghnav-flask-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



Mari buat struktur berikut di folder saat ini. Biarkan semua file kosong untuk saat ini. Ini belum kritis.



Struktur awal:



./
├── githubnavigator/
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
└── requirements.txt


Saatnya menginstal Flask dan Dependency Injector.



Mari tambahkan baris berikut ke file requirements.txt:



dependency-injector
flask


Sekarang mari kita instal:



pip install -r requirements.txt


Dan periksa apakah penginstalan berhasil:



python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"


Anda akan melihat sesuatu seperti:



(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2


Halo Dunia!



Mari buat aplikasi hello world minimal.



Mari tambahkan baris berikut ke file views.py:



"""Views module."""


def index():
    return 'Hello, World!'


Sekarang mari tambahkan sebuah wadah untuk dependensi (selanjutnya hanya sebuah wadah). Penampung akan berisi semua komponen aplikasi. Mari tambahkan dua komponen pertama. Ini adalah aplikasi dan tampilan Flask index.



Mari tambahkan yang berikut ini ke file containers.py:



"""Application containers module."""

from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    index_view = flask.View(views.index)


Sekarang kita perlu membuat pabrik aplikasi Flask. Biasanya disebut create_app(). Ini akan membuat wadah. Container tersebut akan digunakan untuk membuat aplikasi Flask. Langkah terakhir adalah menyiapkan perutean - kita akan menetapkan tampilan index_viewdari penampung untuk menangani permintaan ke root "/" aplikasi kita.



Mari edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()

    app = container.app()
    app.container = container

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


Kontainer adalah objek pertama dalam aplikasi. Ini digunakan untuk mendapatkan semua objek lainnya.


Aplikasi kita sekarang siap untuk mengucapkan "Halo, Dunia!"



Jalankan di terminal:



export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run


Outputnya akan terlihat seperti ini:



* Serving Flask app "githubnavigator.application" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 473-587-859


Buka browser Anda dan buka http://127.0.0.1:5000/ .



Anda akan melihat "Halo, Dunia!"



Luar biasa. Aplikasi minimal kami dimulai dan berjalan dengan sukses.



Mari buat sedikit lebih cantik.



Termasuk gaya



Kami akan menggunakan Bootstrap 4 . Mari gunakan ekstensi Bootstrap-Flask untuk ini . Ini akan membantu kami menambahkan semua file yang diperlukan dalam beberapa klik.



Tambahkan bootstrap-flaskke requirements.txt:



dependency-injector
flask
bootstrap-flask


dan jalankan di terminal:



pip install --upgrade -r requirements.txt


Sekarang mari tambahkan ekstensi bootstrap-flaskke wadah.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    index_view = flask.View(views.index)


Mari kita inisialisasi ekstensi bootstrap-flask. Kami harus berubah create_app().



Edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()

    app = container.app()
    app.container = container

    bootstrap = container.bootstrap()
    bootstrap.init_app(app)

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


Sekarang kita perlu menambahkan template. Untuk melakukan ini, kita perlu menambahkan folder templates/ke dalam paket githubnavigator. Tambahkan dua file di dalam folder template:



  • base.html - template dasar
  • index.html - template halaman utama


Buat folder templatesdan dua file kosong di dalam base.htmldan index.html:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
└── requirements.txt


Sekarang mari isi template dasar.



Mari tambahkan baris berikut ke file base.html:



<!doctype html>
<html lang="en">
    <head>
        {% block head %}
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        {% block styles %}
            <!-- Bootstrap CSS -->
            {{ bootstrap.load_css() }}
        {% endblock %}

        <title>{% block title %}{% endblock %}</title>
        {% endblock %}
    </head>
    <body>
        <!-- Your page content -->
        {% block content %}{% endblock %}

        {% block scripts %}
            <!-- Optional JavaScript -->
            {{ bootstrap.load_js() }}
        {% endblock %}
    </body>
</html>


Sekarang mari isi template halaman master.



Mari tambahkan baris berikut ke file index.html:



{% extends "base.html" %}

{% block title %}Github Navigator{% endblock %}

{% block content %}
<div class="container">
    <h1 class="mb-4">Github Navigator</h1>

    <form>
        <div class="form-group form-row">
            <div class="col-10">
                <label for="search_query" class="col-form-label">
                    Search for:
                </label>
                <input class="form-control" type="text" id="search_query"
                       placeholder="Type something to search on the GitHub"
                       name="query"
                       value="{{ query if query }}">
            </div>
            <div class="col">
                <label for="search_limit" class="col-form-label">
                    Limit:
                </label>
                <select class="form-control" id="search_limit" name="limit">
                    {% for value in [5, 10, 20] %}
                    <option {% if value == limit %}selected{% endif %}>
                        {{ value }}
                    </option>
                    {% endfor %}
                </select>
            </div>
        </div>
    </form>

    <p><small>Results found: {{ repositories|length }}</small></p>

    <table class="table table-striped">
        <thead>
            <tr>
                <th>#</th>
                <th>Repository</th>
                <th class="text-nowrap">Repository owner</th>
                <th class="text-nowrap">Last commit</th>
            </tr>
        </thead>
        <tbody>
        {% for repository in repositories %} {{n}}
            <tr>
              <th>{{ loop.index }}</th>
              <td><a href="{{ repository.url }}">
                  {{ repository.name }}</a>
              </td>
              <td><a href="{{ repository.owner.url }}">
                  <img src="{{ repository.owner.avatar_url }}"
                       alt="avatar" height="24" width="24"/></a>
                  <a href="{{ repository.owner.url }}">
                      {{ repository.owner.login }}</a>
              </td>
              <td><a href="{{ repository.latest_commit.url }}">
                  {{ repository.latest_commit.sha }}</a>
                  {{ repository.latest_commit.message }}
                  {{ repository.latest_commit.author_name }}
              </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
</div>

{% endblock %}


Bagus, hampir selesai. Langkah terakhir adalah mengubah tampilan indexuntuk menggunakan template index.html.



Mari edit views.py:



"""Views module."""

from flask import request, render_template


def index():
    query = request.args.get('query', 'Dependency Injector')
    limit = request.args.get('limit', 10, int)

    repositories = []

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


Selesai.



Pastikan aplikasi sedang berjalan atau dijalankan flask rundan buka http://127.0.0.1:5000/ .



Anda harus melihat:







Menghubungkan Github



Di bagian ini, kami akan mengintegrasikan aplikasi kami dengan Github API.

Kami akan menggunakan perpustakaan PyGithub .



Mari tambahkan ke requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub


dan jalankan di terminal:



pip install --upgrade -r requirements.txt


Sekarang kita perlu menambahkan klien API Github ke penampung. Untuk melakukan ini, kita perlu menggunakan dua penyedia baru dari modul dependency_injector.providers:



  • Penyedia Factoryakan membuat klien Github.
  • Penyedia Configurationakan meneruskan token API dan waktu tunggu Github ke klien.


Ayo lakukan.



Mari edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    index_view = flask.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:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


Dan isi dengan baris berikut:



github:
  request_timeout: 10


Untuk bekerja dengan file konfigurasi, kita akan menggunakan pustaka PyYAML . Mari tambahkan ke file dengan dependensi.



Edit requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml


dan instal dependensi:



pip install --upgrade -r requirements.txt


Kami akan menggunakan variabel lingkungan untuk meneruskan token API GITHUB_TOKEN.



Sekarang kita perlu mengedit create_app()untuk melakukan 2 tindakan saat aplikasi dimulai:



  • Muat konfigurasi dari config.yml
  • Muat token API dari variabel lingkungan GITHUB_TOKEN


Edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()
    container.config.from_yaml('config.yml')
    container.config.github.auth_token.from_env('GITHUB_TOKEN')

    app = container.app()
    app.container = container

    bootstrap = container.bootstrap()
    bootstrap.init_app(app)

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


Sekarang kita perlu membuat token API.



Untuk ini, Anda membutuhkan:



  • Ikuti tutorial ini di Github
  • Setel token ke variabel lingkungan:



    export GITHUB_TOKEN=<your token>


Item ini dapat dilewati untuk sementara.



Aplikasi akan berjalan tanpa token, tetapi dengan bandwidth terbatas. Batasan untuk klien yang tidak diautentikasi: 60 permintaan per jam. Token diperlukan untuk meningkatkan kuota ini menjadi 5.000 per jam.


Selesai.



Penginstalan Github API klien selesai.



Layanan pencarian



Saatnya menambahkan layanan pencarian SearchService. Dia akan:



  • Cari di Github
  • Dapatkan data tambahan tentang komit
  • Konversi hasil format


SearchServiceakan menggunakan klien API Github.



Buat file kosong services.pydalam paket githubnavigator:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   ├── services.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


dan tambahkan baris berikut ke dalamnya:



"""Services module."""

from github import Github
from github.Repository import Repository
from github.Commit import Commit


class SearchService:
    """Search service performs search on Github."""

    def __init__(self, github_client: Github):
        self._github_client = github_client

    def search_repositories(self, query, limit):
        """Search for repositories and return formatted data."""
        repositories = self._github_client.search_repositories(
            query=query,
            **{'in': 'name'},
        )
        return [
            self._format_repo(repository)
            for repository in repositories[:limit]
        ]

    def _format_repo(self, repository: Repository):
        commits = repository.get_commits()
        return {
            'url': repository.html_url,
            'name': repository.name,
            'owner': {
                'login': repository.owner.login,
                'url': repository.owner.html_url,
                'avatar_url': repository.owner.avatar_url,
            },
            'latest_commit': self._format_commit(commits[0]) if commits else {},
        }

    def _format_commit(self, commit: Commit):
        return {
            'sha': commit.sha,
            'url': commit.html_url,
            'message': commit.commit.message,
            'author_name': commit.commit.author.name,
        }


Sekarang mari tambahkan SearchServiceke wadah.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(views.index)


Hubungkan pencarian



Kami sekarang siap untuk pencarian bekerja. Mari gunakan SearchServicedalam indextampilan.



Edit views.py:



"""Views module."""

from flask import request, render_template

from .services import SearchService


def index(search_service: SearchService):
    query = request.args.get('query', 'Dependency Injector')
    limit = request.args.get('limit', 10, int)

    repositories = search_service.search_repositories(query, limit)

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


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 flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(
        views.index,
        search_service=search_service,
    )


Pastikan aplikasi sedang berjalan atau dijalankan flask rundan buka http://127.0.0.1:5000/ .



Kamu akan lihat:







Sedikit refactoring



Tampilan kami indexberisi dua nilai hardcode:



  • Istilah pencarian default
  • Batasi jumlah hasil


Ayo lakukan sedikit refactoring. Kami akan mentransfer nilai-nilai ini ke dalam konfigurasi.



Edit views.py:



"""Views module."""

from flask import request, render_template

from .services import SearchService


def index(
        search_service: SearchService,
        default_query: str,
        default_limit: int,
):
    query = request.args.get('query', default_query)
    limit = request.args.get('limit', default_limit, int)

    repositories = search_service.search_repositories(query, limit)

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


Sekarang kita membutuhkan nilai-nilai ini untuk diteruskan saat dipanggil. Mari perbarui penampung.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.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:



github:
  request_timeout: 10
search:
  default_query: "Dependency Injector"
  default_limit: 10


Selesai.



Refactoring selesai. Mu membuat kode lebih bersih.



Menambahkan tes



Akan menyenangkan menambahkan beberapa tes. Ayo lakukan.



Kami akan menggunakan pytest dan cakupan .



Edit requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov


dan instal paket baru:



pip install -r requirements.txt


Buat file kosong tests.pydalam paket githubnavigator:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   ├── services.py
│   ├── tests.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


dan tambahkan baris berikut ke dalamnya:



"""Tests module."""

from unittest import mock

import pytest
from github import Github
from flask import url_for

from .application import create_app


@pytest.fixture
def app():
    return create_app()


def test_index(client, app):
    github_client_mock = mock.Mock(spec=Github)
    github_client_mock.search_repositories.return_value = [
        mock.Mock(
            html_url='repo1-url',
            name='repo1-name',
            owner=mock.Mock(
                login='owner1-login',
                html_url='owner1-url',
                avatar_url='owner1-avatar-url',
            ),
            get_commits=mock.Mock(return_value=[mock.Mock()]),
        ),
        mock.Mock(
            html_url='repo2-url',
            name='repo2-name',
            owner=mock.Mock(
                login='owner2-login',
                html_url='owner2-url',
                avatar_url='owner2-avatar-url',
            ),
            get_commits=mock.Mock(return_value=[mock.Mock()]),
        ),
    ]

    with app.container.github_client.override(github_client_mock):
        response = client.get(url_for('index'))

    assert response.status_code == 200
    assert b'Results found: 2' in response.data

    assert b'repo1-url' in response.data
    assert b'repo1-name' in response.data
    assert b'owner1-login' in response.data
    assert b'owner1-url' in response.data
    assert b'owner1-avatar-url' in response.data

    assert b'repo2-url' in response.data
    assert b'repo2-name' in response.data
    assert b'owner2-login' in response.data
    assert b'owner2-url' in response.data
    assert b'owner2-avatar-url' in response.data


def test_index_no_results(client, app):
    github_client_mock = mock.Mock(spec=Github)
    github_client_mock.search_repositories.return_value = []

    with app.container.github_client.override(github_client_mock):
        response = client.get(url_for('index'))

    assert response.status_code == 200
    assert b'Results found: 0' in response.data


Sekarang mari kita mulai menguji dan memeriksa cakupan:



py.test githubnavigator/tests.py --cov=githubnavigator


Kamu akan lihat:



platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: flask-1.0.0, cov-2.10.0
collected 2 items

githubnavigator/tests.py ..                                     [100%]

---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name                             Stmts   Miss  Cover
----------------------------------------------------
githubnavigator/__init__.py          0      0   100%
githubnavigator/application.py      11      0   100%
githubnavigator/containers.py       13      0   100%
githubnavigator/services.py         14      0   100%
githubnavigator/tests.py            32      0   100%
githubnavigator/views.py             7      0   100%
----------------------------------------------------
TOTAL                               77      0   100%


Perhatikan bagaimana kami mengganti github_clientdengan mock menggunakan metode ini .override(). Dengan cara ini, Anda bisa mengganti nilai kembalian dari penyedia mana pun.



Kesimpulan



Kami membangun aplikasi Flask kami menggunakan injeksi ketergantungan. Kami menggunakan Dependency Injector sebagai kerangka kerja injeksi ketergantungan.



Bagian utama dari aplikasi kita adalah wadahnya. Ini berisi semua komponen aplikasi dan dependensinya di satu tempat. Ini memberikan kendali atas struktur aplikasi. Mudah untuk dipahami dan diubah:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.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