diff --git a/examples/miniapps/django/.gitignore b/examples/miniapps/django/.gitignore new file mode 100644 index 00000000..60615839 --- /dev/null +++ b/examples/miniapps/django/.gitignore @@ -0,0 +1 @@ +*.sqlite3 diff --git a/examples/miniapps/django/.pydocstylerc b/examples/miniapps/django/.pydocstylerc new file mode 100644 index 00000000..287c7066 --- /dev/null +++ b/examples/miniapps/django/.pydocstylerc @@ -0,0 +1,2 @@ +[pydocstyle] +ignore = D100,D101,D102,D103,D105,D107,D203,D212,D213,D400,D406,D407,D411,D413,D415 diff --git a/examples/miniapps/django/README.rst b/examples/miniapps/django/README.rst new file mode 100644 index 00000000..2f43d9b9 --- /dev/null +++ b/examples/miniapps/django/README.rst @@ -0,0 +1,111 @@ +Django + Dependency Injector Example +==================================== + +Application ``githubnavigator`` is a `Django `_ + +`Dependency Injector `_ example application. + +.. 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 + +Run migrations: + +.. code-block:: bash + + python manage.py migrate + +To run the application do: + +.. code-block:: bash + + python manage.py runserver + +The output should be something like: + +.. code-block:: + + Watching for file changes with StatReloader + Performing system checks... + + System check identified no issues (0 silenced). + October 05, 2020 - 03:17:05 + Django version 3.1.2, using settings 'githubnavigator.settings' + Starting development server at http://127.0.0.1:8000/ + Quit the server with CONTROL-C. + +After that visit http://127.0.0.1:8000/ 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 ``python manage.py runserver`` + + `Read more on Github rate limit `_ + +Test +---- + +This application comes with the unit tests. + +To run the tests do: + +.. code-block:: bash + + coverage run --source='.' manage.py test && coverage report + +The output should be something like: + +.. code-block:: + + Creating test database for alias 'default'... + System check identified no issues (0 silenced). + .. + ---------------------------------------------------------------------- + Ran 2 tests in 0.037s + + OK + Destroying test database for alias 'default'... + Name Stmts Miss Cover + --------------------------------------------------- + githubnavigator/__init__.py 4 0 100% + githubnavigator/asgi.py 4 4 0% + githubnavigator/containers.py 7 0 100% + githubnavigator/services.py 14 0 100% + githubnavigator/settings.py 23 0 100% + githubnavigator/urls.py 3 0 100% + githubnavigator/wsgi.py 4 4 0% + manage.py 12 2 83% + web/__init__.py 0 0 100% + web/apps.py 7 0 100% + web/tests.py 28 0 100% + web/urls.py 3 0 100% + web/views.py 11 0 100% + --------------------------------------------------- + TOTAL 120 10 92% diff --git a/examples/miniapps/django/githubnavigator/__init__.py b/examples/miniapps/django/githubnavigator/__init__.py new file mode 100644 index 00000000..7421382f --- /dev/null +++ b/examples/miniapps/django/githubnavigator/__init__.py @@ -0,0 +1,8 @@ +"""Main package.""" + +from .containers import Container +from . import settings + + +container = Container() +container.config.from_dict(settings.__dict__) diff --git a/examples/miniapps/django/githubnavigator/asgi.py b/examples/miniapps/django/githubnavigator/asgi.py new file mode 100644 index 00000000..b1f6edc7 --- /dev/null +++ b/examples/miniapps/django/githubnavigator/asgi.py @@ -0,0 +1,15 @@ +"""ASGI config for githubnavigator project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings') + +application = get_asgi_application() diff --git a/examples/miniapps/django/githubnavigator/containers.py b/examples/miniapps/django/githubnavigator/containers.py new file mode 100644 index 00000000..99b7eafc --- /dev/null +++ b/examples/miniapps/django/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_TOKEN, + timeout=config.GITHUB_REQUEST_TIMEOUT, + ) + + search_service = providers.Factory( + services.SearchService, + github_client=github_client, + ) diff --git a/examples/miniapps/django/githubnavigator/services.py b/examples/miniapps/django/githubnavigator/services.py new file mode 100644 index 00000000..9c6ff839 --- /dev/null +++ b/examples/miniapps/django/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/django/githubnavigator/settings.py b/examples/miniapps/django/githubnavigator/settings.py new file mode 100644 index 00000000..1d7985dc --- /dev/null +++ b/examples/miniapps/django/githubnavigator/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for githubnavigator project. + +Generated by 'django-admin startproject' using Django 3.0.8. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ')6*iyg26c9l!fvyvwd&3+vyf-dcw)e=5x2t(j)(*c29z@ykhi0' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'web.apps.WebConfig', + 'bootstrap4', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'githubnavigator.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'githubnavigator.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = '/static/' + +# Github client settings +GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') +GITHUB_REQUEST_TIMEOUT = 10 + +# Search settings +DEFAULT_LIMIT = 5 +DEFAULT_QUERY = 'Dependency Injector' +LIMITS = [5, 10, 20] diff --git a/examples/miniapps/django/githubnavigator/urls.py b/examples/miniapps/django/githubnavigator/urls.py new file mode 100644 index 00000000..5e88d78f --- /dev/null +++ b/examples/miniapps/django/githubnavigator/urls.py @@ -0,0 +1,22 @@ +"""githubnavigator URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('', include('web.urls')), + path('admin/', admin.site.urls), +] diff --git a/examples/miniapps/django/githubnavigator/wsgi.py b/examples/miniapps/django/githubnavigator/wsgi.py new file mode 100644 index 00000000..a13cd5f9 --- /dev/null +++ b/examples/miniapps/django/githubnavigator/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for githubnavigator project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings') + +application = get_wsgi_application() diff --git a/examples/miniapps/django/manage.py b/examples/miniapps/django/manage.py new file mode 100755 index 00000000..d3bb8330 --- /dev/null +++ b/examples/miniapps/django/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/django/requirements.txt b/examples/miniapps/django/requirements.txt new file mode 100644 index 00000000..71ef3239 --- /dev/null +++ b/examples/miniapps/django/requirements.txt @@ -0,0 +1,5 @@ +dependency-injector +django +django-bootstrap4 +pygithub +coverage diff --git a/examples/miniapps/django/screenshot.png b/examples/miniapps/django/screenshot.png new file mode 100644 index 00000000..2d399156 Binary files /dev/null and b/examples/miniapps/django/screenshot.png differ diff --git a/examples/miniapps/django/web/__init__.py b/examples/miniapps/django/web/__init__.py new file mode 100644 index 00000000..8a1fbcfa --- /dev/null +++ b/examples/miniapps/django/web/__init__.py @@ -0,0 +1 @@ +"""Web application package.""" diff --git a/examples/miniapps/django/web/apps.py b/examples/miniapps/django/web/apps.py new file mode 100644 index 00000000..c52fbbe2 --- /dev/null +++ b/examples/miniapps/django/web/apps.py @@ -0,0 +1,13 @@ +"""Application config module.""" + +from django.apps import AppConfig + +from githubnavigator import container +from . import views + + +class WebConfig(AppConfig): + name = 'web' + + def ready(self): + container.wire(modules=[views]) diff --git a/examples/miniapps/django/web/templates/base.html b/examples/miniapps/django/web/templates/base.html new file mode 100644 index 00000000..8edd5984 --- /dev/null +++ b/examples/miniapps/django/web/templates/base.html @@ -0,0 +1,10 @@ +{% extends 'bootstrap4/bootstrap4.html' %} + +{% load bootstrap4 %} + +{% block bootstrap4_title %}{% block title %}{% endblock %}{% endblock %} + +{% block bootstrap4_content %} + {% autoescape off %}{% bootstrap_messages %}{% endautoescape %} + {% block content %}(no content){% endblock %} +{% endblock %} diff --git a/examples/miniapps/django/web/templates/index.html b/examples/miniapps/django/web/templates/index.html new file mode 100644 index 00000000..399c2da0 --- /dev/null +++ b/examples/miniapps/django/web/templates/index.html @@ -0,0 +1,69 @@ +{% 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 %} \ No newline at end of file diff --git a/examples/miniapps/django/web/tests.py b/examples/miniapps/django/web/tests.py new file mode 100644 index 00000000..eddc5d22 --- /dev/null +++ b/examples/miniapps/django/web/tests.py @@ -0,0 +1,63 @@ +"""Tests module.""" + +from unittest import mock + +from django.urls import reverse +from django.test import TestCase +from github import Github + +from githubnavigator import container + + +class IndexTests(TestCase): + + def test_index(self): + 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 container.github_client.override(github_client_mock): + response = self.client.get(reverse('index')) + + self.assertContains(response, 'Results found: 2') + + self.assertContains(response, 'repo1-url') + self.assertContains(response, 'repo1-name') + self.assertContains(response, 'owner1-login') + self.assertContains(response, 'owner1-url') + self.assertContains(response, 'owner1-avatar-url') + + self.assertContains(response, 'repo2-url') + self.assertContains(response, 'repo2-name') + self.assertContains(response, 'owner2-login') + self.assertContains(response, 'owner2-url') + self.assertContains(response, 'owner2-avatar-url') + + def test_index_no_results(self): + github_client_mock = mock.Mock(spec=Github) + github_client_mock.search_repositories.return_value = [] + + with container.github_client.override(github_client_mock): + response = self.client.get(reverse('index')) + + self.assertContains(response, 'Results found: 0') diff --git a/examples/miniapps/django/web/urls.py b/examples/miniapps/django/web/urls.py new file mode 100644 index 00000000..de1d2f9f --- /dev/null +++ b/examples/miniapps/django/web/urls.py @@ -0,0 +1,9 @@ +"""URLs module.""" + +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.index, name='index'), +] diff --git a/examples/miniapps/django/web/views.py b/examples/miniapps/django/web/views.py new file mode 100644 index 00000000..6448e535 --- /dev/null +++ b/examples/miniapps/django/web/views.py @@ -0,0 +1,34 @@ +"""Views module.""" + +from typing import List + +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from dependency_injector.wiring import Provide + +from githubnavigator.containers import Container +from githubnavigator.services import SearchService + + +def index( + request: HttpRequest, + 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()], + limits: List[int] = Provide[Container.config.LIMITS], +) -> HttpResponse: + query = request.GET.get('query', default_query) + limit = int(request.GET.get('limit', default_limit)) + + repositories = search_service.search_repositories(query, limit) + + return render( + request, + template_name='index.html', + context={ + 'query': query, + 'limit': limit, + 'limits': limits, + 'repositories': repositories, + } + ) diff --git a/examples/miniapps/flask/README.rst b/examples/miniapps/flask/README.rst index 12bd3dcc..260a95d2 100644 --- a/examples/miniapps/flask/README.rst +++ b/examples/miniapps/flask/README.rst @@ -46,8 +46,7 @@ After that visit http://127.0.0.1:5000/ in your browser. .. note:: - - Github has a rate limit. When thre rate limit is exceed you will see an exception + 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.