diff --git a/examples/miniapps/flask-blueprints/README.rst b/examples/miniapps/flask-blueprints/README.rst new file mode 100644 index 00000000..355fc2af --- /dev/null +++ b/examples/miniapps/flask-blueprints/README.rst @@ -0,0 +1,100 @@ +Flask Blueprints + Dependency Injector Example +============================================== + +This is a `Flask `_ Blueprints + +`Dependency Injector `_ 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 `_ to create a token. + - Set a token to the environment variable: + + .. code-block:: bash + + export GITHUB_TOKEN= + + - Restart the app with ``flask run`` + + `Read more on Github rate limit `_ + +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% diff --git a/examples/miniapps/flask-blueprints/config.yml b/examples/miniapps/flask-blueprints/config.yml new file mode 100644 index 00000000..938c609a --- /dev/null +++ b/examples/miniapps/flask-blueprints/config.yml @@ -0,0 +1,5 @@ +github: + request_timeout: 10 +default: + query: "Dependency Injector" + limit: 10 diff --git a/examples/miniapps/flask-blueprints/githubnavigator/__init__.py b/examples/miniapps/flask-blueprints/githubnavigator/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/flask-blueprints/githubnavigator/application.py b/examples/miniapps/flask-blueprints/githubnavigator/application.py new file mode 100644 index 00000000..eabf8eed --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/application.py @@ -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 diff --git a/examples/miniapps/flask-blueprints/githubnavigator/blueprints/__init__.py b/examples/miniapps/flask-blueprints/githubnavigator/blueprints/__init__.py new file mode 100644 index 00000000..d6dec51f --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/blueprints/__init__.py @@ -0,0 +1 @@ +"""Blueprints package.""" diff --git a/examples/miniapps/flask-blueprints/githubnavigator/blueprints/example.py b/examples/miniapps/flask-blueprints/githubnavigator/blueprints/example.py new file mode 100644 index 00000000..48a30b7b --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/blueprints/example.py @@ -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, + ) diff --git a/examples/miniapps/flask-blueprints/githubnavigator/containers.py b/examples/miniapps/flask-blueprints/githubnavigator/containers.py new file mode 100644 index 00000000..d2a0e7ca --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/containers.py @@ -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, + ) diff --git a/examples/miniapps/flask-blueprints/githubnavigator/services.py b/examples/miniapps/flask-blueprints/githubnavigator/services.py new file mode 100644 index 00000000..9c6ff839 --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/services.py @@ -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, + } diff --git a/examples/miniapps/flask-blueprints/githubnavigator/templates/base.html b/examples/miniapps/flask-blueprints/githubnavigator/templates/base.html new file mode 100644 index 00000000..2797916a --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/templates/base.html @@ -0,0 +1,26 @@ + + + + {% block head %} + + + + + {% block styles %} + + {{ bootstrap.load_css() }} + {% endblock %} + + {% block title %}{% endblock %} + {% endblock %} + + + + {% block content %}{% endblock %} + + {% block scripts %} + + {{ bootstrap.load_js() }} + {% endblock %} + + diff --git a/examples/miniapps/flask-blueprints/githubnavigator/templates/index.html b/examples/miniapps/flask-blueprints/githubnavigator/templates/index.html new file mode 100644 index 00000000..c80c70ac --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/templates/index.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %}Github Navigator{% endblock %} + +{% block content %} +
+

Github Navigator

+ +
+
+
+ + +
+
+ + +
+
+
+ +

Results found: {{ repositories|length }}

+ + + + + + + + + + + + {% for repository in repositories %} {{n}} + + + + + + + {% endfor %} + +
#RepositoryRepository ownerLast commit
{{ loop.index }} + {{ repository.name }} + + avatar + + {{ repository.owner.login }} + + {{ repository.latest_commit.sha }} + {{ repository.latest_commit.message }} + {{ repository.latest_commit.author_name }} +
+
+ +{% endblock %} diff --git a/examples/miniapps/flask-blueprints/githubnavigator/tests.py b/examples/miniapps/flask-blueprints/githubnavigator/tests.py new file mode 100644 index 00000000..ddf4c256 --- /dev/null +++ b/examples/miniapps/flask-blueprints/githubnavigator/tests.py @@ -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 diff --git a/examples/miniapps/flask-blueprints/requirements.txt b/examples/miniapps/flask-blueprints/requirements.txt new file mode 100644 index 00000000..78a650f6 --- /dev/null +++ b/examples/miniapps/flask-blueprints/requirements.txt @@ -0,0 +1,7 @@ +dependency-injector +flask +bootstrap-flask +pygithub +pyyaml +pytest-flask +pytest-cov diff --git a/examples/miniapps/flask-blueprints/screenshot.png b/examples/miniapps/flask-blueprints/screenshot.png new file mode 100644 index 00000000..350aaa67 Binary files /dev/null and b/examples/miniapps/flask-blueprints/screenshot.png differ