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