soy el creador de Dependency Injector . Este es un marco de inyección de dependencia para Python.
En este tutorial, quiero mostrarle cómo utilizar el inyector de dependencia para desarrollar aplicaciones Flask.
El manual consta de las siguientes partes:
- ¿Qué vamos a construir?
- Prepara el medio ambiente
- Estructura del proyecto
- ¡Hola Mundo!
- Incluyendo estilos
- Conectando Github
- Servicio de búsqueda
- Conectar búsqueda
- Un poco de refactorización
- Agregar pruebas
- Conclusión
El proyecto completo se puede encontrar en Github .
Para empezar debes tener:
- Python 3.5+
- Ambiente virtual
Y es deseable tener:
- Habilidades de desarrollo inicial con Flask
- Comprender el principio de inyección de dependencia
¿Qué vamos a construir?
Crearemos una aplicación que te ayudará a buscar repositorios en Github. Llamémoslo Github Navigator.
¿Cómo funciona Github Navigator?
- El usuario abre una página web donde se le solicita que ingrese una consulta de búsqueda.
- El usuario ingresa una consulta y presiona Enter.
- Github Navigator busca repositorios coincidentes en Github.
- Cuando finaliza la búsqueda, Github Navigator muestra al usuario una página web con resultados.
- La página de resultados muestra todos los repositorios encontrados y la consulta de búsqueda.
- Para cada repositorio, el usuario ve:
- nombre del repositorio
- propietario del repositorio
- el último compromiso con el repositorio
- El usuario puede hacer clic en cualquiera de los elementos para abrir su página en Github.
Prepara el medio ambiente
En primer lugar, necesitamos crear una carpeta de proyecto y un entorno virtual:
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
Ahora activemos el entorno virtual:
. venv/bin/activate
El entorno está listo, ahora comencemos con la estructura del proyecto.
Estructura del proyecto
Creemos la siguiente estructura en la carpeta actual. Deje todos los archivos vacíos por ahora. Esto aún no es crítico.
Estructura inicial:
./
├── githubnavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Es hora de instalar Flask and Dependency Injector.
Agreguemos las siguientes líneas al archivo
requirements.txt
:
dependency-injector
flask
Ahora instalémoslos:
pip install -r requirements.txt
Y compruebe que la instalación se haya realizado correctamente:
python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"
Verás algo como:
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2
¡Hola Mundo!
Creemos una aplicación mínima de hola mundo.
Agreguemos las siguientes líneas al archivo
views.py
:
"""Views module."""
def index():
return 'Hello, World!'
Ahora agreguemos un contenedor de dependencias (además, solo un contenedor). El contenedor contendrá todos los componentes de la aplicación. Agreguemos los dos primeros componentes. Esta es una aplicación y vista de Flask
index
.
Agreguemos lo siguiente al archivo
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)
Ahora tenemos que crear una fábrica de aplicaciones Flask. Suele llamarse
create_app()
. Creará un contenedor. El contenedor se utilizará para crear la aplicación Flask. El paso final es configurar el enrutamiento: asignaremos una vista index_view
desde el contenedor para manejar las solicitudes a la raíz "/" de nuestra aplicación.
Editemos
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
El contenedor es el primer objeto de la aplicación. Se usa para obtener todos los demás objetos.
Nuestra aplicación ya está lista para decir "¡Hola, mundo!"
Ejecutar en una terminal:
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
La salida debería verse así:
* 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
Abra su navegador y vaya a http://127.0.0.1:5000/ .
Verá "¡Hola, mundo!"
Excelente. Nuestra aplicación mínima se inicia y se ejecuta correctamente.
Hagámoslo un poco más bonito.
Incluyendo estilos
Usaremos Bootstrap 4 . Usemos la extensión Bootstrap-Flask para esto . Nos ayudará a agregar todos los archivos necesarios en unos pocos clics.
Agregar
bootstrap-flask
a requirements.txt
:
dependency-injector
flask
bootstrap-flask
y ejecutar en la terminal:
pip install --upgrade -r requirements.txt
Ahora agreguemos la extensión
bootstrap-flask
al contenedor.
Editar
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)
Inicialicemos la extensión
bootstrap-flask
. Tendremos que cambiar create_app()
.
Editar
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
Ahora necesitamos agregar plantillas. Para hacer esto, necesitamos agregar una carpeta
templates/
al paquete githubnavigator
. Agregue dos archivos dentro de la carpeta de plantillas:
base.html
- plantilla básicaindex.html
- plantilla de página principal
Cree una carpeta
templates
y dos archivos vacíos dentro base.html
y index.html
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Ahora completemos la plantilla básica.
Agreguemos las siguientes líneas al archivo
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>
Ahora completemos la plantilla de la página maestra.
Agreguemos las siguientes líneas al archivo
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 %}
Genial, casi terminado. El último paso es cambiar la vista
index
para usar la plantilla index.html
.
Editemos
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,
)
Hecho.
Asegúrese de que la aplicación se esté ejecutando o ejecute
flask run
y abra http://127.0.0.1:5000/ .
Debería ver:
Conectando Github
En esta sección, integraremos nuestra aplicación con la API de Github.
Usaremos la biblioteca PyGithub .
Vamos a agregarlo a
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
y ejecutar en la terminal:
pip install --upgrade -r requirements.txt
Ahora necesitamos agregar el cliente API de Github al contenedor. Para hacer esto, necesitaremos utilizar dos nuevos proveedores del módulo
dependency_injector.providers
:
- El proveedor
Factory
creará el cliente Github. - El proveedor
Configuration
pasará el token de API y el tiempo de espera de Github al cliente.
Vamos a hacerlo.
Editemos
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)
Usamos los parámetros de configuración antes de establecer sus valores. Este es el principio por el que trabaja el proveedorConfiguration
.
Primero usamos, luego establecemos los valores.
Ahora agreguemos el archivo de configuración.
Usaremos YAML.
Cree un archivo vacío
config.yml
en la raíz del proyecto:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
Y rellénelo con las siguientes líneas:
github:
request_timeout: 10
Para trabajar con el archivo de configuración, usaremos la biblioteca PyYAML . Agreguémoslo al archivo con dependencias.
Editar
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
e instala la dependencia:
pip install --upgrade -r requirements.txt
Usaremos una variable de entorno para pasar el token de API
GITHUB_TOKEN
.
Ahora necesitamos editar
create_app()
para hacer 2 acciones cuando se inicia la aplicación:
- Cargar configuración desde
config.yml
- Cargar token de API desde la variable de entorno
GITHUB_TOKEN
Editar
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
Ahora necesitamos crear un token de API.
Para esto necesitas:
- Siga este tutorial en Github
- Establezca el token en la variable de entorno:
export GITHUB_TOKEN=<your token>
Este elemento se puede omitir temporalmente.
La aplicación se ejecutará sin un token, pero con un ancho de banda limitado. Límite de clientes no autenticados: 60 solicitudes por hora. El token es necesario para aumentar esta cuota a 5000 por hora.
Hecho.
La instalación de la API de Github del cliente está completa.
Servicio de búsqueda
Es hora de agregar un servicio de búsqueda
SearchService
. Él:
- Buscar en Github
- Obtenga datos adicionales sobre confirmaciones
- Convertir formato de resultado
SearchService
utilizará el cliente API de Github.
Cree un archivo vacío
services.py
en el paquete githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
y agregue las siguientes líneas:
"""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,
}
Ahora agreguemos
SearchService
al contenedor.
Editar
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)
Conectar búsqueda
Ahora estamos listos para que la búsqueda funcione. Vamos uso
SearchService
en la index
vista.
Editar
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,
)
Ahora cambiemos el contenedor para pasar la dependencia
SearchService
a la vista index
cuando se la llame.
Editar
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,
)
Asegúrese de que la aplicación se esté ejecutando o se esté ejecutando
flask run
y abra http://127.0.0.1:5000/ .
Ya verás:
Un poco de refactorización
Nuestra vista
index
contiene dos valores codificados:
- Término de búsqueda predeterminado
- Límite para el número de resultados
Hagamos una pequeña refactorización. Transferiremos estos valores a la configuración.
Editar
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,
)
Ahora necesitamos que estos valores se pasen en la llamada. Actualicemos el contenedor.
Editar
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,
)
Ahora actualice el archivo de configuración.
Editar
config.yml
:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
Hecho.
La refactorización está completa. Mu hizo el código más limpio.
Agregar pruebas
Sería bueno agregar algunas pruebas. Vamos a hacerlo.
Usaremos Pytest y Cobertura .
Editar
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov
e instalar nuevos paquetes:
pip install -r requirements.txt
Cree un archivo vacío
tests.py
en el paquete githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
y agregue las siguientes líneas:
"""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
Ahora comencemos a probar y verificar la cobertura:
py.test githubnavigator/tests.py --cov=githubnavigator
Ya verás:
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%
Observe cómo reemplazamosgithub_client
con simulacro usando el método.override()
. De esta forma, puede anular el valor de retorno de cualquier proveedor.
Conclusión
Creamos nuestra aplicación Flask usando inyección de dependencia. Usamos Dependency Injector como un marco de inyección de dependencias.
La parte principal de nuestra aplicación es el contenedor. Contiene todos los componentes de la aplicación y sus dependencias en un solo lugar. Esto proporciona control sobre la estructura de la aplicación. Es fácil de entender y cambiar:
"""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,
)
Un contenedor como mapa de su aplicación. Siempre sabes qué depende de qué.
¿Que sigue?
- Obtenga más información sobre el inyector de dependencia en GitHub
- Consulte la documentación en Leer los documentos
- ¿Tiene alguna pregunta o encuentra un error? Abrir un problema en Github