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:
- Apa yang akan kita bangun?
- Persiapkan lingkungan
- Struktur proyek
- Halo Dunia!
- Termasuk gaya
- Menghubungkan Github
- 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 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_view
dari 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-flask
ke requirements.txt
:
dependency-injector
flask
bootstrap-flask
dan jalankan di terminal:
pip install --upgrade -r requirements.txt
Sekarang mari tambahkan ekstensi
bootstrap-flask
ke 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 dasarindex.html
- template halaman utama
Buat folder
templates
dan dua file kosong di dalam base.html
dan 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
index
untuk 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 run
dan 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
Factory
akan membuat klien Github. - Penyedia
Configuration
akan 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 penyediaConfiguration
.
Pertama kita gunakan, lalu kita atur nilainya.
Sekarang mari tambahkan file konfigurasi.
Kami akan menggunakan YAML.
Buat file kosong
config.yml
di 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:
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
SearchService
akan menggunakan klien API Github.
Buat file kosong
services.py
dalam 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
SearchService
ke 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
SearchService
dalam index
tampilan.
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
SearchService
ke tampilan index
saat 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 run
dan buka http://127.0.0.1:5000/ .
Kamu akan lihat:
Sedikit refactoring
Tampilan kami
index
berisi 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.py
dalam 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 menggantigithub_client
dengan 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?
- Pelajari lebih lanjut tentang Dependency Injector di GitHub
- Lihat dokumentasinya di Read the Docs
- Punya pertanyaan atau temukan bug? Buka masalah di Github