Flask + Dependency Injector - Guide d'injection de dépendances

Salut,



je suis le créateur de Dependency Injector . Il s'agit d'un framework d'injection de dépendances pour Python.



Dans ce didacticiel, je souhaite montrer comment utiliser l'injecteur de dépendances pour développer des applications Flask.



Le manuel comprend les parties suivantes:



  1. Qu'allons-nous construire?
  2. Préparez l'environnement
  3. Structure du projet
  4. Bonjour le monde!
  5. Y compris les styles
  6. Connecter Github
  7. Service de recherche
  8. Connecter la recherche
  9. Un peu de refactoring
  10. Ajouter des tests
  11. Conclusion


Le projet terminé peut être trouvé sur Github .



Pour commencer, vous devez avoir:



  • Python 3.5+
  • Environnement virtuel


Et il est souhaitable d'avoir:



  • CompĂ©tences de dĂ©veloppement initial avec Flask
  • Comprendre le principe de l'injection de dĂ©pendances


Qu'allons-nous construire?



Nous allons créer une application qui vous aide à rechercher des référentiels sur Github. Appelons cela Github Navigator.



Comment fonctionne Github Navigator?



  • L'utilisateur ouvre une page Web sur laquelle il est invitĂ© Ă  entrer une requĂŞte de recherche.
  • L'utilisateur entre une requĂŞte et appuie sur EntrĂ©e.
  • Le navigateur Github recherche les rĂ©fĂ©rentiels correspondants sur Github.
  • Une fois la recherche terminĂ©e, le navigateur Github montre Ă  l'utilisateur une page Web avec des rĂ©sultats.
  • La page de rĂ©sultats affiche tous les rĂ©fĂ©rentiels trouvĂ©s et la requĂŞte de recherche.
  • Pour chaque rĂ©fĂ©rentiel, l'utilisateur voit:

    • nom du rĂ©fĂ©rentiel
    • propriĂ©taire du rĂ©fĂ©rentiel
    • le dernier commit dans le rĂ©fĂ©rentiel
  • L'utilisateur peut cliquer sur n'importe lequel des Ă©lĂ©ments pour ouvrir sa page sur Github.






Préparez l'environnement



Tout d'abord, nous devons créer un dossier de projet et un environnement virtuel:



mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv


Maintenant, activons l'environnement virtuel:



. venv/bin/activate


L'environnement est prêt, commençons maintenant par la structure du projet.



Structure du projet



Créons la structure suivante dans le dossier actuel. Laissez tous les fichiers vides pour le moment. Ce n'est pas encore critique.



Structure initiale:



./
├── githubnavigator/
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
└── requirements.txt


Il est temps d'installer Flask et Dependency Injector.



Ajoutons les lignes suivantes au fichier requirements.txt:



dependency-injector
flask


Maintenant, installons-les:



pip install -r requirements.txt


Et vérifiez que l'installation a réussi:



python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"


Vous verrez quelque chose comme:



(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2


Bonjour le monde!



Créons une application hello world minimale.



Ajoutons les lignes suivantes au fichier views.py:



"""Views module."""


def index():
    return 'Hello, World!'


Ajoutons maintenant un conteneur pour les dépendances (ci-après juste un conteneur). Le conteneur contiendra tous les composants de l'application. Ajoutons les deux premiers composants. Ceci est une application et une vue Flask index.



Ajoutons ce qui suit au fichier 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)


Nous devons maintenant créer une fabrique d'applications Flask. Il est généralement appelé create_app(). Cela créera un conteneur. Le conteneur sera utilisé pour créer l'application Flask. La dernière étape consiste à configurer le routage - nous attribuerons la vue index_viewdu conteneur pour gérer les requêtes à la racine "/" de notre application.



Éditons 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


Le conteneur est le premier objet de l'application. Il est utilisé pour récupérer tous les autres objets.


Notre application est maintenant prĂŞte Ă  dire "Hello, World!"



Exécuter dans un terminal:



export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run


La sortie devrait ressembler Ă  ceci:



* 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


Ouvrez votre navigateur et accédez à http://127.0.0.1:5000/ .



Vous verrez "Hello, World!"



Excellent. Notre application minimale démarre et fonctionne avec succès.



Rendons-le un peu plus joli.



Y compris les styles



Nous utiliserons Bootstrap 4 . Utilisons l'extension Bootstrap-Flask pour cela . Cela nous aidera à ajouter tous les fichiers nécessaires en quelques clics.



Ajouter bootstrap-flaskĂ  requirements.txt:



dependency-injector
flask
bootstrap-flask


et exécutez dans le terminal:



pip install --upgrade -r requirements.txt


Ajoutons maintenant l'extension bootstrap-flaskau conteneur.



Modifier 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)


Initialisons l'extension bootstrap-flask. Nous devrons changer create_app().



Modifier 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


Nous devons maintenant ajouter des modèles. Pour ce faire, nous devons ajouter un dossier templates/au package githubnavigator. Ajoutez deux fichiers dans le dossier des modèles:



  • base.html - modèle de base
  • index.html - modèle de page principale


Créez un dossier templateset deux fichiers vides à l'intérieur base.htmlet index.html:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
└── requirements.txt


Maintenant, remplissons le modèle de base.



Ajoutons les lignes suivantes au fichier 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>


Maintenant, remplissons le modèle de page maître.



Ajoutons les lignes suivantes au fichier 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 %}


Super, presque terminé. La dernière étape consiste indexà modifier la vue pour utiliser le modèle index.html.



Éditons 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,
    )


Terminé.



Assurez-vous que l'application est en cours d'exécution ou exécutée flask runet ouvrez http://127.0.0.1:5000/ .



Tu devrais voir:







Connecter Github



Dans cette section, nous intégrerons notre application à l'API Github.

Nous utiliserons la bibliothèque PyGithub .



Ajoutons-le Ă  requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub


et exécutez dans le terminal:



pip install --upgrade -r requirements.txt


Nous devons maintenant ajouter le client API Github au conteneur. Pour ce faire, nous devrons utiliser deux nouveaux fournisseurs du module dependency_injector.providers:



  • Le fournisseur FactorycrĂ©era le client Github.
  • Le fournisseur Configurationtransmettra le jeton d'API et le dĂ©lai d'expiration Github au client.


Faisons le.



Éditons 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)


Nous avons utilisé les paramètres de configuration avant de définir leurs valeurs. C'est le principe selon lequel le fournisseur fonctionne Configuration.



Nous utilisons d'abord, puis nous définissons les valeurs.



Ajoutons maintenant le fichier de configuration.

Nous utiliserons YAML.



Créez un fichier vide config.ymlà la racine du projet:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


Et remplissez-le avec les lignes suivantes:



github:
  request_timeout: 10


Pour travailler avec le fichier de configuration, nous utiliserons la bibliothèque PyYAML . Ajoutons-le au fichier avec les dépendances.



Modifier requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml


et installez la dépendance:



pip install --upgrade -r requirements.txt


Nous utiliserons une variable d'environnement pour transmettre le jeton API GITHUB_TOKEN.



Maintenant, nous devons éditer create_app()pour faire 2 actions lorsque l'application démarre:



  • Charger la configuration depuis config.yml
  • Charger le jeton d'API Ă  partir de la variable d'environnement GITHUB_TOKEN


Modifier 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


Nous devons maintenant créer un jeton API.



Pour cela, vous avez besoin de:



  • Suivez ce tutoriel sur Github
  • DĂ©finissez le jeton sur la variable d'environnement:



    export GITHUB_TOKEN=<your token>


Cet élément peut être temporairement ignoré.



L'application fonctionnera sans jeton, mais avec une bande passante limitée. Limite pour les clients non authentifiés: 60 requêtes par heure. Le jeton est nécessaire pour augmenter ce quota à 5000 par heure.


Terminé.



L'installation de l'API client Github est terminée.



Service de recherche



Il est temps d'ajouter un service de recherche SearchService. Il sera:



  • Rechercher sur Github
  • Obtenez des donnĂ©es supplĂ©mentaires sur les commits
  • Convertir le rĂ©sultat du format


SearchServiceutilisera le client API Github.



Créez un fichier vide services.pydans le package githubnavigator:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   ├── services.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


et ajoutez-y les lignes suivantes:



"""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,
        }


Maintenant, ajoutons SearchServiceau conteneur.



Modifier 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)


Connecter la recherche



Nous sommes maintenant prĂŞts pour que la recherche fonctionne. Utilisons SearchServiceen indexvue.



Modifier 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,
    )


Modifions maintenant le conteneur pour transmettre la dépendance SearchServiceà la vue indexlorsqu'elle est appelée.



Modifier 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,
    )


Assurez-vous que l'application est en cours d'exécution ou exécutée flask runet ouvrez http://127.0.0.1:5000/ .



Tu verras:







Un peu de refactoring



Notre vue indexcontient deux valeurs codées en dur:



  • Terme de recherche par dĂ©faut
  • Limite du nombre de rĂ©sultats


Faisons un peu de refactoring. Nous transférerons ces valeurs dans la configuration.



Modifier 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,
    )


Maintenant, nous avons besoin que ces valeurs soient transmises sur appel. Mettons Ă  jour le conteneur.



Modifier 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,
    )


Maintenant, mettons Ă  jour le fichier de configuration.



Modifier config.yml:



github:
  request_timeout: 10
search:
  default_query: "Dependency Injector"
  default_limit: 10


Terminé.



Le refactoring est terminé. Mu a rendu le code plus propre.



Ajouter des tests



Ce serait bien d'ajouter quelques tests. Faisons le.



Nous utiliserons pytest et la couverture .



Modifier requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov


et installez de nouveaux packages:



pip install -r requirements.txt


Créez un fichier vide tests.pydans le package githubnavigator:



./
├── githubnavigator/
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   ├── __init__.py
│   ├── application.py
│   ├── containers.py
│   ├── services.py
│   ├── tests.py
│   └── views.py
├── venv/
├── config.yml
└── requirements.txt


et ajoutez-y les lignes suivantes:



"""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


Commençons maintenant les tests et vérifions la couverture:



py.test githubnavigator/tests.py --cov=githubnavigator


Tu verras:



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%


Remarquez comment nous remplaçons github_clientpar simulacre en utilisant la méthode .override(). De cette façon, vous pouvez remplacer la valeur de retour de n'importe quel fournisseur.



Conclusion



Nous avons construit notre application Flask en utilisant l'injection de dépendances. Nous avons utilisé Dependency Injector comme cadre d'injection de dépendances.



La partie principale de notre application est le conteneur. Il contient tous les composants de l'application et leurs dépendances en un seul endroit. Cela permet de contrôler la structure de l'application. Il est facile de comprendre et de changer:



"""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 conteneur comme carte de votre application. Vous savez toujours ce qui dépend de quoi.



Et après?






All Articles