أنا مبتكر حاقن التبعية . هذا هو إطار عمل حقن التبعية لبايثون.
في هذا البرنامج التعليمي ، أريد أن أوضح كيفية استخدام حاقن التبعية لتطوير تطبيقات Flask.
يتكون الدليل من الأجزاء التالية:
- ماذا سنبني؟
- جهز البيئة
- هيكل المشروع
- مرحبا بالعالم!
- بما في ذلك الأنماط
- ربط جيثب
- خدمة البحث
- ربط البحث
- قليلا من إعادة بناء ديون
- إضافة الاختبارات
- خاتمة
يمكن العثور على المشروع المكتمل على Github .
للبدء ، يجب أن يكون لديك:
- Python 3.5+
- بيئة افتراضية
ويستحب أن يكون لديك:
- مهارات التطوير الأولية باستخدام Flask
- فهم مبدأ حقن التبعية
ماذا سنبني؟
سنقوم ببناء تطبيق يساعدك في البحث عن مستودعات على Github. دعنا نسميها Github Navigator.
كيف يعمل Github Navigator؟
- يفتح المستخدم صفحة ويب حيث يُطلب منه إدخال استعلام بحث.
- يقوم المستخدم بإدخال استعلام والضغط على Enter.
- يبحث Github Navigator عن مستودعات مطابقة على Github.
- عند انتهاء البحث ، يعرض Github Navigator للمستخدم صفحة ويب تحتوي على نتائج.
- تعرض صفحة النتائج جميع المستودعات التي تم العثور عليها واستعلام البحث.
- لكل مستودع ، يرى المستخدم:
- اسم المستودع
- مالك المستودع
- الالتزام الأخير بالمستودع
- يمكن للمستخدم النقر فوق أي عنصر لفتح صفحته على Github.
جهز البيئة
بادئ ذي بدء ، نحتاج إلى إنشاء مجلد مشروع وبيئة افتراضية:
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
لنقم الآن بتنشيط البيئة الافتراضية:
. venv/bin/activate
البيئة جاهزة ، لنبدأ الآن بهيكل المشروع.
هيكل المشروع
لنقم بإنشاء الهيكل التالي في المجلد الحالي. اترك كل الملفات فارغة الآن. هذا ليس حرجا بعد.
الهيكل الأولي:
./
├── githubnavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
حان الوقت لتثبيت Flask and Dependency Injector.
دعنا نضيف الأسطر التالية إلى الملف
requirements.txt
:
dependency-injector
flask
الآن دعنا نثبتها:
pip install -r requirements.txt
وتحقق من نجاح التثبيت:
python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"
سترى شيئًا مثل:
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2
مرحبا بالعالم!
لنقم بإنشاء تطبيق hello world ضئيل.
دعنا نضيف الأسطر التالية إلى الملف
views.py
:
"""Views module."""
def index():
return 'Hello, World!'
الآن دعونا نضيف حاوية من التبعيات (علاوة على ذلك مجرد حاوية). ستحتوي الحاوية على جميع مكونات التطبيق. دعونا نضيف أول مكونين. هذا هو تطبيق وعرض Flask
index
.
دعنا نضيف ما يلي إلى الملف
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)
نحتاج الآن إلى إنشاء مصنع لتطبيق Flask. عادة ما يطلق عليه
create_app()
. سيخلق حاوية. سيتم استخدام الحاوية لإنشاء تطبيق Flask. الخطوة الأخيرة هي إعداد التوجيه - سنقوم بتعيين عرض index_view
من الحاوية لمعالجة الطلبات إلى جذر "/" تطبيقنا.
دعنا نحرر
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
الحاوية هي الكائن الأول في التطبيق. يتم استخدامه للحصول على جميع الأشياء الأخرى.
طلبنا الآن جاهز ليقول "Hello، World!"
تشغيل في المحطة:
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
يجب أن يبدو الناتج كما يلي:
* 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
افتح المتصفح وانتقل إلى http://127.0.0.1:5000/ .
سترى "Hello، World!"
ممتاز. يبدأ تطبيقنا البسيط ويعمل بنجاح.
لنجعلها أجمل قليلاً.
بما في ذلك الأنماط
سنكون باستخدام التمهيد 4 . دعونا نستخدم ملحق Bootstrap-Flask لهذا الغرض . سيساعدنا ذلك في إضافة جميع الملفات الضرورية ببضع نقرات.
أضف
bootstrap-flask
إلى requirements.txt
:
dependency-injector
flask
bootstrap-flask
ونفذ في الجهاز:
pip install --upgrade -r requirements.txt
لنقم الآن بإضافة الامتداد
bootstrap-flask
إلى الحاوية.
تحرير
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)
لنبدأ الامتداد
bootstrap-flask
. سنحتاج إلى التغيير create_app()
.
تحرير
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
الآن نحن بحاجة إلى إضافة قوالب. للقيام بذلك ، نحتاج إلى إضافة مجلد
templates/
إلى الحزمة githubnavigator
. أضف ملفين داخل مجلد القوالب:
base.html
- نموذج أساسيindex.html
- قالب الصفحة الرئيسية
قم بإنشاء مجلد
templates
وملفين فارغين بداخله base.html
و index.html
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
الآن دعنا نملأ النموذج الأساسي.
دعنا نضيف الأسطر التالية إلى الملف
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>
الآن دعنا نملأ قالب الصفحة الرئيسية.
دعنا نضيف الأسطر التالية إلى الملف
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 %}
رائع ، أوشك على الانتهاء. الخطوة الأخيرة هي تغيير العرض
index
لاستخدام النموذج index.html
.
دعنا نحرر
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,
)
منجز.
تأكد من تشغيل التطبيق أو تشغيله
flask run
وافتح http://127.0.0.1:5000/ .
يجب أن ترى:
ربط جيثب
في هذا القسم ، سنقوم بدمج تطبيقنا مع Github API. سنستخدم
مكتبة PyGithub .
دعنا نضيفه إلى
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
ونفذ في الجهاز:
pip install --upgrade -r requirements.txt
نحتاج الآن إلى إضافة عميل Github API إلى الحاوية. للقيام بذلك ، سنحتاج إلى استخدام مزودين جديدين من الوحدة
dependency_injector.providers
:
- سيقوم الموفر
Factory
بإنشاء عميل Github. Configuration
سيمرر الموفر رمز واجهة برمجة التطبيقات ومهلة Github للعميل.
دعنا نقوم به.
دعنا نحرر
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)
استخدمنا معلمات التكوين قبل تحديد قيمها. هذا هو المبدأ الذي يعمل به المزودConfiguration
.
أولا نستخدم ، ثم نضع القيم.
الآن دعنا نضيف ملف التكوين.
سوف نستخدم YAML.
قم بإنشاء ملف فارغ
config.yml
في جذر المشروع:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
واملأه بالأسطر التالية:
github:
request_timeout: 10
للعمل مع ملف التكوين ، سنستخدم مكتبة PyYAML . دعنا نضيفه إلى الملف مع التبعيات.
تحرير
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
وقم بتثبيت التبعية:
pip install --upgrade -r requirements.txt
سنستخدم متغير بيئة لتمرير الرمز المميز لواجهة برمجة التطبيقات
GITHUB_TOKEN
.
نحتاج الآن إلى التعديل
create_app()
للقيام بإجراءين عند بدء التطبيق:
- تكوين التحميل من
config.yml
- تحميل رمز API من متغير البيئة
GITHUB_TOKEN
تحرير
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
الآن نحن بحاجة إلى إنشاء رمز API مميز.
لهذا تحتاج:
- اتبع هذا البرنامج التعليمي على جيثب
- تعيين الرمز إلى متغير البيئة:
export GITHUB_TOKEN=<your token>
يمكن تخطي هذا العنصر مؤقتًا.
سيتم تشغيل التطبيق بدون رمز مميز ، ولكن بنطاق ترددي محدود. الحد الأقصى للعملاء غير المصدقين: 60 طلبًا في الساعة. هناك حاجة إلى الرمز المميز لزيادة هذه الحصة إلى 5000 في الساعة.
منجز.
اكتمل تثبيت Github API الخاص بالعميل.
خدمة البحث
حان الوقت لإضافة خدمة البحث
SearchService
. هو سوف:
- ابحث على جيثب
- احصل على بيانات إضافية حول الالتزامات
- تحويل نتيجة التنسيق
SearchService
سيستخدم عميل Github API.
قم بإنشاء ملف فارغ
services.py
في الحزمة githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
وأضف إليها الأسطر التالية:
"""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,
}
الآن دعنا نضيف
SearchService
إلى الحاوية.
تحرير
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)
ربط البحث
نحن الآن جاهزون للبحث للعمل. دعونا نستخدم
SearchService
في index
العرض.
تحرير
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,
)
لنقم الآن بتغيير الحاوية لتمرير التبعية
SearchService
إلى العرض index
عندما يتم استدعاؤها.
تحرير
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,
)
تأكد من تشغيل التطبيق أو تشغيله
flask run
وافتح http://127.0.0.1:5000/ .
سوف ترى:
قليلا من إعادة بناء ديون
index
يحتوي
العرض الخاص بنا على قيمتين مشفرتين:
- مصطلح البحث الافتراضي
- حد لعدد النتائج
لنقم بإعادة بناء ديون صغيرة. سننقل هذه القيم إلى التكوين.
تحرير
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,
)
الآن نحن بحاجة إلى تمرير هذه القيم عند الطلب. لنقم بتحديث الحاوية.
تحرير
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,
)
لنقم الآن بتحديث ملف التكوين.
تحرير
config.yml
:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
منجز.
تم الانتهاء من إعادة البناء. Mu جعل الكود أنظف.
إضافة الاختبارات
سيكون من الجيد إضافة بعض الاختبارات. دعنا نقوم به.
سنكون باستخدام pytest و التغطية .
تحرير
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov
وتثبيت حزم جديدة:
pip install -r requirements.txt
قم بإنشاء ملف فارغ
tests.py
في الحزمة githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
وأضف إليها الأسطر التالية:
"""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
لنبدأ الآن في الاختبار والتحقق من التغطية:
py.test githubnavigator/tests.py --cov=githubnavigator
سوف ترى:
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%
لاحظ كيف استبدلناgithub_client
بـ mock باستخدام الطريقة.override()
. بهذه الطريقة ، يمكنك تجاوز القيمة المرجعة لأي مزود.
خاتمة
قمنا ببناء تطبيق Flask الخاص بنا باستخدام حقن التبعية. استخدمنا حاقن التبعية كإطار لحقن التبعية.
الجزء الرئيسي من تطبيقنا هو الحاوية. يحتوي على جميع مكونات التطبيق وتبعياتها في مكان واحد. يوفر هذا التحكم في هيكل التطبيق. من السهل فهمها وتغييرها:
"""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,
)
الحاوية كخريطة لتطبيقك. أنت تعرف دائمًا ما يعتمد على ما.
ماذا بعد؟
- تعرف على المزيد حول Dependency Injector على GitHub
- تحقق من الوثائق في قراءة المستندات
- لديك سؤال أو تجد علة؟ افتح مشكلة على جيثب