sou o criador do injetor de dependência . Esta é uma estrutura de injeção de dependência para Python.
Neste tutorial, quero mostrar como usar o injetor de dependência para desenvolver aplicativos Flask.
O manual consiste nas seguintes partes:
- O que vamos construir?
- Prepare o ambiente
- Estrutura do projeto
- Olá Mundo!
- Incluindo estilos
- Conectando Github
- Serviço de busca
- Conectar pesquisa
- Um pouco de refatoração
- Adicionando testes
- Conclusão
O projeto concluído pode ser encontrado no Github .
Para começar você deve ter:
- Python 3.5+
- Ambiente virtual
E é desejável ter:
- Habilidades de desenvolvimento inicial com Flask
- Compreender o princípio da injeção de dependência
O que vamos construir?
Estaremos construindo um aplicativo que ajuda você a pesquisar repositórios no Github. Vamos chamá-lo de Github Navigator.
Como funciona o Github Navigator?
- O usuário abre uma página da web onde é solicitado a inserir uma consulta de pesquisa.
- O usuário insere uma consulta e pressiona Enter.
- O Github Navigator procura por repositórios correspondentes no Github.
- Quando a pesquisa termina, o Github Navigator mostra ao usuário uma página da web com os resultados.
- A página de resultados mostra todos os repositórios encontrados e a consulta de pesquisa.
- Para cada repositório, o usuário vê:
- nome do repositório
- dono do repositório
- o último commit para o repositório
- O usuário pode clicar em qualquer um dos elementos para abrir sua página no Github.
Prepare o ambiente
Em primeiro lugar, precisamos criar uma pasta de projeto e um ambiente virtual:
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
Agora vamos ativar o ambiente virtual:
. venv/bin/activate
O ambiente está pronto, agora vamos começar com a estrutura do projeto.
Estrutura do projeto
Vamos criar a seguinte estrutura na pasta atual. Deixe todos os arquivos vazios por enquanto. Isso ainda não é crítico.
Estrutura inicial:
./
├── githubnavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
É hora de instalar o Flask e o Dependency Injector.
Vamos adicionar as seguintes linhas ao arquivo
requirements.txt
:
dependency-injector
flask
Agora vamos instalá-los:
pip install -r requirements.txt
E verifique se a instalação foi bem-sucedida:
python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"
Você verá 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
Olá Mundo!
Vamos criar um aplicativo hello world mínimo.
Vamos adicionar as seguintes linhas ao arquivo
views.py
:
"""Views module."""
def index():
return 'Hello, World!'
Agora vamos adicionar um contêiner de dependências (mais um contêiner). O contêiner conterá todos os componentes do aplicativo. Vamos adicionar os dois primeiros componentes. Este é um aplicativo e visualização do Flask
index
.
Vamos adicionar o seguinte ao arquivo
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)
Agora precisamos criar uma fábrica de aplicativos Flask. Geralmente é chamado
create_app()
. Isso criará um contêiner. O contêiner será usado para criar o aplicativo Flask. A etapa final é configurar o roteamento - atribuiremos uma visão index_view
do contêiner para lidar com as solicitações para a raiz "/" de nosso aplicativo.
Vamos 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
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
O contêiner é o primeiro objeto do aplicativo. É usado para obter todos os outros objetos.
Nosso aplicativo agora está pronto para dizer "Olá, mundo!"
Executar em um terminal:
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
A saída deve ser semelhante a esta:
* 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 seu navegador e vá para http://127.0.0.1:5000/ .
Você verá "Hello, World!"
Excelente. Nosso aplicativo mínimo é iniciado e executado com êxito.
Vamos deixar um pouco mais bonito.
Incluindo estilos
Estaremos usando o Bootstrap 4 . Vamos usar a extensão Bootstrap-Flask para isso . Isso nos ajudará a adicionar todos os arquivos necessários em alguns cliques.
Adicionar
bootstrap-flask
a requirements.txt
:
dependency-injector
flask
bootstrap-flask
e execute no terminal:
pip install --upgrade -r requirements.txt
Agora vamos adicionar a extensão
bootstrap-flask
ao contêiner.
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)
Vamos inicializar a extensão
bootstrap-flask
. Precisamos mudar 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
Agora precisamos adicionar modelos. Para fazer isso, precisamos adicionar uma pasta
templates/
ao pacote githubnavigator
. Adicione dois arquivos dentro da pasta de modelos:
base.html
- modelo básicoindex.html
- modelo da página principal
Crie uma pasta
templates
e dois arquivos vazios dentro base.html
e index.html
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Agora vamos preencher o modelo básico.
Vamos adicionar as seguintes linhas ao arquivo
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>
Agora vamos preencher o modelo de página mestra.
Vamos adicionar as seguintes linhas ao arquivo
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 %}
Ótimo, quase pronto. A etapa final é alterar a visualização
index
para usar o modelo index.html
.
Vamos editar
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,
)
Feito.
Certifique-se de que o aplicativo esteja em execução ou execute
flask run
e abra http://127.0.0.1:5000/ .
Você deveria ver:
Conectando Github
Nesta seção, iremos integrar nosso aplicativo com a API do Github.
Estaremos usando a biblioteca PyGithub .
Vamos adicioná-lo a
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
e execute no terminal:
pip install --upgrade -r requirements.txt
Agora precisamos adicionar o cliente API Github ao contêiner. Para fazer isso, precisaremos usar dois novos provedores do módulo
dependency_injector.providers
:
- O provedor
Factory
criará o cliente Github. - O provedor
Configuration
passará o token de API e o tempo limite do Github para o cliente.
Vamos fazer isso.
Vamos 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 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 os parâmetros de configuração antes de definir seus valores. Este é o princípio pelo qual o provedor trabalhaConfiguration
.
Primeiro usamos, depois definimos os valores.
Agora vamos adicionar o arquivo de configuração.
Usaremos YAML.
Crie um arquivo vazio
config.yml
na raiz do projeto:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
E preencha com as seguintes linhas:
github:
request_timeout: 10
Para trabalhar com o arquivo de configuração, usaremos a biblioteca PyYAML . Vamos adicioná-lo ao arquivo com dependências.
Editar
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
e instale a dependência:
pip install --upgrade -r requirements.txt
Usaremos uma variável de ambiente para passar o token de API
GITHUB_TOKEN
.
Agora precisamos editar
create_app()
para fazer 2 ações quando o aplicativo é iniciado:
- Carregar configuração de
config.yml
- Carregar token de API da variável de ambiente
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
Agora precisamos criar um token de API.
Para isso você precisa:
- Siga este tutorial no Github
- Defina o token para a variável de ambiente:
export GITHUB_TOKEN=<your token>
Este item pode ser temporariamente ignorado.
O aplicativo será executado sem um token, mas com largura de banda limitada. Limite para clientes não autenticados: 60 solicitações por hora. O token é necessário para aumentar essa cota para 5.000 por hora.
Feito.
A instalação do cliente Github API está concluída.
Serviço de busca
É hora de adicionar um serviço de pesquisa
SearchService
. Ele vai:
- Pesquise no Github
- Obtenha dados adicionais sobre commits
- Resultado de conversão de formato
SearchService
usará o cliente API Github.
Crie um arquivo vazio
services.py
no pacote githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
e adicione as seguintes linhas a ele:
"""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,
}
Agora vamos adicionar
SearchService
ao contêiner.
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 pesquisa
Agora estamos prontos para que a pesquisa funcione. Vamos usar
SearchService
em 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,
)
Agora vamos alterar o contêiner para passar a dependência
SearchService
para a visualização index
quando for chamada.
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,
)
Certifique-se de que o aplicativo esteja em execução ou execute
flask run
e abra http://127.0.0.1:5000/ .
Você verá:
Um pouco de refatoração
Nossa visualização
index
contém dois valores codificados:
- Termo de pesquisa padrão
- Limite para o número de resultados
Vamos refatorar um pouco. Vamos transferir esses valores para a configuração.
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,
)
Agora precisamos que esses valores sejam repassados à chamada. Vamos atualizar o contêiner.
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,
)
Agora vamos atualizar o arquivo de configuração.
Editar
config.yml
:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
Feito.
A refatoração está completa. Mu tornou o código mais limpo.
Adicionando testes
Seria bom adicionar alguns testes. Vamos fazer isso.
Estaremos usando teste e cobertura .
Editar
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov
e instale novos pacotes:
pip install -r requirements.txt
Crie um arquivo vazio
tests.py
no pacote githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
e adicione as seguintes linhas a ele:
"""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
Agora vamos começar a testar e verificar a cobertura:
py.test githubnavigator/tests.py --cov=githubnavigator
Você verá:
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 como substituímosgithub_client
por mock usando o método.override()
. Dessa forma, você pode substituir o valor de retorno de qualquer provedor.
Conclusão
Construímos nosso aplicativo Flask usando injeção de dependência. Usamos o Dependency Injector como uma estrutura de injeção de dependência.
A parte principal do nosso aplicativo é o contêiner. Ele contém todos os componentes do aplicativo e suas dependências em um só lugar. Isso fornece controle sobre a estrutura do aplicativo. É fácil de entender e mudar:
"""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,
)
Um contêiner como um mapa de seu aplicativo. Você sempre sabe o que depende do quê.
Qual é o próximo?
- Saiba mais sobre o Dependency Injector no GitHub
- Confira a documentação em Read the Docs
- Tem uma pergunta ou encontrou um bug? Abra um problema no Github