Flask + Dependency Injector - guia de injeção de dependência

Olá,



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:



  1. O que vamos construir?
  2. Prepare o ambiente
  3. Estrutura do projeto
  4. Olá Mundo!
  5. Incluindo estilos
  6. Conectando Github
  7. Serviço de busca
  8. Conectar pesquisa
  9. Um pouco de refatoração
  10. Adicionando testes
  11. 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_viewdo 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-flaska requirements.txt:



dependency-injector
flask
bootstrap-flask


e execute no terminal:



pip install --upgrade -r requirements.txt


Agora vamos adicionar a extensão bootstrap-flaskao 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ásico
  • index.html - modelo da página principal


Crie uma pasta templatese dois arquivos vazios dentro base.htmle 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 indexpara 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 rune 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 Factorycriará o cliente Github.
  • O provedor Configurationpassará 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 trabalha Configuration.



Primeiro usamos, depois definimos os valores.



Agora vamos adicionar o arquivo de configuração.

Usaremos YAML.



Crie um arquivo vazio config.ymlna 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


SearchServiceusará o cliente API Github.



Crie um arquivo vazio services.pyno 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 SearchServiceao 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 SearchServiceem indexvista.



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 SearchServicepara a visualização indexquando 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 rune abra http://127.0.0.1:5000/ .



Você verá:







Um pouco de refatoração



Nossa visualização indexconté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.pyno 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ímos github_clientpor 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?






All Articles