mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-01-30 19:24:31 +03:00
Add flask blueprints example
This commit is contained in:
parent
ae3024588c
commit
ca671abea6
100
examples/miniapps/flask-blueprints/README.rst
Normal file
100
examples/miniapps/flask-blueprints/README.rst
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
Flask Blueprints + Dependency Injector Example
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
This is a `Flask <https://flask.palletsprojects.com/>`_ Blueprints +
|
||||||
|
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||||
|
|
||||||
|
The example application helps to search for repositories on the Github.
|
||||||
|
|
||||||
|
.. image:: screenshot.png
|
||||||
|
|
||||||
|
Run
|
||||||
|
---
|
||||||
|
|
||||||
|
Create virtual environment:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
virtualenv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
|
Install requirements:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
To run the application do:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export FLASK_APP=githubnavigator.application
|
||||||
|
export FLASK_ENV=development
|
||||||
|
flask run
|
||||||
|
|
||||||
|
The output should be something like:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
After that visit http://127.0.0.1:5000/ in your browser.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Github has a rate limit. When the rate limit is exceed you will see an exception
|
||||||
|
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
|
||||||
|
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
|
||||||
|
need to set personal access token.
|
||||||
|
|
||||||
|
It's easy:
|
||||||
|
|
||||||
|
- Follow this `guide <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`_ to create a token.
|
||||||
|
- Set a token to the environment variable:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export GITHUB_TOKEN=<your token>
|
||||||
|
|
||||||
|
- Restart the app with ``flask run``
|
||||||
|
|
||||||
|
`Read more on Github rate limit <https://developer.github.com/v3/#rate-limiting>`_
|
||||||
|
|
||||||
|
Test
|
||||||
|
----
|
||||||
|
|
||||||
|
This application comes with the unit tests.
|
||||||
|
|
||||||
|
To run the tests do:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
py.test githubnavigator/tests.py --cov=githubnavigator
|
||||||
|
|
||||||
|
The output should be something like:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
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 15 0 100%
|
||||||
|
githubnavigator/blueprints/example.py 12 0 100%
|
||||||
|
githubnavigator/containers.py 7 0 100%
|
||||||
|
githubnavigator/services.py 14 0 100%
|
||||||
|
githubnavigator/tests.py 34 0 100%
|
||||||
|
-----------------------------------------------------------
|
||||||
|
TOTAL 82 0 100%
|
5
examples/miniapps/flask-blueprints/config.yml
Normal file
5
examples/miniapps/flask-blueprints/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
github:
|
||||||
|
request_timeout: 10
|
||||||
|
default:
|
||||||
|
query: "Dependency Injector"
|
||||||
|
limit: 10
|
|
@ -0,0 +1 @@
|
||||||
|
"""Top-level package."""
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""Application module."""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
|
|
||||||
|
from .containers import Container
|
||||||
|
from .blueprints import example
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> Flask:
|
||||||
|
container = Container()
|
||||||
|
container.config.from_yaml('config.yml')
|
||||||
|
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||||
|
container.wire(modules=[example])
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.container = container
|
||||||
|
app.register_blueprint(example.blueprint)
|
||||||
|
|
||||||
|
bootstrap = Bootstrap()
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
|
||||||
|
return app
|
|
@ -0,0 +1 @@
|
||||||
|
"""Blueprints package."""
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Example blueprint."""
|
||||||
|
|
||||||
|
from flask import Blueprint, request, render_template
|
||||||
|
from dependency_injector.wiring import inject, Provide
|
||||||
|
|
||||||
|
from githubnavigator.services import SearchService
|
||||||
|
from githubnavigator.containers import Container
|
||||||
|
|
||||||
|
|
||||||
|
blueprint = Blueprint('example', __name__, template_folder='templates/')
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/')
|
||||||
|
@inject
|
||||||
|
def index(
|
||||||
|
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,
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Containers module."""
|
||||||
|
|
||||||
|
from dependency_injector import containers, providers
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
from . import services
|
||||||
|
|
||||||
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""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,
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,70 @@
|
||||||
|
{% 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 %}
|
71
examples/miniapps/flask-blueprints/githubnavigator/tests.py
Normal file
71
examples/miniapps/flask-blueprints/githubnavigator/tests.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
"""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():
|
||||||
|
app = create_app()
|
||||||
|
yield app
|
||||||
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
|
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('example.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('example.index'))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Results found: 0' in response.data
|
7
examples/miniapps/flask-blueprints/requirements.txt
Normal file
7
examples/miniapps/flask-blueprints/requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
dependency-injector
|
||||||
|
flask
|
||||||
|
bootstrap-flask
|
||||||
|
pygithub
|
||||||
|
pyyaml
|
||||||
|
pytest-flask
|
||||||
|
pytest-cov
|
BIN
examples/miniapps/flask-blueprints/screenshot.png
Normal file
BIN
examples/miniapps/flask-blueprints/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 647 KiB |
Loading…
Reference in New Issue
Block a user