Flask tutorial¶
This tutorial shows how to build a Flask application following the dependency injection
principle.
Start from the scratch or jump to the section:
You can find complete project on the Github.
What are we going to build?¶
We will build a web application that helps to search for repositories on the Github. Let’s call it Github Navigator.
How does Github Navigator work?
User opens a web page that asks to provide a search query.
User types the query and hits Enter.
Github Navigator takes that and searches through the Github for matching repositories.
When search is done Github Navigator returns user a web page with the result.
The results page shows all matching repositories and the provided search query.
- For any matching repository user sees:
the repository name
the owner of the repository
the last commit to the repository
User can click on the repository, the repository owner or the last commit to open its web page on the Github.
Prepare the environment¶
Let’s create the environment for the project.
First we need to create a project folder:
mkdirghnav-flask-tutorial
cdghnav-flask-tutorial
Now let’s create and activate virtual environment:
python3-mvenvvenv .venv/bin/activate
Project layout¶
Environment is ready and now we’re going to create the layout of the project.
Create next structure in the current directory. All files should be empty. That’s ok for now.
Initial project layout:
./ ├── githubnavigator/ │ ├── __init__.py │ ├── application.py │ ├── containers.py │ └── views.py ├── venv/ └── requirements.txt
Now it’s time to install Flask and Dependency Injector.
Put next lines into the requirements.txt file:
dependency-injector flask
Now let’s install it:
pipinstall-rrequirements.txt
And check that installation is successful:
python-c"import dependency_injector; print(dependency_injector.__version__)" python-c"import flask; print(flask.__version__)"
You should see something like:
(venv)$python-c"import dependency_injector; print(dependency_injector.__version__)" 4.37.0 (venv)$python-c"import flask; print(flask.__version__)" 2.0.2
Versions can be different. That’s fine.
Hello World!¶
Let’s create minimal application.
Put next into the views.py:
"""Views module.""" defindex(): return "Hello, World!"
Ok, we have the view.
Now let’s create a container. Container will keep all of the application components and their dependencies.
Edit containers.py:
"""Containers module.""" fromdependency_injectorimport containers classContainer(containers.DeclarativeContainer): ...
Container is empty for now. We will add the providers in the following sections.
Finally we need to create Flask application factory. It will create and configure container
and Flask application. It is traditionally called create_app().
We will assign index view to handle user requests to the root / of our web application.
Put next into the application.py:
"""Application module.""" fromflaskimport Flask from.containersimport Container from.import views defcreate_app() -> Flask: container = Container() app = Flask(__name__) app.container = container app.add_url_rule("/", "index", views.index) return app
Ok. Now we’re ready to say "Hello, World!".
Do next in the terminal:
exportFLASK_APP=githubnavigator.application exportFLASK_ENV=development flaskrun
The output should be something like:
*ServingFlaskapp"githubnavigator.application"(lazyloading) *Environment:development *Debugmode:on *Runningonhttp://127.0.0.1:5000/(PressCTRL+Ctoquit) *Restartingwithfseventsreloader *Debuggerisactive! *DebuggerPIN:473-587-859
Open your browser and go to the http://127.0.0.1:5000/.
You should see Hello, World!.
That’s it. Our minimal application is up and running.
Make it pretty¶
Now let’s make it look pretty. We will use Bootstrap 4. For adding it to our application we will get Bootstrap-Flask extension. It will help us to add all needed static files in few clicks.
Add bootstrap-flask to the requirements.txt:
dependency-injector
flask
bootstrap-flask
and run in the terminal:
pipinstall-rrequirements.txt
Let’s initialize bootstrap-flask extension. We will need to modify create_app().
Edit application.py:
"""Application module.""" fromflaskimport Flask fromflask_bootstrapimport Bootstrap from.containersimport Container from.import views defcreate_app() -> Flask: container = Container() app = Flask(__name__) app.container = container app.add_url_rule("/", "index", views.index) bootstrap = Bootstrap() bootstrap.init_app(app) return app
Now we need to add the templates. For doing this we will need to add the folder templates/ to
the githubnavigator package. We also will need two files there:
base.html- the layoutindex.html- the main page
Create templates folder and put two empty files into it base.html and index.html:
./ ├──githubnavigator/ │├──templates/ ││├──base.html ││└──index.html │├──__init__.py │├──application.py │├──containers.py │└──views.py ├──venv/ └──requirements.txt
Now let’s fill in the layout.
Put next into the 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>
And put something to the index page.
Put next into the 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 %}
Ok, almost there. The last step is to make index view to render the index.html template.
Edit views.py:
"""Views module.""" fromflaskimport request, render_template defindex(): 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, )
That’s it.
Make sure the app is running or use flask run and open http://127.0.0.1:5000/.
You should see:
../_images/screen-01.pngConnect to the GitHub¶
In this section we will integrate our application with Github API.
We will use PyGithub library for working with Github API.
Let’s add it to the requirements.txt:
dependency-injector
flask
bootstrap-flask
pygithub
and run in the terminal:
pipinstall-rrequirements.txt
Now we need to add Github API client the container. We will need to add two more providers from
the dependency_injector.providers module:
Factoryprovider. It will create aGithubclient.Configurationprovider. It will provide an API token and a request timeout for theGithubclient. We will specify the location of the configuration file. The configuration provider will parse the configuration file when we create a container instance.
Edit containers.py:
"""Containers module.""" fromdependency_injectorimport containers, providers fromgithubimport Github classContainer(containers.DeclarativeContainer): config = providers.Configuration(yaml_files=["config.yml"]) github_client = providers.Factory( Github, login_or_token=config.github.auth_token, timeout=config.github.request_timeout, )
Note
Don’t forget to remove the Ellipsis ... from the container. We don’t need it anymore
since we container is not empty.
Now let’s add the configuration file. We will use YAML. Create an empty file config.yml
in the root of the project:
./
├──githubnavigator/
│├──templates/
││├──base.html
││└──index.html
│├──__init__.py
│├──application.py
│├──containers.py
│└──views.py
├──venv/
├──config.yml
└──requirements.txt
and put next into it:
github: request_timeout:10
We will use PyYAML library for parsing the configuration file. Let’s add it to the requirements file.
Edit requirements.txt:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
and install it:
pipinstall-rrequirements.txt
We will use the GITHUB_TOKEN environment variable to provide the API token. Let’s edit
create_app() to fetch the token value from it.
Edit application.py:
"""Application module.""" fromflaskimport Flask fromflask_bootstrapimport Bootstrap from.containersimport Container from.import views defcreate_app() -> Flask: container = Container() container.config.github.auth_token.from_env("GITHUB_TOKEN") app = Flask(__name__) app.container = container app.add_url_rule("/", "index", views.index) bootstrap = Bootstrap() bootstrap.init_app(app) return app
Now we need create an API token.
As for now, don’t worry, just take this one:
exportGITHUB_TOKEN=cbde697a6e01424856fde2b7f94a88d1b501320e
Note
To create your own token:
Follow the Github guide.
Set the token to the environment variable:
exportGITHUB_TOKEN=<yourtoken>
That’s it.
Github API client setup is done.
Search service¶
Now it’s time to add SearchService. It will:
Perform the search.
Fetch commit extra data for each result.
Format result data.
SearchService will use Github API client.
Create empty file services.py in the githubnavigator package:
./
├──githubnavigator/
│├──templates/
││├──base.html
││└──index.html
│├──__init__.py
│├──application.py
│├──containers.py
│├──services.py
│└──views.py
├──venv/
├──config.yml
└──requirements.txt
and put next into it:
"""Services module.""" fromgithubimport Github fromgithub.Repositoryimport Repository fromgithub.Commitimport Commit classSearchService: """Search service performs search on Github.""" def__init__(self, github_client: Github): self._github_client = github_client defsearch_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, }
Now let’s add SearchService to the container.
Edit containers.py:
"""Containers module.""" fromdependency_injectorimport containers, providers fromgithubimport Github from.import services classContainer(containers.DeclarativeContainer): config = providers.Configuration(yaml_files=["config.yml"]) 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, )
Inject search service into view¶
Now we are ready to make the search work.
Let’s inject SearchService into the index view. We will use Wiring feature.
Edit views.py:
"""Views module.""" fromflaskimport request, render_template fromdependency_injector.wiringimport inject, Provide from.servicesimport SearchService from.containersimport Container @inject defindex(search_service: SearchService = Provide[Container.search_service]): 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, )
To make the injection work we need to wire the container with the views module.
Let’s configure the container to automatically make wiring with the views module when we
create a container instance.
Edit containers.py:
"""Containers module.""" fromdependency_injectorimport containers, providers fromgithubimport Github from.import services classContainer(containers.DeclarativeContainer): wiring_config = containers.WiringConfiguration(modules=[".views"]) config = providers.Configuration(yaml_files=["config.yml"]) 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, )
Make sure the app is running or use flask run and open http://127.0.0.1:5000/.
You should see:
../_images/screen-02.pngMake some refactoring¶
Our index view has two hardcoded config values:
Default search query
Default results limit
Let’s make some refactoring. We will move these values to the config.
Edit views.py:
"""Views module.""" fromflaskimport request, render_template fromdependency_injector.wiringimport inject, Provide from.servicesimport SearchService from.containersimport Container @inject defindex( search_service: SearchService = Provide[Container.search_service], default_query: str = Provide[Container.config.default.query], default_limit: int = Provide[Container.config.default.limit.as_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, )
Edit config.yml:
github: request_timeout:10 default: query:"DependencyInjector" limit:10
That’s it. The refactoring is done. We’ve made it cleaner.
Tests¶
In this section we will add some tests.
We will use pytest with its Flask extension and coverage.
Edit requirements.txt:
dependency-injector flask bootstrap-flask pygithub pyyaml pytest-flask pytest-cov
And install added packages:
pipinstall-rrequirements.txt
Create empty file tests.py in the githubnavigator package:
./
├──githubnavigator/
│├──templates/
││├──base.html
││└──index.html
│├──__init__.py
│├──application.py
│├──containers.py
│├──services.py
│├──tests.py
│└──views.py
├──venv/
├──config.yml
└──requirements.txt
and put next into it:
"""Tests module.""" fromunittestimport mock importpytest fromgithubimport Github fromflaskimport url_for from.applicationimport create_app @pytest.fixture defapp(): app = create_app() yield app app.container.unwire() deftest_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 deftest_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
Now let’s run it and check the coverage:
py.testgithubnavigator/tests.py--cov=githubnavigator
You should see:
platformdarwin--Python3.10.0,pytest-6.2.5,py-1.10.0,pluggy-1.0.0 plugins:cov-3.0.0,flask-1.2.0 collected2items githubnavigator/tests.py..[100%] ----------coverage:platformdarwin,python3.10.0-final-0---------- NameStmtsMissCover ---------------------------------------------------- githubnavigator/__init__.py00100% githubnavigator/application.py130100% githubnavigator/containers.py80100% githubnavigator/services.py140100% githubnavigator/tests.py340100% githubnavigator/views.py100100% ---------------------------------------------------- TOTAL790100%
Note
Take a look at the highlights in the tests.py.
It emphasizes the overriding of the Github API client.
Conclusion¶
In this tutorial we’ve built a Flask application following the dependency injection principle.
We’ve used the Dependency Injector as a dependency injection framework.
Containers and Providers helped to specify how to assemble search service and integrate it with a 3rd-party library.
Configuration provider helped to deal with reading YAML file and environment variable.
We used Wiring feature to inject the dependencies into the index() view.
Provider overriding feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add or change something in future.
You can find complete project on the Github.
What’s next?
Sponsor the project on GitHub:
[フレーム]