Flask tutorial

This tutorial shows how to build aFlask application following the dependency injectionprinciple.

Start from the scratch or jump to the section:

You can find complete project on theGithub.

What are we going to build?

We will build a web application that helps to search for repositories on the Github. Let’s call itGithub 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 pageon the Github.

../_images/screen-02.png

Prepare the environment

Let’s create the environment for the project.

First we need to create a project folder:

mkdirghnav-flask-tutorialcdghnav-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 installFlask andDependencyInjector.

Put next lines into therequirements.txt file:

dependency-injectorflask

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 theviews.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.

Editcontainers.py:

"""Containers module."""fromdependency_injectorimportcontainersclassContainer(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 containerand Flask application. It is traditionally calledcreate_app().We will assignindex view to handle user requests to the root/ of our web application.

Put next into theapplication.py:

"""Application module."""fromflaskimportFlaskfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)returnapp

Ok. Now we’re ready to say “Hello, World!”.

Do next in the terminal:

exportFLASK_APP=githubnavigator.applicationexportFLASK_ENV=developmentflaskrun

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 thehttp://127.0.0.1:5000/.

You should seeHello,World!.

That’s it. Our minimal application is up and running.

Make it pretty

Now let’s make it look pretty. We will useBootstrap 4.For adding it to our application we will getBootstrap-Flask extension.It will help us to add all needed static files in few clicks.

Addbootstrap-flask to therequirements.txt:

dependency-injectorflaskbootstrap-flask

and run in the terminal:

pipinstall-rrequirements.txt

Let’s initializebootstrap-flask extension. We will need to modifycreate_app().

Editapplication.py:

"""Application module."""fromflaskimportFlaskfromflask_bootstrapimportBootstrapfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)bootstrap=Bootstrap()bootstrap.init_app(app)returnapp

Now we need to add the templates. For doing this we will need to add the foldertemplates/ tothegithubnavigator package. We also will need two files there:

  • base.html - the layout

  • index.html - the main page

Createtemplates folder and put two empty files into itbase.html andindex.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 thebase.html:

<!doctype html><html lang="en">    <head>{%blockhead%}        <!-- Required meta tags -->        <meta charset="utf-8">        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">{%blockstyles%}            <!-- Bootstrap CSS -->{{bootstrap.load_css()}}{%endblock%}        <title>{%blocktitle%}{%endblock%}</title>{%endblock%}    </head>    <body>        <!-- Your page content -->{%blockcontent%}{%endblock%}{%blockscripts%}            <!-- Optional JavaScript -->{{bootstrap.load_js()}}{%endblock%}    </body></html>

And put something to the index page.

Put next into theindex.html:

{%extends"base.html"%}{%blocktitle%}Github Navigator{%endblock%}{%blockcontent%}<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="{{queryifquery}}">            </div>            <div class="col">                <label for="search_limit" class="col-form-label">                    Limit:                </label>                <select class="form-control" id="search_limit" name="limit">{%forvaluein[5,10,20]%}                    <option{%ifvalue==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>{%forrepositoryinrepositories%}{{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 makeindex view to render theindex.html template.

Editviews.py:

"""Views module."""fromflaskimportrequest,render_templatedefindex():query=request.args.get("query","Dependency Injector")limit=request.args.get("limit",10,int)repositories=[]returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)

That’s it.

Make sure the app is running or useflaskrun and openhttp://127.0.0.1:5000/.

You should see:

../_images/screen-01.png

Connect to the GitHub

In this section we will integrate our application with Github API.

We will usePyGithub library for working with Github API.

Let’s add it to therequirements.txt:

dependency-injectorflaskbootstrap-flaskpygithub

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 fromthedependency_injector.providers module:

  • Factory provider. It will create aGithub client.

  • Configuration provider. It will provide an API token and a request timeout for theGithub client.We will specify the location of the configuration file. The configuration provider will parsethe configuration file when we create a container instance.

Editcontainers.py:

"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubclassContainer(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 anymoresince we container is not empty.

Now let’s add the configuration file. We will use YAML. Create an empty fileconfig.ymlin 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 usePyYAML library for parsing the configurationfile. Let’s add it to the requirements file.

Editrequirements.txt:

dependency-injectorflaskbootstrap-flaskpygithubpyyaml

and install it:

pipinstall-rrequirements.txt

We will use theGITHUB_TOKEN environment variable to provide the API token. Let’s editcreate_app() to fetch the token value from it.

Editapplication.py:

"""Application module."""fromflaskimportFlaskfromflask_bootstrapimportBootstrapfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()container.config.github.auth_token.from_env("GITHUB_TOKEN")app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)bootstrap=Bootstrap()bootstrap.init_app(app)returnapp

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 theGithub 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 addSearchService. It will:

  • Perform the search.

  • Fetch commit extra data for each result.

  • Format result data.

SearchService will useGithub API client.

Create empty fileservices.py in thegithubnavigator 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."""fromgithubimportGithubfromgithub.RepositoryimportRepositoryfromgithub.CommitimportCommitclassSearchService:"""Search service performs search on Github."""def__init__(self,github_client:Github):self._github_client=github_clientdefsearch_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)forrepositoryinrepositories[: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])ifcommitselse{},}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 addSearchService to the container.

Editcontainers.py:

"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubfrom.importservicesclassContainer(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 injectSearchService into theindex view. We will useWiring feature.

Editviews.py:

"""Views module."""fromflaskimportrequest,render_templatefromdependency_injector.wiringimportinject,Providefrom.servicesimportSearchServicefrom.containersimportContainer@injectdefindex(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)returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)

To make the injection work we need to wire the container with theviews module.Let’s configure the container to automatically make wiring with theviews module when wecreate a container instance.

Editcontainers.py:

"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubfrom.importservicesclassContainer(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 useflaskrun and openhttp://127.0.0.1:5000/.

You should see:

../_images/screen-02.png

Make some refactoring

Ourindex 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.

Editviews.py:

"""Views module."""fromflaskimportrequest,render_templatefromdependency_injector.wiringimportinject,Providefrom.servicesimportSearchServicefrom.containersimportContainer@injectdefindex(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)returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)

Editconfig.yml:

github:request_timeout:10default: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 usepytest with its Flask extension andcoverage.

Editrequirements.txt:

dependency-injectorflaskbootstrap-flaskpygithubpyyamlpytest-flaskpytest-cov

And install added packages:

pipinstall-rrequirements.txt

Create empty filetests.py in thegithubnavigator 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."""fromunittestimportmockimportpytestfromgithubimportGithubfromflaskimporturl_forfrom.applicationimportcreate_app@pytest.fixturedefapp():app=create_app()yieldappapp.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()]),),]withapp.container.github_client.override(github_client_mock):response=client.get(url_for("index"))assertresponse.status_code==200assertb"Results found: 2"inresponse.dataassertb"repo1-url"inresponse.dataassertb"repo1-name"inresponse.dataassertb"owner1-login"inresponse.dataassertb"owner1-url"inresponse.dataassertb"owner1-avatar-url"inresponse.dataassertb"repo2-url"inresponse.dataassertb"repo2-name"inresponse.dataassertb"owner2-login"inresponse.dataassertb"owner2-url"inresponse.dataassertb"owner2-avatar-url"inresponse.datadeftest_index_no_results(client,app):github_client_mock=mock.Mock(spec=Github)github_client_mock.search_repositories.return_value=[]withapp.container.github_client.override(github_client_mock):response=client.get(url_for("index"))assertresponse.status_code==200assertb"Results found: 0"inresponse.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.0plugins:cov-3.0.0,flask-1.2.0collected2itemsgithubnavigator/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 thetests.py.

It emphasizes the overriding of theGithub API client.

Conclusion

In this tutorial we’ve built aFlask application following the dependency injection principle.We’ve used theDependencyInjector as a dependency injection framework.

Containers andProviders helped to specify how to assemble search service andintegrate it with a 3rd-party library.

Configuration provider helped to deal with reading YAML file and environment variable.

We usedWiring feature to inject the dependencies into theindex() view.Provider overriding feature helped in testing.

We kept all the dependencies injected explicitly. This will help when you need to add orchange something in future.

You can find complete project on theGithub.

What’s next?

Sponsor the project on GitHub: