commit 70bff65fc07d83ac08bb3a35c35a8bd8737ec16d Author: Dmitry Afanasyev Date: Tue Jul 6 16:36:19 2021 +0300 github stars project celery-rabbit diff --git a/celery-rabbit-example/.gitignore b/celery-rabbit-example/.gitignore new file mode 100644 index 0000000..8e0776e --- /dev/null +++ b/celery-rabbit-example/.gitignore @@ -0,0 +1 @@ +!.env diff --git a/celery-rabbit-example/Dockerfile b/celery-rabbit-example/Dockerfile new file mode 100644 index 0000000..f2b4d46 --- /dev/null +++ b/celery-rabbit-example/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.8.6-buster + +ENV PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONDONTWRITEBYTECODE=1 \ + # pip: + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + bash \ + build-essential \ + curl \ + gettext \ + git \ + libpq-dev \ + nano + +WORKDIR /code + +# Copy and install dependencies: +COPY requirements.txt /code/ +RUN python -m pip install --upgrade pip +RUN pip install --no-cache-dir -r /code/requirements.txt + +# Copy source files: +COPY . /code/ +# COPY app.py /code/ + + + diff --git a/celery-rabbit-example/README.md b/celery-rabbit-example/README.md new file mode 100644 index 0000000..792d838 --- /dev/null +++ b/celery-rabbit-example/README.md @@ -0,0 +1,6 @@ +# celery first example + +Steps: +1. Run `docker-compose up` +2. Show logs +3. In a new terminal run `docker-compose exec worker python` diff --git a/celery-rabbit-example/celery_config/__init__.py b/celery-rabbit-example/celery_config/__init__.py new file mode 100644 index 0000000..d5dc429 --- /dev/null +++ b/celery-rabbit-example/celery_config/__init__.py @@ -0,0 +1,3 @@ +# from app_celery import app as my_celery_app +# +# __all__ = ('my_celery_app', ) diff --git a/celery-rabbit-example/celery_config/app_celery.py b/celery-rabbit-example/celery_config/app_celery.py new file mode 100644 index 0000000..8fa81eb --- /dev/null +++ b/celery-rabbit-example/celery_config/app_celery.py @@ -0,0 +1,25 @@ +from celery import Celery +from pathlib import Path +from decouple import AutoConfig + +BASE_DIR = Path.cwd().parent +config = AutoConfig(search_path=BASE_DIR.joinpath('config')) + + +RABBITMQ_DEFAULT_USER = config('RABBITMQ_DEFAULT_USER') +RABBITMQ_DEFAULT_PASS = config('RABBITMQ_DEFAULT_PASS') +RABBITMQ_PORT = config('RABBITMQ_PORT', cast=int, default=5672) +RABBITMQ_HOST = config('RABBITMQ_HOST') + + +app_celery_instance = Celery( + 'tasks', + broker='amqp://{login}:{password}@{host}:{port}'.format( + login=RABBITMQ_DEFAULT_USER, + password=RABBITMQ_DEFAULT_PASS, + host=RABBITMQ_HOST, + port=RABBITMQ_PORT, + ), + # TODO: try to get async results with and without backend configured + backend='rpc://', +) diff --git a/celery-rabbit-example/config/.env b/celery-rabbit-example/config/.env new file mode 100644 index 0000000..d2258a6 --- /dev/null +++ b/celery-rabbit-example/config/.env @@ -0,0 +1,6 @@ +# RabbitMQ settings: + +RABBITMQ_DEFAULT_USER=rabbit_admin +RABBITMQ_DEFAULT_PASS=mypass +RABBITMQ_PORT=5672 +RABBITMQ_HOST=rabbitmq_host diff --git a/celery-rabbit-example/docker-compose.yml b/celery-rabbit-example/docker-compose.yml new file mode 100644 index 0000000..7f75a24 --- /dev/null +++ b/celery-rabbit-example/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.7' + +services: + + rabbitmq: + hostname: rabbitmq_host + image: 'rabbitmq:3.8.18-management-alpine' + container_name: first_rabbit + env_file: config/.env + restart: unless-stopped + ports: + - 8080:15672 + - 5672:5672 + + worker: + container_name: first_celery + build: . + command: celery --app=my_app:app_celery_instance worker --loglevel=INFO + env_file: config/.env + depends_on: + - rabbitmq + restart: unless-stopped + +networks: + default: + name: celery_network + driver: bridge + diff --git a/celery-rabbit-example/my_app.py b/celery-rabbit-example/my_app.py new file mode 100644 index 0000000..99efd1f --- /dev/null +++ b/celery-rabbit-example/my_app.py @@ -0,0 +1,14 @@ +from celery_config.app_celery import app_celery_instance + + +@app_celery_instance.task +def add(first: int, second: int) -> int: + print(first + second) + return first + second + + +# TODO: try with `@app.task(throws=(ZeroDivisionError,))` +@app_celery_instance.task +def div(first: int, second: int) -> float: + # TODO: show how errors work + return first / second diff --git a/celery-rabbit-example/requirements.txt b/celery-rabbit-example/requirements.txt new file mode 100644 index 0000000..34b6537 --- /dev/null +++ b/celery-rabbit-example/requirements.txt @@ -0,0 +1,2 @@ +celery==5.0.2 +python-decouple==3.3 diff --git a/new-github-repos/.dockerignore b/new-github-repos/.dockerignore new file mode 100644 index 0000000..374757d --- /dev/null +++ b/new-github-repos/.dockerignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Docker +Dockerfile +docker-compose.yml +docker-compose.override.yml +docker/docker-compose.prod.yml + +# JetBrains +.idea/ diff --git a/new-github-repos/.editorconfig b/new-github-repos/.editorconfig new file mode 100644 index 0000000..fa8404d --- /dev/null +++ b/new-github-repos/.editorconfig @@ -0,0 +1,25 @@ +# Check http://editorconfig.org for more information +# This is the main config file for this project: +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.py] +indent_style = space +indent_size = 4 + +[*.pyi] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/new-github-repos/.gitignore b/new-github-repos/.gitignore new file mode 100644 index 0000000..87d0206 --- /dev/null +++ b/new-github-repos/.gitignore @@ -0,0 +1,247 @@ +#### joe made this: https://goel.io/joe + +# Git style-guide: +# https://github.com/agis-/git-style-guide + +#####=== OSX ===##### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +#####=== Windows ===##### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +#####=== Python ===##### + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#####=== Sass ===##### + +.sass-cache +*.css.map + +#####=== Yeoman ===##### + +node_modules/ +bower_components/ +*.log + +build/ +dist/ + +#####=== Vagrant ===##### +.vagrant/ + +#####=== Node ===##### + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Debug log from npm +npm-debug.log + +#### jetbrains #### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +# You can uncomment these lines to enable configuration sharing between +# team members, or you can restrict `.idea/` folder at all (default). + +# User-specific stuff: +# .idea/**/workspace.xml +# .idea/**/tasks.xml +# .idea/dictionaries + +# # Sensitive or high-churn files: +# .idea/**/dataSources/ +# .idea/**/dataSources.ids +# .idea/**/dataSources.xml +# .idea/**/dataSources.local.xml +# .idea/**/sqlDataSources.xml +# .idea/**/dynamic.xml +# .idea/**/uiDesigner.xml + +# # Gradle: +# .idea/**/gradle.xml +# .idea/**/libraries + +# # Mongo Explorer plugin: +# .idea/**/mongoSettings.xml + +# # Cursive Clojure plugin +# .idea/replstate.xml + +# Restrict `.idea/` folder at all: +.idea/ + +# CMake +cmake-build-debug/ + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + +#####=== Custom ===##### +# Directories: +media/ +.static/ +/static/ + +# File types: +*.sqlite3 +# .db + + +# Configuration file with private data: +# *.env +# .env + +# Local settings files: +*local.py + +# Deploy files for Docker: +docker-compose.deploy.yml + +# Certificates: +docker/caddy/certs/ + +# Artifacts: +.gitlab/.svn/ +artifacts/ + +# mypy: +.mypy_cache/ + +# pytest: +.pytest_cache/ + +# ipython: +.ipython/ diff --git a/new-github-repos/.gitlab-ci.yml b/new-github-repos/.gitlab-ci.yml new file mode 100644 index 0000000..dc2d9cc --- /dev/null +++ b/new-github-repos/.gitlab-ci.yml @@ -0,0 +1,96 @@ +--- + +variables: + GROUP_NAME: "balsh" + PROJECT_NAME: "github-repos" + REGISTRY: "registry.gitlab.com" + IMAGE_FULL_NAME: "${REGISTRY}/${GROUP_NAME}/${PROJECT_NAME}" + + +# Base scripts +# ============ + +.docker: + # We use a custom dind image that is based on raw `docker`, + # it has all the dependencies required. + # By using it we reduce the build time significantly. + # You can fallback to use raw `docker` image, see: + # https://github.com/wemake-services/wemake-dind/ + image: wemakeservices/wemake-dind:latest + interruptible: true + services: + - docker:dind + variables: + DOCKER_DRIVER: overlay2 + before_script: &docker-before-script + # Making sure we are in the right directory, does nothing by default: + - pwd && echo "$CI_PROJECT_DIR" && cd "$CI_PROJECT_DIR" + # Creating `.env` configuration file: + - dump-env -t config/.env.template -p 'SECRET_' > config/.env + # Login into Docker registry: + - echo "$CI_JOB_TOKEN" | docker login "$REGISTRY" + -u gitlab-ci-token --password-stdin + # Debug information: + - docker info && docker-compose --version && git --version + + +# Test scripts +# ============ + +test: + stage: test + extends: .docker + before_script: + - *docker-before-script + # Pulling cache: + - docker pull "${IMAGE_FULL_NAME}:dev" || true + - docker tag "${IMAGE_FULL_NAME}:dev" "${PROJECT_NAME}:dev" || true + script: + # Checking config: + - docker-compose -f docker-compose.yml + -f docker/docker-compose.prod.yml config --quiet + + # The logic itself: + - docker-compose build + - docker-compose run --user=root --rm web sh ./docker/ci.sh + - disl "${PROJECT_NAME}:dev" 950MiB + + # Pushing back the result for future runs: + - docker tag "${PROJECT_NAME}:dev" "${IMAGE_FULL_NAME}:dev" + - docker push "${IMAGE_FULL_NAME}:dev" + only: + - merge_requests + + +# Release scripts +# =============== + +# Releasing image, when in `master` branch, +# can be replaced with `kira-release` bot: +# https://github.com/wemake-services/kira-release +release-image: + extends: .docker + stage: deploy + allow_failure: false + before_script: + # Build local image to be released to gitlab registry, + # modify it to suite your needs as you wish. + # We only care about the name of the image: + - *docker-before-script + + # Now we need the latest images for cache and improved build times: + - docker pull "${IMAGE_FULL_NAME}:latest" || true + - docker pull "${IMAGE_FULL_NAME}:dev" || true + # Create correct tags: + - docker tag "${IMAGE_FULL_NAME}:latest" "${PROJECT_NAME}:latest" || true + - docker tag "${IMAGE_FULL_NAME}:dev" "${PROJECT_NAME}:dev" || true + + # Building the image itself: + - docker-compose -f docker-compose.yml + -f docker/docker-compose.prod.yml build + script: + - docker push "${IMAGE_FULL_NAME}:latest" + only: + - master + environment: + name: production # used to track time with 'cycle analytics' diff --git a/new-github-repos/.python-version b/new-github-repos/.python-version new file mode 100644 index 0000000..203e6d5 --- /dev/null +++ b/new-github-repos/.python-version @@ -0,0 +1 @@ +3.8.9 diff --git a/new-github-repos/CHANGELOG.md b/new-github-repos/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/README.md b/new-github-repos/README.md new file mode 100644 index 0000000..3228290 --- /dev/null +++ b/new-github-repos/README.md @@ -0,0 +1,32 @@ +# github-repos + +github repos + +This project was generated with [`wemake-django-template`](https://github.com/wemake-services/wemake-django-template). Current template version is: [281e5b90040a710092c5be2dbd6b381c464cdf87](https://github.com/wemake-services/wemake-django-template/tree/281e5b90040a710092c5be2dbd6b381c464cdf87). See what is [updated](https://github.com/wemake-services/wemake-django-template/compare/281e5b90040a710092c5be2dbd6b381c464cdf87...master) since then. + + +[![wemake.services](https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake.services) +[![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide) + + +## Prerequisites + +You will need: + +- `python3.8` (see `pyproject.toml` for full version) +- `postgresql` with version `9.6` +- `docker` with [version at least](https://docs.docker.com/compose/compose-file/#compose-and-docker-compatibility-matrix) `18.02` + + +## Development + +When developing locally, we use: + +- [`editorconfig`](http://editorconfig.org/) plugin (**required**) +- [`poetry`](https://github.com/python-poetry/poetry) (**required**) +- `pycharm 2017+` or `vscode` + + +## Documentation + +Full documentation is available here: [`docs/`](docs). diff --git a/new-github-repos/config/.env b/new-github-repos/config/.env new file mode 100644 index 0000000..9f12c51 --- /dev/null +++ b/new-github-repos/config/.env @@ -0,0 +1,43 @@ +# === General === + +DOMAIN_NAME=localhost +TLS_EMAIL=webmaster@localhost + + +# === Django === +# Generate yours with: +# python3 -c 'from django.utils.crypto import get_random_string; print(get_random_string(50))' + +DJANGO_SECRET_KEY=I^[!b6gyNlXmaI,/{tagz+>:4V]%HJNW(=(@:*T~)g-t47tc7y + + +# === Database === + +# These variables are special, since they are consumed +# by both django and postgres docker image. +# Cannot be renamed if you use postgres in docker. +# See: https://hub.docker.com/_/postgres + +POSTGRES_DB=github-repos +POSTGRES_USER=github-admin +POSTGRES_PASSWORD=admin_password + +# Used only by django: +DJANGO_DATABASE_HOST=db +DJANGO_DATABASE_PORT=5432 + +# ==== Email ======= + +EMAIL_HOST=smtp.yandex.ru +EMAIL_HOST_USER=balsh-django@yandex.ru +EMAIL_HOST_PASSWORD=nifwotooabmfauld +EMAIL_PORT=465 +EMAIL_USE_SSL=True +EMAIL_USE_TLS=False + +# =======RabbitMQ======= + +RABBITMQ_DEFAULT_USER=rabbit_admin +RABBITMQ_DEFAULT_PASS=mypass +RABBITMQ_PORT=5672 +RABBITMQ_HOST=rabbitmq_docker diff --git a/new-github-repos/docker-compose.yml b/new-github-repos/docker-compose.yml new file mode 100644 index 0000000..523924f --- /dev/null +++ b/new-github-repos/docker-compose.yml @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +version: "3.7" + +services: + db: + image: "postgres:12" + container_name: github_repos_db + restart: unless-stopped + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - webnet + env_file: ./config/.env + ports: + - 5433:5432 + + web: + image: "github-repos" + container_name: github_repos_web + build: + context: . + dockerfile: ./docker/django/Dockerfile +# args: +# DJANGO_ENV: development +# cache_from: +# - "github-repos:dev" +# - "github-repos:latest" +# - "*" +# volumes: +# - django-static:/var/www/django/static + restart: unless-stopped + volumes: + - .:/code + ports: + - 8000:8000 + depends_on: + - db + networks: + - webnet + env_file: ./config/.env + command: > + bash -c "python manage.py migrate --noinput + && python -Wd manage.py runserver 0.0.0.0:8000" +# healthcheck: +# # We use `$$` here because: +# # one `$` goes to shell, +# # one `$` goes to `docker-compose.yml` escaping +# test: | +# /usr/bin/test $$( +# /usr/bin/curl --fail http://localhost:8000/health/?format=json +# --write-out "%{http_code}" --silent --output /dev/null +# ) -eq 200 +# interval: 10s +# timeout: 5s +# retries: 5 +# start_period: 30s + +# This task is an example of how to extend existing ones: +# some_worker: +# <<: *web +# command: python manage.py worker_process + +networks: + # Network for your internals, use it by default: + webnet: + +volumes: + pgdata: + django-static: diff --git a/new-github-repos/docker/caddy/Caddyfile b/new-github-repos/docker/caddy/Caddyfile new file mode 100644 index 0000000..f368c74 --- /dev/null +++ b/new-github-repos/docker/caddy/Caddyfile @@ -0,0 +1,40 @@ +# See https://caddyserver.com/docs + +# Email for Let's Encrypt expiration notices +{ + email {$TLS_EMAIL} +} + +# "www" redirect to "non-www" version +www.{$DOMAIN_NAME} { + redir https://{$DOMAIN_NAME}{uri} +} + +{$DOMAIN_NAME} { + # HTTPS options: + header Strict-Transport-Security max-age=31536000; + + # Removing some headers for improved security: + header -Server + + # Exclude matcher for Django assets + @excludeDirs { + not path /static/* /media/* + } + + # Serving dynamic requests: + reverse_proxy @excludeDirs web:8000 + + # Serves static files, should be the same as `STATIC_ROOT` setting: + file_server { + root /var/www/django + } + + # Allows to use `.gz` files when available: + encode gzip + + # Logs: + log { + output stdout + } +} diff --git a/new-github-repos/docker/ci.sh b/new-github-repos/docker/ci.sh new file mode 100755 index 0000000..07f00f9 --- /dev/null +++ b/new-github-repos/docker/ci.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +# Initializing global variables and functions: +: "${DJANGO_ENV:=development}" + +# Fail CI if `DJANGO_ENV` is not set to `development`: +if [ "$DJANGO_ENV" != 'development' ]; then + echo 'DJANGO_ENV is not set to development. Running tests is not safe.' + exit 1 +fi + +pyclean () { + # Cleaning cache: + find . \ + | grep -E '(__pycache__|\.hypothesis|\.perm|\.cache|\.static|\.py[cod]$)' \ + | xargs rm -rf +} + +run_ci () { + echo '[ci started]' + set -x # we want to print commands during the CI process. + + # Testing filesystem and permissions: + touch .perm && rm -f .perm + touch '/var/www/django/media/.perm' && rm -f '/var/www/django/media/.perm' + touch '/var/www/django/static/.perm' && rm -f '/var/www/django/static/.perm' + + # Checking `.env` files: + dotenv-linter config/.env config/.env.template + + # Running linting for all python files in the project: + flake8 . + + # Running type checking, see https://github.com/typeddjango/django-stubs + mypy manage.py server $(find tests -name '*.py') + + # Running tests: + pytest --dead-fixtures + pytest + + # Run checks to be sure we follow all django's best practices: + python manage.py check --fail-level WARNING + + # Run checks to be sure settings are correct (production flag is required): + DJANGO_ENV=production python manage.py check --deploy --fail-level WARNING + + # Check that staticfiles app is working fine: + DJANGO_ENV=production DJANGO_COLLECTSTATIC_DRYRUN=1 \ + python manage.py collectstatic --no-input --dry-run + + # Check that all migrations worked fine: + python manage.py makemigrations --dry-run --check + + # Check that all migrations are backwards compatible: + python manage.py lintmigrations --exclude-apps=axes --warnings-as-errors + + # Checking if all the dependencies are secure and do not have any + # known vulnerabilities: + safety check --full-report + + # Checking `pyproject.toml` file contents: + poetry check + + # Checking dependencies status: + pip check + + # Checking docs: + doc8 -q docs + + # Checking `yaml` files: + yamllint -d '{"extends": "default", "ignore": ".venv"}' -s . + + # Checking translation files, ignoring ordering and locations: + polint -i location,unsorted locale + + # Also checking translation files for syntax errors: + if find locale -name '*.po' -print0 | grep -q "."; then + # Only executes when there is at least one `.po` file: + dennis-cmd lint --errorsonly locale + fi + + set +x + echo '[ci finished]' +} + +# Remove any cache before the script: +pyclean + +# Clean everything up: +trap pyclean EXIT INT TERM + +# Run the CI process: +run_ci diff --git a/new-github-repos/docker/django/Dockerfile b/new-github-repos/docker/django/Dockerfile new file mode 100644 index 0000000..b82b3cd --- /dev/null +++ b/new-github-repos/docker/django/Dockerfile @@ -0,0 +1,46 @@ +FROM python:3.8.9-slim-buster + +ENV BUILD_ONLY_PACKAGES='wget' \ + # python: + PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONDONTWRITEBYTECODE=1 \ + # pip: + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + # poetry: + POETRY_VERSION=1.1.4 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_CACHE_DIR='/var/cache/pypoetry' \ + PATH="$PATH:/root/.poetry/bin" + +# System deps: +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + bash \ + build-essential \ + curl \ + gettext \ + git \ + libpq-dev \ + nano \ + # Defining build-time-only dependencies: + $BUILD_ONLY_PACKAGES \ + && curl -sSL 'https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py' | python \ + && poetry --version \ + # Removing build-time-only dependencies: + && apt-get remove -y $BUILD_ONLY_PACKAGES \ + # Cleaning cache: + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* \ + && rm -rf $POETRY_CACHE_DIR + +WORKDIR /code + +# Copy only requirements, to cache them in docker layer +COPY ./poetry.lock ./pyproject.toml /code/ +RUN poetry install +COPY . /code diff --git a/new-github-repos/docker/django/entrypoint.sh b/new-github-repos/docker/django/entrypoint.sh new file mode 100644 index 0000000..67d3e4a --- /dev/null +++ b/new-github-repos/docker/django/entrypoint.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +readonly cmd="$*" + +postgres_ready () { + # Check that postgres is up and running on port `5432`: + dockerize -wait 'tcp://db:5432' -timeout 5s +} + +# We need this line to make sure that this container is started +# after the one with postgres: +until postgres_ready; do + >&2 echo 'Postgres is unavailable - sleeping' +done + +# It is also possible to wait for other services as well: redis, elastic, mongo +>&2 echo 'Postgres is up - continuing...' + +# Evaluating passed command (do not touch): +# shellcheck disable=SC2086 +exec $cmd diff --git a/new-github-repos/docker/django/gunicorn.sh b/new-github-repos/docker/django/gunicorn.sh new file mode 100644 index 0000000..c110f6d --- /dev/null +++ b/new-github-repos/docker/django/gunicorn.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +# We are using `gunicorn` for production, see: +# http://docs.gunicorn.org/en/stable/configure.html + +# Check that $DJANGO_ENV is set to "production", +# fail otherwise, since it may break things: + + +echo "DJANGO_ENV is $DJANGO_ENV" +if [ "$DJANGO_ENV" != 'production' ]; then + echo 'Error: DJANGO_ENV is not set to "production".' + echo 'Application will not start.' + exit 1 +fi + +export DJANGO_ENV + + +# Run python specific scripts: +# Running migrations in startup script might not be the best option, see: +# docs/pages/template/production-checklist.rst +python /code/manage.py migrate --noinput +python /code/manage.py collectstatic --noinput +python /code/manage.py compilemessages + +# Start gunicorn: +# Docs: http://docs.gunicorn.org/en/stable/settings.html +# Concerning `workers` setting see: +# https://github.com/wemake-services/wemake-django-template/issues/1022 +/usr/local/bin/gunicorn server.wsgi \ + --workers=4 `# Sync worker settings` \ + --max-requests=2000 \ + --max-requests-jitter=400 \ + --bind='0.0.0.0:8000' `# Run Django on 8000 port` \ + --chdir='/code' `# Locations` \ + --log-file=- \ + --worker-tmp-dir='/dev/shm' diff --git a/new-github-repos/docker/docker-compose.prod.yml b/new-github-repos/docker/docker-compose.prod.yml new file mode 100644 index 0000000..815c6c1 --- /dev/null +++ b/new-github-repos/docker/docker-compose.prod.yml @@ -0,0 +1,65 @@ +--- + +# This compose-file is production only. So, it should not be called directly. +# +# Instead, it should be a part of your deploy strategy. +# This setup is supposed to be used with `docker-swarm`. +# See `./docs/pages/template/production.rst` docs. + +version: "3.6" +services: + caddy: + image: "caddy:2.2.1" + restart: unless-stopped + env_file: ./config/.env + volumes: + - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile # configuration + - caddy-config:/config # configuration autosaves + - caddy-data:/data # saving certificates + - django-static:/var/www/django/static # serving django's statics + - django-media:/var/www/django/media # serving django's media + ports: + - "80:80" + - "443:443" + depends_on: + - web + networks: + - proxynet + + web: + <<: &web + # Image for production: + image: "registry.gitlab.com/balsh/github-repos:latest" + build: + target: production_build + args: + DJANGO_ENV: production + + restart: unless-stopped + volumes: + - django-media:/var/www/django/media # since in dev it is app's folder + - django-locale:/code/locale # since in dev it is app's folder + + command: sh ./docker/django/gunicorn.sh + networks: + - proxynet + expose: + - 8000 + +# This task is an example of how to extend existing ones: +# some_wroker: +# <<: *web +# command: python manage.py worker_process +# deploy: +# replicas: 2 + +networks: + # Network for your proxy server and application to connect them, + # do not use it for anything else! + proxynet: + +volumes: + django-media: + django-locale: + caddy-config: + caddy-data: diff --git a/new-github-repos/docs/Makefile b/new-github-repos/docs/Makefile new file mode 100644 index 0000000..7af6a32 --- /dev/null +++ b/new-github-repos/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = wemake-django-template +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/new-github-repos/docs/README.md b/new-github-repos/docs/README.md new file mode 100644 index 0000000..e7061d1 --- /dev/null +++ b/new-github-repos/docs/README.md @@ -0,0 +1,18 @@ +# Starting with the docs + +We are using [Sphinx](http://www.sphinx-doc.org) to manage our documentation. +If you have never worked with `Sphinx` this guide +will cover the most common uses cases. + + +## Quickstart + +1. Clone this repository +2. Install dependencies, [here's how to do it](pages/template/development.rst) +3. Run `cd docs && make html` +4. Open `_build/html/index.html` with your browser + + +## Where to go next + +Read the main page of the opened documentation website. It will guide you. diff --git a/new-github-repos/docs/_static/.gitkeep b/new-github-repos/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/docs/_templates/moreinfo.html b/new-github-repos/docs/_templates/moreinfo.html new file mode 100644 index 0000000..1b45b2f --- /dev/null +++ b/new-github-repos/docs/_templates/moreinfo.html @@ -0,0 +1,28 @@ +

+ Links +

+ + + + + diff --git a/new-github-repos/docs/conf.py b/new-github-repos/docs/conf.py new file mode 100644 index 0000000..e6fc79f --- /dev/null +++ b/new-github-repos/docs/conf.py @@ -0,0 +1,136 @@ +# wemake-django-template documentation build configuration file, created by +# sphinx-quickstart on Sat Sep 30 12:42:34 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys +from contextlib import suppress + +import tomlkit + +sys.path.insert(0, os.path.abspath('..')) + +# We need this block, because +# django might be not installed, maybe we are running our +# build process in ReadTheDocs? +# https://github.com/wemake-services/wemake-django-template/issues/133 +with suppress(ImportError): + import django # noqa: WPS433 + + # Normal django setup. That's how it should be in development: + os.environ['DJANGO_SETTINGS_MODULE'] = 'server.settings' + django.setup() + + +# -- Project information ----------------------------------------------------- + +def _get_project_meta(): + with open('../pyproject.toml') as pyproject: + file_contents = pyproject.read() + + return tomlkit.parse(file_contents)['tool']['poetry'] + + +pkg_meta = _get_project_meta() +project = str(pkg_meta['name']) +author = str(pkg_meta['authors'][0]) +copyright = author # noqa: WPS125 + +# The short X.Y version +version = str(pkg_meta['version']) +# The full version, including alpha/beta/rc tags +release = version + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = '3.3' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', + 'sphinx.ext.napoleon', + + # 3rd party, order matters: + # https://github.com/wemake-services/wemake-django-template/issues/159 + 'sphinx_autodoc_typehints', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = ['.rst'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'moreinfo.html', + 'searchbox.html', + ], +} diff --git a/new-github-repos/docs/documents/.gitkeep b/new-github-repos/docs/documents/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/docs/index.rst b/new-github-repos/docs/index.rst new file mode 100644 index 0000000..02663d5 --- /dev/null +++ b/new-github-repos/docs/index.rst @@ -0,0 +1,102 @@ +Welcome to wemake-django-template's documentation! +================================================== + + +What this project is all about? +The main idea of this project is to provide a fully configured +template for ``django`` projects, where code quality, testing, +documentation, security, and scalability are number one priorities. + +This template is a result of implementing +`our processes `_, +it should not be considered as an independent part. + + +Goals +----- + +When developing this template we had several goals in mind: + +- Development environment should be bootstrapped easily, + so we use ``docker-compose`` for that +- Development should be consistent, so we use strict quality and style checks +- Development, testing, and production should have the same environment, + so again we develop, test, and run our apps in ``docker`` containers +- Documentation and codebase are the only sources of truth + + +Limitations +----------- + +This project implies that: + +- You are using ``docker`` for deployment +- You are using Gitlab and Gitlab CI +- You are not using any frontend assets in ``django``, + you store your frontend separately + + +Should I choose this template? +------------------------------ + +This template is oriented on big projects, +when there are multiple people working on it for a long period of time. + +If you want to simply create a working prototype without all these +limitations and workflows - feel free to choose any +`other template `_. + + +How to start +------------ + +You should start with reading the documentation. +Reading order is important. + +There are multiple processes that you need to get familiar with: + +- First time setup phase: what system requirements you must fulfill, + how to install dependencies, how to start your project +- Active development phase: how to make changes, run tests, + + +.. toctree:: + :maxdepth: 2 + :caption: Setting things up: + + pages/template/overview.rst + pages/template/development.rst + pages/template/django.rst + +.. toctree:: + :maxdepth: 2 + :caption: Quality assurance: + + pages/template/documentation.rst + pages/template/linters.rst + pages/template/testing.rst + pages/template/security.rst + pages/template/gitlab-ci.rst + +.. toctree:: + :maxdepth: 2 + :caption: Production: + + pages/template/production-checklist.rst + pages/template/production.rst + +.. toctree:: + :maxdepth: 1 + :caption: Extras: + + pages/template/upgrading-template.rst + pages/template/faq.rst + pages/template/troubleshooting.rst + + +Indexes and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/new-github-repos/docs/make.bat b/new-github-repos/docs/make.bat new file mode 100644 index 0000000..b22d898 --- /dev/null +++ b/new-github-repos/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=wemake-django-template + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/new-github-repos/docs/pages/project/.gitkeep b/new-github-repos/docs/pages/project/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/docs/pages/template/development.rst b/new-github-repos/docs/pages/template/development.rst new file mode 100644 index 0000000..17218c8 --- /dev/null +++ b/new-github-repos/docs/pages/template/development.rst @@ -0,0 +1,178 @@ +Development +=========== + +Our development process is focused on high quality and development comfort. +We use tools that are proven to be the best in class. + +There are two possible ways to develop your apps. + +1. local development +2. development inside ``docker`` + +You can choose one or use both at the same time. +How to choose what method should you use? + +Local development is much easier and much faster. +You can choose it if you don't have too many infrastructure dependencies. +That's a default option for the new projects. + +Choosing ``docker`` development means that you already have a complex +setup of different technologies, containers, networks, etc. +This is a default option for older and more complicated projects. + + +Dependencies +------------ + +We use ``poetry`` to manage dependencies. +So, please do not use ``virtualenv`` or ``pip`` directly. +Before going any further, please, +take a moment to read the `official documentation `_ +about ``poetry`` to know some basics. + +If you are using ``docker`` then prepend ``docker-compose run --rm web`` +before any of those commands to execute them. + +Please, note that you don't need almost all of them with ``docker``. +You can just skip this sub-section completely. +Go right to `Development with docker`_. + +Installing dependencies +~~~~~~~~~~~~~~~~~~~~~~~ + +You do not need to run any of these command for ``docker`` based development, +since it is already executed inside ``Dockerfile``. + +Please, note that ``poetry`` will automatically create a ``virtualenv`` for +this project. It will use you current ``python`` version. +To install all existing dependencies run: + +.. code:: bash + + poetry install + +To install dependencies for production use, you will need to run: + +.. code:: bash + + poetry install --no-dev + +And to activate ``virtualenv`` created by ``poetry`` run: + +.. code:: bash + + poetry shell + +Adding new dependencies +~~~~~~~~~~~~~~~~~~~~~~~ + +To add a new dependency you can run: + +- ``poetry add django`` to install ``django`` as a production dependency +- ``poetry add --dev pytest`` to install ``pytest`` + as a development dependency + +This command might be used with ``docker``. + +Updating poetry version +~~~~~~~~~~~~~~~~~~~~~~~ + +Package managers should also be pinned very strictly. +We had a lot of problems in production +because we were not pinning package manager versions. + +This can result in broken ``lock`` files, inconsistent installation process, +bizarre bugs, and missing packages. You do not want to experience that! + +How can we have the same ``poetry`` version for all users in a project? +That's where ``[build-system]`` tag shines. It specifies the exact version of +your ``poetry`` installation that must be used for the project. +Version mismatch will fail your build. + +When you want to update ``poetry``, you have to bump it in several places: + +1. ``pyproject.toml`` +2. ``docker/django/Dockerfile`` + +Then you are fine! + + +Development with docker +----------------------- + +To start development server inside ``docker`` you will need to run: + +.. code:: bash + + docker-compose build + docker-compose run --rm web python manage.py migrate + docker-compose up + +Running scripts inside docker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As we have already mentioned inside the previous section +we use ``docker-compose run`` to run scripts inside docker. + +What do you need to know about it? + +1. You can run anything you want: ``poetry``, ``python``, ``sh``, etc +2. Most likely it will have a permanent effect, due to ``docker volumes`` +3. You need to use ``--rm`` to automatically remove this container afterward + +**Note**: ``docker`` commands do not need to use ``virtualenv`` at all. + +Extra configuration +~~~~~~~~~~~~~~~~~~~ + +You might want to tweak ``INTERNAL_IPS`` ``django`` setting +to include your ``docker`` container address into it. +Otherwise ``django-debug-toolbar`` might not show up. + +To get your ``docker`` ip run: + +.. code:: bash + + docker inspect your-container-name | grep -e '"Gateway"' + +You can also configure a permanent hostname inside your ``/etc/hosts`` to +access your ``docker`` containers with a permanent hostname. + + +Local development +----------------- + +When cloning a project for the first time you may +need to configure it properly, +see :ref:`django` section for more information. + +**Note**, that you will need to activate ``virtualenv`` created +by ``poetry`` before running any of these commands. +**Note**, that you only need to run these commands once per project. + +Local database +~~~~~~~~~~~~~~ + +When using local development environment without ``docker``, +you will need a ``postgres`` up and running. +To create new development database run +(make sure that database and user names are correct for your case): + +.. code:: bash + + psql postgres -U postgres -f sql/create_database.sql + +Then migrate your database: + +.. code:: bash + + python manage.py migrate + +Running project +~~~~~~~~~~~~~~~ + +If you have reached this point, you should be able to run the project. + +.. code:: bash + + python manage.py runserver diff --git a/new-github-repos/docs/pages/template/django.rst b/new-github-repos/docs/pages/template/django.rst new file mode 100644 index 0000000..86fb5fd --- /dev/null +++ b/new-github-repos/docs/pages/template/django.rst @@ -0,0 +1,167 @@ +.. _django: + +Django +====== + + +Configuration +------------- + +We share the same configuration structure for almost every possible +environment. + +We use: + +- ``django-split-settings`` to organize ``django`` + settings into multiple files and directories +- ``.env`` files to store secret configuration +- ``python-decouple`` to load ``.env`` files into ``django`` + +Components +~~~~~~~~~~ + +If you have some specific components like ``celery`` or ``mailgun`` installed, +they could be configured in separate files. +Just create a new file in ``server/settings/components/``. +Then add it into ``server/settings/__init__.py``. + +Environments +~~~~~~~~~~~~ + +To run ``django`` on different environments just +specify ``DJANGO_ENV`` environment variable. +It must have the same name as one of the files +from ``server/settings/environments/``. +Then, values from this file will override other settings. + +Local settings +~~~~~~~~~~~~~~ + +If you need some specific local configuration tweaks, +you can create file ``server/settings/environments/local.py.template`` +to ``server/settings/environments/local.py``. +It will be loaded into your settings automatically if exists. + +.. code:: bash + + cp server/settings/environments/local.py.template server/settings/environments/local.py + +See ``local.py.template`` version for the reference. + + +Secret settings +--------------- + +We share the same mechanism for secret settings for all our tools. +We use ``.env`` files for ``django``, ``postgres``, ``docker``, etc. + +Initially, you will need to copy file +``config/.env.template`` to ``config/.env``: + +.. code:: bash + + cp config/.env.template config/.env + +When adding any new secret ``django`` settings you will need to: + +1. Add new key and value to ``config/.env`` +2. Add new key without value to ``config/.env.template``, + add a comment on how to get this value for other users +3. Add new variable inside ``django`` settings +4. Use ``python-decouple`` to load this ``env`` variable like so: + ``MY_SECRET = config('MY_SECRET')`` + + +Secret settings in production +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We do not store our secret settings inside our source code. +All sensible settings are stored in ``config/.env`` file, +which is not tracked by the version control. + +So, how do we store secrets? We store them as secret environment variables +in `GitLab CI `_. +Then we use `dump-env `_ +to dump variables from both environment and ``.env`` file template. +Then, this file is copied inside ``docker`` image and when +this image is built - everything is ready for production. + +Here's an example: + +1. We add a ``SECRET_DJANGO_SECRET_KEY`` variable to Gitlab CI secret variables +2. Then ``dump-env`` dumps ``SECRET_DJANGO_SECRET_KEY`` + as ``DJANGO_SECRET_KEY`` and writes it to ``config/.env`` file +3. Then it is loaded by ``django`` inside the settings: + ``SECRET_KEY = config('DJANGO_SECRET_KEY')`` + +However, there are different options to store secret settings: + +- `ansible-vault `_ +- `git-secret `_ +- `Vault `_ + +Depending on a project we use different tools. +With ``dump-env`` being the default and the simplest one. + + +Extensions +---------- + +We use different ``django`` extensions that make your life easier. +Here's a full list of the extensions for both development and production: + +- `django-split-settings`_ - organize + ``django`` settings into multiple files and directories. + Easily override and modify settings. + Use wildcards in settings file paths and mark settings files as optional +- `django-axes`_ - keep track + of failed login attempts in ``django`` powered sites +- `django-csp`_ - `Content Security Policy`_ for ``django`` +- `django-referrer-policy`_ - middleware implementing the `Referrer-Policy`_ +- `django-health-check`_ - checks for various conditions and provides reports + when anomalous behavior is detected +- `django-add-default-value`_ - this django Migration Operation can be used to + transfer a Fields default value to the database scheme +- `django-deprecate-fields`_ - this package allows deprecating model fields and + allows removing them in a backwards compatible manner +- `django-migration-linter`_ - detect backward incompatible migrations for + your django project +- `zero-downtime-migrations`_ - apply ``django`` migrations on PostgreSql + without long locks on tables + +Development only extensions: + +- `django-debug-toolbar`_ - a configurable set of panels that + display various debug information about the current request/response +- `django-querycount`_ - middleware that prints the number + of DB queries to the runserver console +- `nplusone`_ - auto-detecting the `n+1 queries problem`_ in ``django`` + +.. _django-split-settings: https://github.com/sobolevn/django-split-settings +.. _django-axes: https://github.com/jazzband/django-axes +.. _django-csp: https://github.com/mozilla/django-csp +.. _`Content Security Policy`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +.. _django-referrer-policy: https://github.com/ubernostrum/django-referrer-policy +.. _`Referrer-Policy`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy +.. _django-health-check: https://github.com/KristianOellegaard/django-health-check +.. _django-add-default-value: https://github.com/3YOURMIND/django-add-default-value +.. _django-deprecate-fields: https://github.com/3YOURMIND/django-deprecate-fields +.. _django-migration-linter: https://github.com/3YOURMIND/django-migration-linter +.. _zero-downtime-migrations: https://github.com/yandex/zero-downtime-migrations +.. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar +.. _django-querycount: https://github.com/bradmontgomery/django-querycount +.. _nplusone: https://github.com/jmcarp/nplusone +.. _`n+1 queries problem`: https://stackoverflow.com/questions/97197/what-is-the-n1-select-query-issue + + +Further reading +--------------- + +- `django-split-settings tutorial `_ +- `docker env-file docs `_ + + +Django admin +~~~~~~~~~~~~ + +- `Django Admin Cookbook `_ diff --git a/new-github-repos/docs/pages/template/documentation.rst b/new-github-repos/docs/pages/template/documentation.rst new file mode 100644 index 0000000..7a6491e --- /dev/null +++ b/new-github-repos/docs/pages/template/documentation.rst @@ -0,0 +1,98 @@ +Documentation +============= + +`We `_ write a lot of documentation. +Since we believe, that documentation is a crucial factor +which defines project success or failure. + +Here's how we write docs for ``django`` projects. + + +Dependencies +------------ + +We are using ``sphinx`` as a documentation builder. +We use ``sphinx.ext.napoleon`` to write +pretty docstrings inside the source code. +We also use ``sphinx_autodoc_typehints`` to inject type annotations into docs. + +We also use two sources of truth for the dependencies here: + +- ``docs/requirements.txt`` +- ``pyproject.toml`` + +Why? Because we are using ReadTheDocs +for this template (only for original Github repo), and it +does only support traditional ``requirements.txt``. + + +Structure +--------- + +We use a clear structure for this documentation. + +- ``pages/template`` contains docs + from `wemake-django-template `_. + These files should not be modified locally. + If you have any kind of question or problems, + just open an issue `on github `_ +- ``pages/project`` contains everything related to the project itself. + Usage examples, an auto-generated documentation from your source code, + configuration, business, and project goals +- ``documents`` contains different non-sphinx documents + like ``doc`` files, spreadsheets, and mockups + +Please, do not mix it up. + +How to structure project docs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is a good practice to write a single ``rst`` document +for every single ``py`` file. +Obviously, ``rst`` structure fully copies the structure of your source code. +This way it is very easy to navigate through the docs, +since you already know the structure. + +For each ``django`` application we tend to create +a file called ``index.rst`` which is considered +the main file for the application. + +And ``pages/project/index.rst`` is the main file for the whole project. + + +How to contribute +----------------- + +We enforce everyone to write clean and explaining documentation. +However, there are several rules about writing styling. + +We are using `doc8 `_ to validate our docs. +So, here's the command to do it: + +.. code:: bash + + doc8 ./docs + +This is also used in our CI process, so your build will fail +if there are violations. + + +Useful plugins +-------------- + +Some ``sphinx`` plugins are not included, since they are very specific. +However, they are very useful: + +- `sphinxcontrib-mermaid `_ - sphinx plugin to create general flowcharts, sequence and gantt diagrams +- `sphinxcontrib-plantuml `_ - sphinx plugin to create UML diagrams +- `nbsphinx `_ - sphinx plugin to embed ``ipython`` notebooks into your docs + + +Further reading +--------------- + +- `sphinx `_ +- `sphinx with django `_ +- `sphinx-autodoc-typehints `_ +- `Architecture Decision Record (ADR) `_ +- `adr-tools `_ diff --git a/new-github-repos/docs/pages/template/faq.rst b/new-github-repos/docs/pages/template/faq.rst new file mode 100644 index 0000000..a01c18f --- /dev/null +++ b/new-github-repos/docs/pages/template/faq.rst @@ -0,0 +1,29 @@ +Frequently asked questions +========================== + +Will you ever support drf / celery / flask / gevent? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +No. This template is focused on bringing best practices to ``django`` +projects. It only includes workflow and configuration for this framework. + +Other tools are not mandatory. And can easily be added by a developer. + +Will you have an build-time option to include or change anything? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +No, we believe that options bring inconsistencies to the project. +You can also make the wrong choice. So, we are protecting you from that. + +You can only have options that are already present in this template. +Fork it, if you do not agree with this policy. + +This code quality is unbearable! Can I turn it off? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Of course, no one can stop you from that. +But what the point in using this template then? + +Our code quality defined by this template is minimally acceptable. +We know tools to make it even better. But they are not included. +Since they are literally hardcore. diff --git a/new-github-repos/docs/pages/template/gitlab-ci.rst b/new-github-repos/docs/pages/template/gitlab-ci.rst new file mode 100644 index 0000000..d7ef0e4 --- /dev/null +++ b/new-github-repos/docs/pages/template/gitlab-ci.rst @@ -0,0 +1,56 @@ +Gitlab CI +========= + +We use ``Gitlab CI`` to build our containers, test it, +and store them in the internal registry. + +These images are then pulled into the production servers. + + +Configuration +------------- + +All configuration is done inside ``.gitlab-ci.yml``. + + +Pipelines +--------- + +We have two pipelines configured: for ``master`` and other branches. +That's how it works: we only run testing for feature branches and do the whole +building/testing/deploying process for the ``master`` branch. + +This allows us to speed up development process. + + +Automatic dependencies update +----------------------------- + +You can use `dependabot `_ +to enable automatic dependencies updates via Pull Requests to your repository. +Similar to the original template repository: `list of pull requests `_. + +It is available to both Github and Gitlab. +But, for Gitlab version you currently have to update your `.gitlab-ci.yml `_. + + +Secret variables +---------------- + +If some real secret variables are required, then you can use `gitlab secrets `_. +And these kind of variables are required *most* of the time. + +See :ref:`django` on how to use ``dump-env`` and ``gitlab-ci`` together. + + +Documentation +------------- +After each deploy from master branch this documentation compiles into nice looking html page. +See `gitlab pages info `_. + + +Further reading +--------------- + +- `Container Registry `_ +- `Gitlab CI/CD `_ diff --git a/new-github-repos/docs/pages/template/linters.rst b/new-github-repos/docs/pages/template/linters.rst new file mode 100644 index 0000000..7191e02 --- /dev/null +++ b/new-github-repos/docs/pages/template/linters.rst @@ -0,0 +1,130 @@ +.. _linters: + +Linters +======= + +This project uses several linters to make coding style consistent. +All configuration is stored inside ``setup.cfg``. + + +wemake-python-styleguide +------------------------ + +``wemake-python-styleguide`` is a ``flake8`` based plugin. +And it is also the strictest and most opinionated python linter ever. +See `wemake-python-styleguide `_ +docs. + +Things that are included in the linting process: + +- `flake8 `_ is used a general tool for linting +- `isort `_ is used to validate ``import`` order +- `bandit `_ for static security checks +- `eradicate `_ to find dead code +- and more! + +Running linting process for all ``python`` files in the project: + +.. code:: bash + + flake8 . + +Extra plugins +~~~~~~~~~~~~~ + +We also use some extra plugins for ``flake8`` +that are not bundled with ``wemake-python-styleguide``: + +- `flake8-pytest `_ - ensures that ``pytest`` best practices are used +- `flake8-pytest-style `_ - ensures that ``pytest`` tests and fixtures are written in a single style +- `flake8-django `_ - plugin to enforce best practices in a ``django`` project + + +django-migration-linter +----------------------- + +We use ``django-migration-linter`` to find backward incompatible migrations. +It allows us to write 0-downtime friendly code. + +See `django-migration-linter `_ +docs, it contains a lot of useful information about ways and tools to do it. + +That's how this check is executed: + +.. code:: bash + + python manage.py lintmigrations --exclude-apps=axes + +Important note: you might want to exclude some packages with broken migrations. +Sometimes, there's nothing we can do about it. + + +yamllint +-------- + +Is used to lint your ``yaml`` files. +See `yamllint `_ docs. + +.. code:: bash + + yamllint -d '{"extends": "default", "ignore": ".venv"}' -s . + + +dotenv-linter +------------- + +Is used to lint your ``.env`` files. +See `dotenv-linter `_ docs. + +.. code:: bash + + dotenv-linter config/.env config/.env.template + + +polint and dennis +----------------- + +Are used to lint your ``.po`` files. +See `polint `_ docs. +Also see `dennis `_ docs. + +.. code:: bash + + polint -i location,unsorted locale + dennis-cmd lint --errorsonly locale + + +Packaging +--------- + +We also use ``pip`` and ``poetry`` self checks to be sure +that packaging works correctly. + +.. code:: bash + + poetry check && pip check + + +Linters that are not included +----------------------------- + +Sometimes we use several other linters that are not included. +That's because they require another technology stack to be installed +or just out of scope. + +We also recommend to check the list of linters +`recommended by wemake-python-styleguide `_. + +Here's the list of these linters. You may still find them useful. + +shellcheck +~~~~~~~~~~ + +This linter is used to lint your ``.sh`` files. +See `shellcheck `_ docs. + +hadolint +~~~~~~~~ + +This linter is used to lint your ``Dockerfile`` syntax. +See `hadolint `_ diff --git a/new-github-repos/docs/pages/template/overview.rst b/new-github-repos/docs/pages/template/overview.rst new file mode 100644 index 0000000..8225701 --- /dev/null +++ b/new-github-repos/docs/pages/template/overview.rst @@ -0,0 +1,135 @@ +Overview +======== + + +System requirements +------------------- + +- ``git`` with a version at least ``2.16`` or higher +- ``docker`` with a version at least ``18.02`` or higher +- ``docker-compose`` with a version at least ``1.21`` or higher +- ``python`` with exact version, see ``pyproject.toml`` + + +Architecture +------------ + +config +~~~~~~ + +- ``config/.env.template`` - a basic example of what keys must be contained in + your ``.env`` file, this file is committed to VCS + and must not contain private or secret values +- ``config/.env`` - main file for secret configuration, + contains private and secret values, should not be committed to VCS + +root project +~~~~~~~~~~~~ + +- ``README.md`` - main readme file, it specifies the entry + point to the project's documentation +- ``.dockerignore`` - specifies what files should not be + copied to the ``docker`` image +- ``.editorconfig`` - file with format specification. + You need to install the required plugin for your IDE in order to enable it +- ``.gitignore`` - file that specifies + what should we commit into the repository and we should not +- ``.gitlab-ci.yml`` - GitLab CI configuration file. + It basically defines what to do with your project + after pushing it to the repository. Currently it is used for testing + and releasing a ``docker`` image +- ``docker-compose.yml`` - this the file specifies ``docker`` services + that are needed for development and testing +- ``docker-compose.override.yml`` - local override for ``docker-compose``. + Is applied automatically and implicitly when + no arguments provided to ``docker-compose`` command +- ``manage.py`` - main file for your ``django`` project. + Used as an entry point for the ``django`` project +- ``pyproject.toml`` - main file of the project. + It defines the project's dependencies. +- ``poetry.lock`` - lock file for dependencies. + It is used to install exactly the same versions of dependencies on each build +- ``setup.cfg`` - configuration file, that is used by all tools in this project +- ``locale/`` - helper folder, that is used to store locale data, + empty by default +- ``sql/`` - helper folder, that contains ``sql`` script for database setup + and teardown for local development + +server +~~~~~~ + +- ``server/__init__.py`` - package definition, empty file +- ``server/urls.py`` - ``django`` `urls definition `_ +- ``server/wsgi.py`` - ``django`` `wsgi definition `_ +- ``server/apps/`` - place to put all your apps into +- ``server/apps/main`` - ``django`` application, used as an example, + could be removed +- ``server/settings`` - settings defined with ``django-split-settings``, + see this `tutorial `_ + for more information +- ``server/templates`` - external folder for ``django`` templates, + used for simple files as ``robots.txt`` and so on + +docker +~~~~~~ + +- ``docker/ci.sh`` - file that specifies all possible checks that + we execute during our CI process +- ``docker/docker-compose.prod.yml`` - additional service definition file + used for production +- ``docker/django/Dockerfile`` - ``django`` container definition, + used both for development and production +- ``docker/django/entrypoint.sh`` - entry point script that is used + when ``django`` container is starting +- ``docker/django/gunicorn.sh`` - production script for ``django``, + that's how we configure ``gunicorn`` runner +- ``docker/caddy/Caddyfile`` - configuration file for Caddy webserver + +tests +~~~~~ + +- ``tests/test_server`` - tests that ensures that basic ``django`` + stuff is working, should not be removed +- ``tests/test_apps/test_main`` - example tests for the ``django`` app, + could be removed +- ``tests/conftest.py`` - main configuration file for ``pytest`` runner + +docs +~~~~ + +- ``docs/Makefile`` - command file that builds the documentation for Unix +- ``docs/make.bat`` - command file for Windows +- ``docs/conf.py`` - ``sphinx`` configuration file +- ``docs/index.rst`` - main documentation file, used as an entry point +- ``docs/pages/project`` - folder that will contain + documentation written by you! +- ``docs/pages/template`` - folder that contains documentation that + is common for each project built with this template +- ``docs/documents`` - folder that should contain any documents you have: + spreadsheets, images, requirements, presentations, etc +- ``docs/requirements.txt`` - helper file, contains dependencies + for ``readthedocs`` service. Can be removed +- ``docs/README.rst`` - helper file for this directory, + just tells what to do next + + +Container internals +------------------- + +We use the ``docker-compose`` to link different containers together. +We also utilize different ``docker`` networks to control access. + +Some containers might have long starting times, for example: + +- ``postgres`` +- ``rabbitmq`` +- frontend, like ``node.js`` + +To be sure that container is started at the right time, +we utilize ``dockerize`` `script `_. +It is executed inside ``docker/django/entrypoint.sh`` file. + +We start containers with ``tini``. +Because this way we have a proper signal handling +and eliminate zombie processes. +Read the `official docs `_ to know more. diff --git a/new-github-repos/docs/pages/template/production-checklist.rst b/new-github-repos/docs/pages/template/production-checklist.rst new file mode 100644 index 0000000..2c69d36 --- /dev/null +++ b/new-github-repos/docs/pages/template/production-checklist.rst @@ -0,0 +1,186 @@ +.. _`going-to-production`: + +Going to production +=================== + +This section covers everything you need to know before going to production. + + +Django +------ + +Checks +~~~~~~ + +Before going to production make sure you have checked everything: + +1. Migrations are up-to-date +2. Static files are all present +3. There are no security or other ``django`` warnings + +Checking migrations, static files, +and security is done inside ``ci.sh`` script. + +We check that there are no unapplied migrations: + +.. code :: bash + + python manage.py makemigrations --dry-run --check + +If you have forgotten to create a migration and changed the model, +you will see an error on this line. + +We also check that static files can be collected: + +.. code :: bash + + DJANGO_ENV=production python manage.py collectstatic --no-input --dry-run + +However, this check does not cover all the cases. +Sometimes ``ManifestStaticFilesStorage`` will fail on real cases, +but will pass with ``--dry-run`` option. +You can disable ``--dry-run`` option if you know what you are doing. +Be careful with this option, when working with auto-uploading +your static files to any kind of CDNs. + +That's how we check ``django`` warnings: + +.. code:: bash + + DJANGO_ENV=production python manage.py check --deploy --fail-level WARNING + +These warnings are raised by ``django`` +when it detects any configuration issues. + +This command should give not warnings or errors. +It is bundled into `docker`, so the container will not work with any warnings. + +Static and media files +~~~~~~~~~~~~~~~~~~~~~~ + +We use ``/var/www/django`` folder to store our media +and static files in production as ``/var/www/django/static`` +and ``/var/www/django/media``. +Docker uses these two folders as named volumes. +And later these volumes are also mounted to ``caddy`` +with ``ro`` mode so it possible to read their contents. + +To find the exact location of these files on your host +you will need to do the following: + +.. code:: bash + + docker volume ls # to find volumes' names + docker volume inspect VOLUME_NAME + +Sometimes storing your media files inside a container is not a good idea. +Use ``CDN`` when you have a lot of user content +or it is very important not to lose it. +There are `helper libraries `_ +to bind ``django`` and these services. + +If you don't need ``media`` files support, just remove the volumes. + +Migrations +~~~~~~~~~~ + +We do run migration in the ``gunicorn.sh`` by default. +Why do we do this? Because that's probably the easiest way to do it. +But it clearly has some disadvantages: + +- When scaling your container for multiple nodes you will have multiple + threads running the same migrations. And it might be a problem since + migrations do not guarantee that it will work this way. +- You can perform some operations multiple times +- Possible other evil things may happen + +So, what to do in this case? +Well, you can do whatever it takes to run migrations in a single thread. +For example, you can create a separate container to do just that. +Other options are fine as well. + + +Postgres +-------- + +Sometimes using ``postgres`` inside a container +`is not a good idea `_. +So, what should be done in this case? + +First of all, move your database ``docker`` service definition +inside ``docker-compose.override.yml``. +Doing so will not affect development, +but will remove database service from production. +Next, you will need to specify `extra_hosts `_ +to contain your ``postgresql`` address. +Lastly, you would need to add new hosts to ``pg_hba.conf``. + +`Here `_ +is a nice tutorial about this topic. + + +Caddy +----- + +Let's Encrypt +~~~~~~~~~~~~~ + +We are using ``Caddy`` and ``Let's Encrypt`` for HTTPS. +The Caddy webserver used in the default configuration will get +you a valid certificate from ``Let's Encrypt`` and update it automatically. +All you need to do to enable this is to make sure +that your DNS records are pointing to the server Caddy runs on. + +Read more: `Automatic HTTPS `_ +in Caddy docs. + +Caddyfile validation +~~~~~~~~~~~~~~~~~~~~ + +You can also run ``-validate`` command to validate ``Caddyfile`` contents. + +Here's it would look like: + +.. code:: bash + + docker-compose -f docker-compose.yml -f docker/docker-compose.prod.yml + run --rm caddy -validate + +This check is not included in the pipeline by default, +because it is quite long to start all the machinery for this single check. + +Disabling HTTPS +~~~~~~~~~~~~~~~ + +You would need to `disable `_ +``https`` inside ``Caddy`` and in production settings for Django. +Because Django itself also redirects to `https`. +See `docs `_. + +You would also need to disable ``manage.py check`` +in ``docker/ci.sh``. +Otherwise, your application won't start, +it would not pass ``django``'s security checks. + +Disabling WWW subdomain +~~~~~~~~~~~~~~~~~~~~~~~ + +If you for some reason do not require ``www.`` subdomain, +then delete ``www.{$DOMAIN_NAME}`` section from ``Caddyfile``. + +Third-Level domains +~~~~~~~~~~~~~~~~~~~ + +You have to disable ``www`` subdomain if +your app works on third-level domains like: + +- ``kira.wemake.services`` +- ``support.myapp.com`` + +Otherwise, ``Caddy`` will server redirects to ``www.example.yourdomain.com``. + + +Further reading +--------------- + +- Django's deployment `checklist `_ diff --git a/new-github-repos/docs/pages/template/production.rst b/new-github-repos/docs/pages/template/production.rst new file mode 100644 index 0000000..ceffd5a --- /dev/null +++ b/new-github-repos/docs/pages/template/production.rst @@ -0,0 +1,77 @@ +Production +========== + +We use different tools and setup for production. +We do not fully provide this part with the template. Why? + +1. It requires a lot of server configuration +2. It heavily depends on your needs: performance, price, technology, etc +3. It is possible to show some vulnerable parts to possible attackers + +So, you will need to deploy your application by yourself. +Here, we would like to cover some basic things that are not changed +from deployment strategy. + +The easiest deployment strategy for small apps is ``docker-compose`` and +``systemd`` inside a host operating system. + + +Production configuration +------------------------ + +You will need to specify extra configuration +to run ``docker-compose`` in production. +Since production build also uses ``caddy``, +which is not required into the development build. + +.. code:: bash + + docker-compose -f docker-compose.yml -f docker/docker-compose.prod.yml up + + +Pulling pre-built images +------------------------ + +You will need to pull pre-built images from ``Gitlab`` to run them. +How to do that? + +The first step is to create a personal access token for this service. +Then, login into your registry with: + +.. code:: bash + + docker login registry.gitlab.your.domain + +And now you are ready to pull your images: + +.. code:: bash + + docker pull your-image:latest + +See `official Gitlab docs `_. + + +Updating already running service +-------------------------------- + +If you need to update an already running service, +them you will have to use ``docker service update`` +or ``docker stack deploy``. + +Updating existing `service `_. +Updating existing `stack `_. + +Zero-Time Updates +~~~~~~~~~~~~~~~~~ + +Zero-Time Updates can be tricky. +You need to create containers with the new code, update existing services, +wait for the working sessions to be completed, and to shut down old +containers. + + +Further reading +--------------- + +- Production with `docker-compose `_ +- `Full tutorial `_ diff --git a/new-github-repos/docs/pages/template/security.rst b/new-github-repos/docs/pages/template/security.rst new file mode 100644 index 0000000..368f6c6 --- /dev/null +++ b/new-github-repos/docs/pages/template/security.rst @@ -0,0 +1,114 @@ +Security +======== + +Security is our first priority. +We try to make projects as secure as possible. +We use a lot of 3rd party tools to achieve that. + + +Django +------ + +Django has a lot of `security-specific settings `_ +that are all turned on by default in this template. + +We also :ref:`enforce ` all the best practices +by running ``django`` checks inside CI for each commit. + +We also use a set of custom ``django`` apps +to enforce even more security rules: + +- `django-axes `_ to track and ban repeating access requests +- `django-csp `_ to enforce `Content-Security Policy `_ for our webpages +- `django-http-referrer-policy `_ to enforce `Referrer Policy `_ for our webpages + +And there are also some awesome extensions that are not included: + +- `django-honeypot `_ - django application that provides utilities for preventing automated form spam + +Passwords +~~~~~~~~~ + +We use strong algorithms for password hashing: +``bcrypt``, ``PBKDF2`` and ``Argon2`` which are known to be secure enough. + + +Dependencies +------------ + +We use `poetry `_ which ensures +that all the dependencies hashes match during the installation process. +Otherwise, the build will fail. +So, it is almost impossible to replace an already existing package +with a malicious one. + +We also use `safety `_ +to analyze vulnerable dependencies to prevent the build +to go to the production with known unsafe dependencies. + +.. code:: bash + + safety check + +We also use `Github security alerts `_ +for our main template repository. + + +Static analysis +--------------- + +We use ``wemake-python-styleguide`` which +includes `bandit `_ security checks inside. + +You can also install `pyt `_ +which is not included by default. +It will include even more static checks for +``sql`` injections, ``xss`` and others. + + +Dynamic analysis +---------------- + +You can monitor your running application to detect anomalous activities. +Tools to consider: + +- `dagda `_ - a tool to perform static analysis of known vulnerabilities, trojans, viruses, malware & other malicious threats in docker images/containers and to monitor the docker daemon and running docker containers for detecting anomalous activities + +All the tools above are not included into this template. +You have to install them by yourself. + + +Secrets +------- + +We store secrets separately from code. So, it is harder for them to leak. +However, we encourage to use tools like +`truffleHog `_ or `detect-secrets `_ inside your workflow. + +You can also turn on `Gitlab secrets checker `_ which we highly recommend. + + +Audits +------ + +The only way to be sure that your app is secure +is to constantly audit it in production. + +There are different tools to help you: + +- `twa `_ - tiny web auditor that has a lot of security checks for the webpages +- `XSStrike `_ - automated tool to check that your application is not vulnerable to ``xss`` errors +- `docker-bench `_ - a script that checks for dozens of common best-practices around deploying Docker containers in production +- `lynis `_ - a battle-tested security tool for systems running Linux, macOS, or Unix-based operating system +- `trivy `_ - a simple and comprehensive vulnerability scanner for containers + +But, even after all you attempts to secure your application, +it **won't be 100% safe**. Do not fall into this false feeling of security. + + +Further reading +--------------- + +- `Open Web Application Security Project `_ +- `Docker security `_ +- `AppArmor `_ and `bane `_ diff --git a/new-github-repos/docs/pages/template/testing.rst b/new-github-repos/docs/pages/template/testing.rst new file mode 100644 index 0000000..a689760 --- /dev/null +++ b/new-github-repos/docs/pages/template/testing.rst @@ -0,0 +1,111 @@ +Testing +======= + +We try to keep our quality standards high. +So, we use different tools to make this possible. + +We use `mypy `_ for optional +static typing. +We run tests with `pytest `_ framework. + + +pytest +------ + +``pytest`` is the main tool for test discovery, collection, and execution. +It is configured inside ``setup.cfg`` file. + +We use a lot of ``pytest`` plugins that enhance our development experience. +List of these plugins is available inside ``pyproject.toml`` file. + +Running: + +.. code:: bash + + pytest + +We also have some options that are set on each run via ``--addopts`` +inside the ``setup.cfg`` file. + +Plugins +~~~~~~~ + +We use different ``pytest`` plugins to make our testing process better. +Here's the full list of things we use: + +- `pytest-django`_ - plugin that introduce a lot of ``django`` specific + helpers, fixtures, and configuration +- `django-test-migrations`_ - plugin to test Django migrations and their order +- `pytest-cov`_ - plugin to measure test coverage +- `pytest-randomly`_ - plugin to execute tests in random order and + also set predictable random seed, so you can easily debug + what went wrong for tests that rely on random behavior +- `pytest-deadfixtures`_ - plugin to find unused or duplicate fixtures +- `pytest-timeout`_ - plugin to raise errors for tests + that take too long to finish, this way you can control test execution speed +- `pytest-testmon`_ - plugin for `Test Driven Development`_ which executes + tests that are affected by your code changes + +.. _pytest-django: https://github.com/pytest-dev/pytest-django +.. _django-test-migrations: https://github.com/wemake-services/django-test-migrations +.. _pytest-cov: https://github.com/pytest-dev/pytest-cov +.. _pytest-randomly: https://github.com/pytest-dev/pytest-randomly +.. _pytest-deadfixtures: https://github.com/jllorencetti/pytest-deadfixtures +.. _pytest-timeout: https://pypi.org/project/pytest-timeout +.. _pytest-testmon: https://github.com/tarpas/pytest-testmon +.. _`Test Driven Development`: https://en.wikipedia.org/wiki/Test-driven_development + +Tweaking tests performance +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are several options you can provide or remove to make your tests faster: + +- You can use ``pytest-xdist`` together with + ``-n auto`` to schedule several numbers of workers, + sometimes when there are a lot of tests it may increase the testing speed. + But on a small project with a small amount of test it just + gives you an overhead, so removing it (together with `--boxed`) + will boost your testing performance +- If there are a lot of tests with database access + it may be wise to add + `--reuse-db option `_, + so ``django`` won't recreate database on each test +- If there are a lot of migrations to perform you may also add + `--nomigrations option `_, + so ``django`` won't run all the migrations + and instead will inspect and create models directly +- Removing ``coverage``. Sometimes that an option. + When running tests in TDD style why would you need such a feature? + So, coverage will be calculated when you will ask for it. + That's a huge speed up +- Removing linters. Sometimes you may want to split linting and testing phases. + This might be useful when you have a lot of tests, and you want to run + linters before, so it won't fail your complex testing pyramid with a simple + whitespace violation + + +mypy +---- + +Running ``mypy`` is required before any commit: + +.. code:: bash + + mypy server tests/**/*.py + +This will eliminate a lot of possible ``TypeError`` and other issues +in both `server/` and `tests/` directories. +We use `tests/**/*.py` because `tests/` is not a python package, +so it is not importable. + +However, this will not make code 100% safe from errors. +So, both the testing and review process are still required. + +``mypy`` is configured via ``setup.cfg``. +Read the `docs `_ +for more information. + +We also use `django-stubs `_ +to type ``django`` internals. +This package is optional and can be removed, +if you don't want to type your ``django`` for some reason. diff --git a/new-github-repos/docs/pages/template/troubleshooting.rst b/new-github-repos/docs/pages/template/troubleshooting.rst new file mode 100644 index 0000000..0f5a294 --- /dev/null +++ b/new-github-repos/docs/pages/template/troubleshooting.rst @@ -0,0 +1,47 @@ +Troubleshooting +=============== + +This section is about some of the problems you may encounter and +how to solve these problems. + + +Docker +------ + +Pillow +~~~~~~ + +If you want to install ``Pillow`` that you should +add this to dockerfile and rebuild image: + +- ``RUN apk add jpeg-dev zlib-dev`` +- ``LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "poetry install ..."`` + +See ``_ + +Root owns build artifacts +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This happens on some systems. +It happens because build happens in ``docker`` as the ``root`` user. +The fix is to pass current ``UID`` to ``docker``. +See ``_. + +MacOS performance +~~~~~~~~~~~~~~~~~ + +If you use the MacOS you +know that you have problems with disk performance. +Starting and restarting an application is slower than with Linux +(it's very noticeable for project with large codebase). +For particular solve this problem add ``:delegated`` to each +your volumes in ``docker-compose.yml`` file. + +.. code:: yaml + + volumes: + - pgdata:/var/lib/postgresql/data:delegated + +For more information, you can look at the +`docker documents `_ +and a good `article `_. diff --git a/new-github-repos/docs/pages/template/upgrading-template.rst b/new-github-repos/docs/pages/template/upgrading-template.rst new file mode 100644 index 0000000..15230f3 --- /dev/null +++ b/new-github-repos/docs/pages/template/upgrading-template.rst @@ -0,0 +1,51 @@ +Upgrading template +================== + +Upgrading your project to be up-to-date with this template is a primary goal. +This is achieved by manually applying ``diff`` to your existing code. + +``diff`` can be viewed from the project's ``README.md``. +See `an example `_. + +When the upgrade is applied just change the commit hash in your template +to the most recent one. + + +Versions +-------- + +Sometimes, when we break something heavily, we create a version. +That's is required for our users, so they can use old releases to create +projects as they used to be a long time ago. + +However, we do not officially support older versions. +And we do not recommend to use them. + +A full list of versions can be `found here `_. + + +Migration guides +---------------- + +Each time we create a new version, we also provide a migration guide. +What is a migration guide? +It is something you have to do to your project +other than just copy-pasting diffs from new versions. + +Goodbye, pipenv! +~~~~~~~~~~~~~~~~ + +This version requires a manual migration step. + +1. You need to install ``poetry`` +2. You need to create a new ``pyproject.toml`` file with ``poetry init`` +3. You need to adjust name, version, description, and authors meta fields +4. You need to copy-paste dependencies from ``Pipfile`` to ``pyproject.toml`` +5. You need to set correct version for each dependency in the list, + use ``"^x.y"`` `notation `_ +6. You need to adjust ``[build-system]`` tag and ``POETRY_VERSION`` variable + to fit your ``poetry`` version +7. Create ``poetry.lock`` file with ``poetry lock`` + +It should be fine! You may, however, experience some bugs related to different +dependency version resolution mechanisms. But, ``poetry`` does it better. diff --git a/new-github-repos/docs/requirements.txt b/new-github-repos/docs/requirements.txt new file mode 100644 index 0000000..a07079a --- /dev/null +++ b/new-github-repos/docs/requirements.txt @@ -0,0 +1,6 @@ +# This file is required since we use ReadTheDocs services. +# If it is not used, feel free to delete it. + +sphinx==4.0.3 +sphinx_autodoc_typehints==1.12.0 +tomlkit==0.7.2 diff --git a/new-github-repos/locale/.gitkeep b/new-github-repos/locale/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/manage.py b/new-github-repos/manage.py new file mode 100755 index 0000000..4038558 --- /dev/null +++ b/new-github-repos/manage.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import os +import sys + + +def main() -> None: + """ + Main function. + + It does several things: + 1. Sets default settings module, if it is not set + 2. Warns if Django is not installed + 3. Executes any given command + """ + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + + try: + from django.core import management # noqa: WPS433 + except ImportError: + 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?', + ) + + management.execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/new-github-repos/poetry.lock b/new-github-repos/poetry.lock new file mode 100644 index 0000000..fb8a0f1 --- /dev/null +++ b/new-github-repos/poetry.lock @@ -0,0 +1,2719 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "astor" +version = "0.8.1" +description = "Read/rewrite/write Python ASTs" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "autorepr" +version = "0.3.0" +description = "Makes civilized __repr__, __str__, and __unicode__ methods" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "babel" +version = "2.9.1" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "bandit" +version = "1.7.0" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "blinker" +version = "1.4" +description = "Fast, simple object-to-object and broadcast signaling" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "cachy" +version = "0.3.0" +description = "Cachy provides a simple yet effective caching library." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +redis = ["redis (>=3.3.6,<4.0.0)"] +memcached = ["python-memcached (>=1.59,<2.0)"] +msgpack = ["msgpack-python (>=0.5,<0.6)"] + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.5" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click-default-group" +version = "1.2.2" +description = "Extends click.Group to invoke a command without explicit subcommand name" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "configupdater" +version = "2.0" +description = "Parser like ConfigParser but for updating configuration files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +testing = ["sphinx", "flake8", "pytest", "pytest-cov", "pytest-virtualenv", "pytest-xdist"] + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "darglint" +version = "1.8.0" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "decorator" +version = "5.0.9" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "dennis" +version = "0.9" +description = "Utilities for working with PO and POT files to ease development and improve localization quality" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=6" +polib = ">=1.0.8" + +[[package]] +name = "dictdiffer" +version = "0.8.1" +description = "Dictdiffer is a library that helps you to diff and patch dictionaries." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +all = ["Sphinx (>=1.4.4)", "sphinx-rtd-theme (>=0.1.9)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "mock (>=1.3.0)", "pydocstyle (>=1.0.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)", "tox (>=3.7.0)", "numpy (>=1.11.0)"] +docs = ["Sphinx (>=1.4.4)", "sphinx-rtd-theme (>=0.1.9)"] +numpy = ["numpy (>=1.11.0)"] +tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "mock (>=1.3.0)", "pydocstyle (>=1.0.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)", "tox (>=3.7.0)"] + +[[package]] +name = "django" +version = "3.2.5" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-axes" +version = "5.20.0" +description = "Keep track of failed login attempts in Django-powered sites." +category = "main" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +django = ">=2.2" +django-ipware = ">=3,<4" + +[[package]] +name = "django-coverage-plugin" +version = "2.0.0" +description = "Django template coverage.py plugin" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +coverage = "*" +six = ">=1.4.0" + +[[package]] +name = "django-csp" +version = "3.7" +description = "Django Content Security Policy support." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=1.8" + +[package.extras] +jinja2 = ["jinja2 (>=2.9.6)"] +tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"] + +[[package]] +name = "django-debug-toolbar" +version = "3.2.1" +description = "A configurable set of panels that display various debug information about the current request/response." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" +sqlparse = ">=0.2.0" + +[[package]] +name = "django-extra-checks" +version = "0.11.0" +description = "Collection of useful checks for Django Checks Framework" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pytest (>=6,<7)", "pytest-cov (>=2,<3)", "pytest-django (>=4,<5)", "Django (>=3.1,<3.2)", "djangorestframework (>=3.12)", "django-stubs (==1.7.0)", "djangorestframework-stubs (==1.3.0)", "flake8 (==3.8.0)", "flake8-bugbear (==20.11.1)", "pre-commit (==2.9.3)", "isort (>=5,<6)", "pdbpp", "tox (>=3,<4)", "black (==20.8b1)"] + +[[package]] +name = "django-health-check" +version = "3.16.4" +description = "Run checks on services like databases, queue servers, celery processes, etc." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django = ">=2.2" + +[[package]] +name = "django-http-referrer-policy" +version = "1.1.1" +description = "django-http-referrer-policy provides a middleware class implementing the Referrer-Policy header for Django-powered sites." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.dependencies] +Django = ">=1.11" + +[[package]] +name = "django-ipware" +version = "3.0.2" +description = "A Django utility application that returns client's real IP address" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "django-migration-linter" +version = "3.0.0" +description = "Detect backward incompatible migrations for your django project" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +appdirs = ">=1.4.3" +django = ">=1.11" +six = ">=1.14.0" +toml = ">=0.10.2" + +[package.extras] +test = ["tox (>=3.15.2)", "mysqlclient (>=1.4.6)", "psycopg2-binary (>=2.8.5,<2.9)", "django-add-default-value (>=0.4.0)", "coverage (>=5.5)", "mock (>=3.0.5)", "backports.tempfile (>=1.0)"] + +[[package]] +name = "django-permissions-policy" +version = "4.1.0" +description = "Set the draft security HTTP header Permissions-Policy (previously Feature-Policy) on your Django app." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "django-querycount" +version = "0.7.0" +description = "Middleware that Prints the number of DB queries to the runserver console." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "django-split-settings" +version = "1.0.1" +description = "Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards and optional settings files." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "django-stubs" +version = "1.8.0" +description = "Mypy stubs for Django" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = "*" +django-stubs-ext = "*" +mypy = ">=0.790" +typing-extensions = "*" + +[[package]] +name = "django-stubs-ext" +version = "0.2.0" +description = "Monkey-patching and extensions for django-stubs" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = "*" + +[[package]] +name = "django-test-migrations" +version = "1.1.0" +description = "Test django schema and data migrations, including ordering" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +typing_extensions = ">=3.7.4,<4.0.0" + +[[package]] +name = "doc8" +version = "0.8.1" +description = "Style checker for Sphinx (or other) RST documentation" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +chardet = "*" +docutils = "*" +Pygments = "*" +restructuredtext-lint = ">=0.7" +six = "*" +stevedore = "*" + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dotenv-linter" +version = "0.2.0" +description = "Linting dotenv files like a charm!" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +click = ">=6,<8" +click_default_group = ">=1.2,<2.0" +ply = ">=3.11,<4.0" +typing_extensions = ">=3.6,<4.0" + +[[package]] +name = "dparse" +version = "0.5.1" +description = "A parser for Python dependency files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +packaging = "*" +pyyaml = "*" +toml = "*" + +[package.extras] +pipenv = ["pipenv"] + +[[package]] +name = "dump-env" +version = "1.3.0" +description = "A utility tool to create .env files" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "eradicate" +version = "2.0.0" +description = "Removes commented-out code." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "flake8-bandit" +version = "2.1.2" +description = "Automated security testing with bandit and flake8." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bandit = "*" +flake8 = "*" +flake8-polyfill = "*" +pycodestyle = "*" + +[[package]] +name = "flake8-broken-line" +version = "0.3.0" +description = "Flake8 plugin to forbid backslashes for line breaks" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +flake8 = ">=3.5,<4.0" + +[[package]] +name = "flake8-bugbear" +version = "21.4.3" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "black", "hypothesis", "hypothesmith"] + +[[package]] +name = "flake8-commas" +version = "2.0.0" +description = "Flake8 lint for trailing commas." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=2,<4.0.0" + +[[package]] +name = "flake8-comprehensions" +version = "3.5.0" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +flake8 = ">=3.0,<3.2.0 || >3.2.0,<4" + +[[package]] +name = "flake8-debugger" +version = "4.0.0" +description = "ipdb/pdb statement checker plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +flake8 = ">=3.0" +pycodestyle = "*" +six = "*" + +[[package]] +name = "flake8-django" +version = "1.1.2" +description = "Plugin to catch bad style specific to Django Projects." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +flake8 = ">=3.8.4,<4.0.0" + +[[package]] +name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-eradicate" +version = "1.1.0" +description = "Flake8 plugin to find commented out code" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">=3.5,<4.0" + +[[package]] +name = "flake8-isort" +version = "4.0.0" +description = "flake8 plugin that integrates isort ." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.2.1,<4" +isort = ">=4.3.5,<6" +testfixtures = ">=6.8.0,<7" + +[package.extras] +test = ["pytest (>=4.0.2,<6)", "toml"] + +[[package]] +name = "flake8-logging-format" +version = "0.6.0" +description = "Flake8 extension to validate (lack of) logging format strings" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8-plugin-utils" +version = "1.3.2" +description = "The package provides base classes and utils for flake8 plugin writing" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-pytest-style" +version = "1.5.0" +description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +flake8-plugin-utils = ">=1.3.2,<2.0.0" + +[[package]] +name = "flake8-quotes" +version = "3.2.0" +description = "Flake8 lint for quotes." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-rst-docstrings" +version = "0.2.3" +description = "Python docstring reStructuredText (RST) validator" +category = "dev" +optional = false +python-versions = ">=3.3" + +[package.dependencies] +flake8 = ">=3.0.0" +pygments = "*" +restructuredtext-lint = "*" + +[[package]] +name = "flake8-string-format" +version = "0.3.0" +description = "string format checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[package]] +name = "gitpython" +version = "3.1.18" +description = "Python Git Library" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "hypothesis" +version = "6.14.1" +description = "A library for property-based testing" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] +cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["pytz (>=2014.1)", "django (>=2.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark-parser (>=0.6.5)"] +numpy = ["numpy (>=1.9.0)"] +pandas = ["pandas (>=0.25)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] + +[[package]] +name = "identify" +version = "2.2.10" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.extras] +license = ["editdistance-s"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "4.6.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "ipython" +version = "7.25.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.9.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "jedi" +version = "0.18.0" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] + +[[package]] +name = "jinja2" +version = "3.0.1" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "0.10.0" +description = "JSON Matching Expressions" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "loguru" +version = "0.5.3" +description = "Python logging made (stupidly) simple" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "marshmallow" +version = "3.12.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] +lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-polyfield" +version = "5.10" +description = "An unofficial extension to Marshmallow to allow for polymorphic fields" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +marshmallow = ">=3.0.0b10" + +[[package]] +name = "matplotlib-inline" +version = "0.1.2" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "more-itertools" +version = "8.8.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nitpick" +version = "0.26.0" +description = "Enforce the same configuration across multiple projects" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +attrs = "*" +autorepr = "*" +cachy = "*" +click = "*" +ConfigUpdater = "*" +dictdiffer = "*" +flake8 = ">=3.0.0" +identify = "*" +jmespath = "*" +loguru = "*" +marshmallow = ">=3.0.0b10" +marshmallow-polyfield = ">=5.10,<6.0" +more-itertools = "*" +pluggy = "*" +pydantic = "*" +python-slugify = "*" +requests = "*" +"ruamel.yaml" = "*" +sortedcontainers = "*" +toml = "*" +tomlkit = "*" + +[package.extras] +test = ["freezegun", "pytest-cov", "pytest", "responses", "testfixtures"] +lint = ["pylint"] +doc = ["sphinx", "sphinx-rtd-theme", "sphobjinv"] + +[[package]] +name = "nplusone" +version = "1.0.0" +description = "Detecting the n+1 queries problem in Python" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +blinker = ">=1.3" +six = ">=1.9.0" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "parso" +version = "0.8.2" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pbr" +version = "5.6.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pep8-naming" +version = "0.11.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "polib" +version = "1.1.1" +description = "A library to manipulate gettext files (po and mo files)." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "polint" +version = "0.4" +description = "Linter for gettext PO files" +category = "dev" +optional = false +python-versions = ">2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.dependencies] +docopt = "*" +polib = "*" + +[package.extras] +quality = ["flake8", "isort", "pydocstyle"] +tests = ["mock"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.19" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2-binary" +version = "2.8.6" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.9.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-deadfixtures" +version = "2.2.1" +description = "A simple plugin to list unused fixtures in pytest" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=3.0.0" + +[[package]] +name = "pytest-django" +version = "4.4.0" +description = "A Django plugin for pytest." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytest-randomly" +version = "3.8.0" +description = "Pytest plugin to randomly order tests and control random.seed." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +pytest = "*" + +[[package]] +name = "pytest-testmon" +version = "1.1.1" +description = "selects tests affected by changed files and methods" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = ">=4,<6" +pytest = ">=5,<7" + +[[package]] +name = "pytest-timeout" +version = "1.4.2" +description = "py.test plugin to abort hanging tests" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=3.6.0" + +[[package]] +name = "python-decouple" +version = "3.4" +description = "Strict separation of settings from code." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-slugify" +version = "5.0.2" +description = "A Python Slugify application that handles Unicode" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "restructuredtext-lint" +version = "1.3.2" +description = "reStructuredText linter" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +docutils = ">=0.11,<1.0" + +[[package]] +name = "ruamel.yaml" +version = "0.17.10" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "dev" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.6" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "safety" +version = "1.10.3" +description = "Checks installed dependencies for known vulnerabilities." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +Click = ">=6.0" +dparse = ">=0.5.1" +packaging = "*" +requests = "*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "4.0.3" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "1.12.0" +description = "Type hints (PEP 484) support for the Sphinx autodoc extension" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Sphinx = ">=3.0" + +[package.extras] +test = ["pytest (>=3.1.0)", "typing-extensions (>=3.5)", "sphobjinv (>=2.0)", "Sphinx (>=3.2.0)", "dataclasses"] +type_comments = ["typed-ast (>=1.4.0)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sqlparse" +version = "0.4.1" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "stevedore" +version = "3.3.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "structlog" +version = "21.1.0" +description = "Structured Logging for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] +docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] + +[[package]] +name = "testfixtures" +version = "6.17.1" +description = "A collection of helpers and mock objects for unit tests and doc tests." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +build = ["setuptools-git", "wheel", "twine"] +docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomlkit" +version = "0.7.2" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "traitlets" +version = "5.0.5" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +ipython-genutils = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "wemake-python-styleguide" +version = "0.15.3" +description = "The strictest and most opinionated python linter ever" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +astor = ">=0.8,<0.9" +attrs = "*" +darglint = ">=1.2,<2.0" +flake8 = ">=3.7,<4.0" +flake8-bandit = ">=2.1,<3.0" +flake8-broken-line = ">=0.3,<0.4" +flake8-bugbear = ">=20.1,<22.0" +flake8-commas = ">=2.0,<3.0" +flake8-comprehensions = ">=3.1,<4.0" +flake8-debugger = ">=4.0,<5.0" +flake8-docstrings = ">=1.3,<2.0" +flake8-eradicate = ">=1.0,<2.0" +flake8-isort = ">=4.0,<5.0" +flake8-quotes = ">=3.0,<4.0" +flake8-rst-docstrings = ">=0.2.3,<0.3.0" +flake8-string-format = ">=0.3,<0.4" +pep8-naming = ">=0.11,<0.12" +pygments = ">=2.4,<3.0" +typing_extensions = ">=3.6,<4.0" + +[[package]] +name = "win32-setctime" +version = "1.0.3" +description = "A small Python utility to set file creation time on Windows" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] + +[[package]] +name = "yamllint" +version = "1.26.1" +description = "A linter for YAML files." +category = "dev" +optional = false +python-versions = ">=3.5.*" + +[package.dependencies] +pathspec = ">=0.5.3" +pyyaml = "*" + +[[package]] +name = "zipp" +version = "3.5.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "3.8.9" +content-hash = "44d732e8d5e1308964707fbf89dfe49a552ab69c7e8e942dc10c17cf3d86c426" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +astor = [ + {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, + {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +autorepr = [ + {file = "autorepr-0.3.0-py2-none-any.whl", hash = "sha256:c34567e4073630feb52d9c788fc198085e9e9de4817e3b93b7c4c534fc689f11"}, + {file = "autorepr-0.3.0-py2.py3-none-any.whl", hash = "sha256:1d9010d14fb325d3961e3aa73692685563f97d6ba4a2f0f735329fb37422599c"}, + {file = "autorepr-0.3.0.tar.gz", hash = "sha256:ef770b84793d5433e6bb893054973b8c7ce6b487274f9c3f734f678cae11e85e"}, +] +babel = [ + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +bandit = [ + {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, + {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +blinker = [ + {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"}, +] +cachy = [ + {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, + {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +cffi = [ + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +click-default-group = [ + {file = "click-default-group-1.2.2.tar.gz", hash = "sha256:d9560e8e8dfa44b3562fbc9425042a0fd6d21956fcc2db0077f63f34253ab904"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +configupdater = [ + {file = "ConfigUpdater-2.0-py2.py3-none-any.whl", hash = "sha256:bc62bd5141c45a89840a3e82e0a06f23fb2c00de82e2b72c8030cafb4daea9a2"}, + {file = "ConfigUpdater-2.0.tar.gz", hash = "sha256:6a60447fb25e5cb5036cdd5761287ac5649135a49094bc8bd71d999417483441"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +darglint = [ + {file = "darglint-1.8.0-py3-none-any.whl", hash = "sha256:ac6797bcc918cd8d8f14c168a4a364f54e1aeb4ced59db58e7e4c6dfec2fe15c"}, + {file = "darglint-1.8.0.tar.gz", hash = "sha256:aa605ef47817a6d14797d32b390466edab621768ea4ca5cc0f3c54f6d8dcaec8"}, +] +decorator = [ + {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, + {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, +] +dennis = [ + {file = "dennis-0.9-py2.py3-none-any.whl", hash = "sha256:f6487392ac91800c5f0684a99b404b7fd0f72ceb48faeb5a0ce4e2c24fb62d3f"}, + {file = "dennis-0.9.tar.gz", hash = "sha256:8c942dd5da7d03c65daebc069c5ee5c7f1374ac9b0c8c89c627caa66fe822604"}, +] +dictdiffer = [ + {file = "dictdiffer-0.8.1-py2.py3-none-any.whl", hash = "sha256:d79d9a39e459fe33497c858470ca0d2e93cb96621751de06d631856adfd9c390"}, + {file = "dictdiffer-0.8.1.tar.gz", hash = "sha256:1adec0d67cdf6166bda96ae2934ddb5e54433998ceab63c984574d187cc563d2"}, +] +django = [ + {file = "Django-3.2.5-py3-none-any.whl", hash = "sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"}, + {file = "Django-3.2.5.tar.gz", hash = "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd"}, +] +django-axes = [ + {file = "django-axes-5.20.0.tar.gz", hash = "sha256:fe2c36a2252e1936e901d87bf49249aa8ac33655dd47c4083ba5ff56512cc247"}, + {file = "django_axes-5.20.0-py3-none-any.whl", hash = "sha256:7128589d9002216f5131be2581c4ef9c2039d59624406e2dcad2624bff850304"}, +] +django-coverage-plugin = [ + {file = "django_coverage_plugin-2.0.0.tar.gz", hash = "sha256:5a7ac412528876563a45f9b54ad9962e33e5f95b409843c4c6c92cb0247eee66"}, +] +django-csp = [ + {file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"}, + {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, +] +django-debug-toolbar = [ + {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, + {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, +] +django-extra-checks = [ + {file = "django-extra-checks-0.11.0.tar.gz", hash = "sha256:61681433f2df8b8932676c216472b7f5554bda0018058d0a0bac89bf377347bd"}, + {file = "django_extra_checks-0.11.0-py3-none-any.whl", hash = "sha256:b1a19c03243d67c380c8af60c1ef837323c21e2fd888e0fd532d1df5f1866ba0"}, +] +django-health-check = [ + {file = "django-health-check-3.16.4.tar.gz", hash = "sha256:334bcbbb9273a6dbd9c928e78474306e623dfb38cc442281cb9fd230a20a7fdb"}, + {file = "django_health_check-3.16.4-py2.py3-none-any.whl", hash = "sha256:86a8869d67e72394a1dd73e37819a7d2cfd915588b96927fda611d7451fd4735"}, +] +django-http-referrer-policy = [ + {file = "django-http-referrer-policy-1.1.1.tar.gz", hash = "sha256:917f5ed62054b27eff3172b7eccfb018469e85ddd538767328553282a70493c3"}, + {file = "django_http_referrer_policy-1.1.1-py2.py3-none-any.whl", hash = "sha256:7617d1256f1ab80c1a12bffdd8c8d24b94093eb1e21d035e2aa6d7d3c5ac999e"}, +] +django-ipware = [ + {file = "django-ipware-3.0.2.tar.gz", hash = "sha256:c7df8e1410a8e5d6b1fbae58728402ea59950f043c3582e033e866f0f0cf5e94"}, +] +django-migration-linter = [ + {file = "django-migration-linter-3.0.0.tar.gz", hash = "sha256:a45d955bbd01fff57d19978cd7e99599e7592fb94c27032edfd026ef51e45a05"}, + {file = "django_migration_linter-3.0.0-py2.py3-none-any.whl", hash = "sha256:beb62cf9777b6e3a9d902726fd5e514f73edae3398e1e948c5497c34315c87ad"}, +] +django-permissions-policy = [ + {file = "django-permissions-policy-4.1.0.tar.gz", hash = "sha256:30df0dfe6579f60743d181378c7cbbb44526a5df469aa633251234387d3819f1"}, + {file = "django_permissions_policy-4.1.0-py3-none-any.whl", hash = "sha256:774ca81646c84793c8c5645e581ecd4d08a88d492cf90c8983648d8fddcbb3c8"}, +] +django-querycount = [ + {file = "django-querycount-0.7.0.tar.gz", hash = "sha256:8f5123d78716ff0704f2373e746a7200b8d8417798ce4a99bf2de87e3768f9ce"}, +] +django-split-settings = [ + {file = "django-split-settings-1.0.1.tar.gz", hash = "sha256:2da16cd967cd38315ec7ff0ae0c9db8488f8528bb2e5de26cd898328dc4bbeac"}, + {file = "django_split_settings-1.0.1-py3-none-any.whl", hash = "sha256:8d636649023289d0ef0ba08b0a4f37761adc94a29ee0ebfe65922c3cb0594ede"}, +] +django-stubs = [ + {file = "django-stubs-1.8.0.tar.gz", hash = "sha256:717967d7fee0a6af0746724a0be80d72831a982a40fa8f245a6a46f4cafd157b"}, + {file = "django_stubs-1.8.0-py3-none-any.whl", hash = "sha256:bde9e44e3c4574c2454e74a3e607cc3bc23b0441bb7d1312cd677d5e30984b74"}, +] +django-stubs-ext = [ + {file = "django-stubs-ext-0.2.0.tar.gz", hash = "sha256:c14f297835a42c1122421ec7e2d06579996b29d33b8016002762afa5d78863af"}, + {file = "django_stubs_ext-0.2.0-py3-none-any.whl", hash = "sha256:bd4a1e36ef2ba0ef15801933c85c68e59b383302c873795c6ecfc25950c7ecdb"}, +] +django-test-migrations = [ + {file = "django-test-migrations-1.1.0.tar.gz", hash = "sha256:27c0127552920bbdc339a84de360f1792abc8c353e2c8d2b86af92dc1ade6703"}, + {file = "django_test_migrations-1.1.0-py3-none-any.whl", hash = "sha256:7ea17dac1a0b0c8084681899c6563d85f4262832f2fbb0c6240b12e554333934"}, +] +doc8 = [ + {file = "doc8-0.8.1-py2.py3-none-any.whl", hash = "sha256:4d58a5c8c56cedd2b2c9d6e3153be5d956cf72f6051128f0f2255c66227df721"}, + {file = "doc8-0.8.1.tar.gz", hash = "sha256:4d1df12598807cf08ffa9a1d5ef42d229ee0de42519da01b768ff27211082c12"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +dotenv-linter = [ + {file = "dotenv-linter-0.2.0.tar.gz", hash = "sha256:c99b981966450e48007b92547ed8e16355ec88f1dac5494a4576440546467cf7"}, + {file = "dotenv_linter-0.2.0-py3-none-any.whl", hash = "sha256:536992ef6f6bf803e58e2984cf428486491059ca750b13bd9eb6b5186110709a"}, +] +dparse = [ + {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, + {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, +] +dump-env = [ + {file = "dump-env-1.3.0.tar.gz", hash = "sha256:6c2874c7f30c7cea945f3438e37e67e081644bd6c5383189613e5fc31c277837"}, + {file = "dump_env-1.3.0-py3-none-any.whl", hash = "sha256:c2f3bdda16f0dbb79fca0b22f8df763cff4009d311342c73ce36bcd1ad8b007c"}, +] +eradicate = [ + {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +flake8-bandit = [ + {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, +] +flake8-broken-line = [ + {file = "flake8-broken-line-0.3.0.tar.gz", hash = "sha256:f74e052833324a9e5f0055032f7ccc54b23faabafe5a26241c2f977e70b10b50"}, + {file = "flake8_broken_line-0.3.0-py3-none-any.whl", hash = "sha256:611f79c7f27118e7e5d3dc098ef7681c40aeadf23783700c5dbee840d2baf3af"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-21.4.3.tar.gz", hash = "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7"}, + {file = "flake8_bugbear-21.4.3-py36.py37.py38-none-any.whl", hash = "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa"}, +] +flake8-commas = [ + {file = "flake8-commas-2.0.0.tar.gz", hash = "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7"}, + {file = "flake8_commas-2.0.0-py2.py3-none-any.whl", hash = "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e"}, +] +flake8-comprehensions = [ + {file = "flake8-comprehensions-3.5.0.tar.gz", hash = "sha256:f24be9032587127f7a5bc6d066bf755b6e66834f694383adb8a673e229c1f559"}, + {file = "flake8_comprehensions-3.5.0-py3-none-any.whl", hash = "sha256:b07aef3277623db32310aa241a1cec67212b53c1d18e767d7e26d4d83aa05bf7"}, +] +flake8-debugger = [ + {file = "flake8-debugger-4.0.0.tar.gz", hash = "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6"}, + {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"}, +] +flake8-django = [ + {file = "flake8-django-1.1.2.tar.gz", hash = "sha256:b4314abb5bacda450d2eae564a0604447111b1b98188e46bca41682ad2ab59d6"}, + {file = "flake8_django-1.1.2-py3-none-any.whl", hash = "sha256:f8bfdbe8352c2c5f3788c2a2f6652dd2604af24af07a5aa112206d63ae228fdc"}, +] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] +flake8-eradicate = [ + {file = "flake8-eradicate-1.1.0.tar.gz", hash = "sha256:f5917d6dbca352efcd10c15fdab9c55c48f0f26f6a8d47898b25d39101f170a8"}, + {file = "flake8_eradicate-1.1.0-py3-none-any.whl", hash = "sha256:d8e39b684a37c257a53cda817d86e2d96c9ba3450ddc292742623a5dfee04d9e"}, +] +flake8-isort = [ + {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, + {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, +] +flake8-logging-format = [ + {file = "flake8-logging-format-0.6.0.tar.gz", hash = "sha256:ca5f2b7fc31c3474a0aa77d227e022890f641a025f0ba664418797d979a779f8"}, +] +flake8-plugin-utils = [ + {file = "flake8-plugin-utils-1.3.2.tar.gz", hash = "sha256:20fa2a8ca2decac50116edb42e6af0a1253ef639ad79941249b840531889c65a"}, + {file = "flake8_plugin_utils-1.3.2-py3-none-any.whl", hash = "sha256:1fe43e3e9acf3a7c0f6b88f5338cad37044d2f156c43cb6b080b5f9da8a76f06"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +flake8-pytest-style = [ + {file = "flake8-pytest-style-1.5.0.tar.gz", hash = "sha256:668ce8f55edf7db4ac386d2735c3b354b5cb47aa341a4655d91a5788dd03124b"}, + {file = "flake8_pytest_style-1.5.0-py3-none-any.whl", hash = "sha256:ec287a7dc4fe95082af5e408c8b2f8f4b6bcb366d5a17ff6c34112eb03446580"}, +] +flake8-quotes = [ + {file = "flake8-quotes-3.2.0.tar.gz", hash = "sha256:3f1116e985ef437c130431ac92f9b3155f8f652fda7405ac22ffdfd7a9d1055e"}, +] +flake8-rst-docstrings = [ + {file = "flake8-rst-docstrings-0.2.3.tar.gz", hash = "sha256:3045794e1c8467fba33aaea5c246b8369efc9c44ef8b0b20199bb6df7a4bd47b"}, + {file = "flake8_rst_docstrings-0.2.3-py3-none-any.whl", hash = "sha256:565bbb391d7e4d0042924102221e9857ad72929cdd305b26501736ec22c1451a"}, +] +flake8-string-format = [ + {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, + {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, +] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +gitpython = [ + {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"}, + {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"}, +] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] +hypothesis = [ + {file = "hypothesis-6.14.1-py3-none-any.whl", hash = "sha256:6dad44da8962cc7c13cdc28cf9b78c6779b5a0b0279a9baac427ae6d109f70e3"}, + {file = "hypothesis-6.14.1.tar.gz", hash = "sha256:88b0736a5691b68b8e16a4b7ee8e4c8596810c5a20989ea5b5f64870a7c25740"}, +] +identify = [ + {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"}, + {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +ipython = [ + {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, + {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +isort = [ + {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, + {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, +] +jedi = [ + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, +] +jinja2 = [ + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, +] +jmespath = [ + {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, + {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, +] +loguru = [ + {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, + {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +marshmallow = [ + {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, + {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, +] +marshmallow-polyfield = [ + {file = "marshmallow-polyfield-5.10.tar.gz", hash = "sha256:75d0e31b725650e91428f975a66ed30f703cc6f9fcfe45b8436ee6d676921691"}, + {file = "marshmallow_polyfield-5.10-py3-none-any.whl", hash = "sha256:a0a91730c6d21bfac1563990c7ba1413928e7499af669619d4fb38d1fb25c4e9"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, + {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"}, + {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"}, +] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nitpick = [ + {file = "nitpick-0.26.0-py3-none-any.whl", hash = "sha256:f5cfcd68f83910ce8a4cd998804a4e15dfd5a9a54a3cb4e201f1ec4bee67fe89"}, + {file = "nitpick-0.26.0.tar.gz", hash = "sha256:b11009c77975990d7776ea6d307ed4272f5122e83b2fc16a6bad557222b6d809"}, +] +nplusone = [ + {file = "nplusone-1.0.0-py2.py3-none-any.whl", hash = "sha256:96b1e6e29e6af3e71b67d0cc012a5ec8c97c6a2f5399f4ba41a2bbe0e253a9ac"}, + {file = "nplusone-1.0.0.tar.gz", hash = "sha256:1726c0a10c0aa7eabb04e24db2882ff97b6b7ee29d729a8d97dcbd12ef5a5651"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +parso = [ + {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, + {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pbr = [ + {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, + {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, +] +pep8-naming = [ + {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, + {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +ply = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] +polib = [ + {file = "polib-1.1.1-py2.py3-none-any.whl", hash = "sha256:d3ee85e0c6788f789353416b1612c6c92d75fe6ccfac0029711974d6abd0f86d"}, + {file = "polib-1.1.1.tar.gz", hash = "sha256:e02c355ae5e054912e3b0d16febc56510eff7e49d60bf22aecb463bd2f2a2dfa"}, +] +polint = [ + {file = "polint-0.4.tar.gz", hash = "sha256:51b68a8719aaa141a288718e950d5f5c529d6b8efbfd6e3e35ab951092a91edd"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, + {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, +] +psycopg2-binary = [ + {file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"}, + {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"}, + {file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"}, + {file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"}, + {file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"}, + {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"}, + {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"}, + {file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"}, + {file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"}, + {file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"}, + {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"}, + {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"}, + {file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"}, + {file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"}, + {file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"}, + {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"}, + {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"}, + {file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"}, + {file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"}, + {file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"}, + {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"}, + {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"}, + {file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"}, + {file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"}, + {file = "psycopg2_binary-2.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83"}, + {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"}, + {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"}, + {file = "psycopg2_binary-2.8.6-cp39-cp39-win32.whl", hash = "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056"}, + {file = "psycopg2_binary-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pygments = [ + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pytest-deadfixtures = [ + {file = "pytest-deadfixtures-2.2.1.tar.gz", hash = "sha256:ca15938a4e8330993ccec9c6c847383d88b3cd574729530647dc6b492daa9c1e"}, + {file = "pytest_deadfixtures-2.2.1-py2.py3-none-any.whl", hash = "sha256:db71533f2d9456227084e00a1231e732973e299ccb7c37ab92e95032ab6c083e"}, +] +pytest-django = [ + {file = "pytest-django-4.4.0.tar.gz", hash = "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"}, + {file = "pytest_django-4.4.0-py3-none-any.whl", hash = "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606"}, +] +pytest-randomly = [ + {file = "pytest-randomly-3.8.0.tar.gz", hash = "sha256:d9e21a72446757129378beea00bc9a32df1fb1cfd0bbe408be1ae9685bdf1209"}, + {file = "pytest_randomly-3.8.0-py3-none-any.whl", hash = "sha256:f5b7a09e84ee1eabcdedbb73c51d0929ae2f582bab6941dbb513bb49296d6340"}, +] +pytest-testmon = [ + {file = "pytest-testmon-1.1.1.tar.gz", hash = "sha256:c8810f991545e352f646fb382e5962ff54b8aa52b09d62d35ae04f0d7a9c58d9"}, +] +pytest-timeout = [ + {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"}, + {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"}, +] +python-decouple = [ + {file = "python-decouple-3.4.tar.gz", hash = "sha256:2e5adb0263a4f963b58d7407c4760a2465d464ee212d733e2a2c179e54c08d8f"}, + {file = "python_decouple-3.4-py3-none-any.whl", hash = "sha256:a8268466e6389a639a20deab9d880faee186eb1eb6a05e54375bdf158d691981"}, +] +python-slugify = [ + {file = "python-slugify-5.0.2.tar.gz", hash = "sha256:f13383a0b9fcbe649a1892b9c8eb4f8eab1d6d84b84bb7a624317afa98159cab"}, + {file = "python_slugify-5.0.2-py2.py3-none-any.whl", hash = "sha256:6d8c5df75cd4a7c3a2d21e257633de53f52ab0265cd2d1dc62a730e8194a7380"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +restructuredtext-lint = [ + {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.17.10-py3-none-any.whl", hash = "sha256:ffb9b703853e9e8b7861606dfdab1026cf02505bade0653d1880f4b2db47f815"}, + {file = "ruamel.yaml-0.17.10.tar.gz", hash = "sha256:106bc8d6dc6a0ff7c9196a47570432036f41d556b779c6b4e618085f57e39e67"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, + {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, +] +safety = [ + {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"}, + {file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] +sphinx = [ + {file = "Sphinx-4.0.3-py3-none-any.whl", hash = "sha256:5747f3c855028076fcff1e4df5e75e07c836f0ac11f7df886747231092cfe4ad"}, + {file = "Sphinx-4.0.3.tar.gz", hash = "sha256:dff357e6a208eb7edb2002714733ac21a9fe597e73609ff417ab8cf0c6b4fbb8"}, +] +sphinx-autodoc-typehints = [ + {file = "sphinx-autodoc-typehints-1.12.0.tar.gz", hash = "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52"}, + {file = "sphinx_autodoc_typehints-1.12.0-py3-none-any.whl", hash = "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] +sqlparse = [ + {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, + {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, +] +stevedore = [ + {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, + {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, +] +structlog = [ + {file = "structlog-21.1.0-py2.py3-none-any.whl", hash = "sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47"}, + {file = "structlog-21.1.0.tar.gz", hash = "sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57"}, +] +testfixtures = [ + {file = "testfixtures-6.17.1-py2.py3-none-any.whl", hash = "sha256:9ed31e83f59619e2fa17df053b241e16e0608f4580f7b5a9333a0c9bdcc99137"}, + {file = "testfixtures-6.17.1.tar.gz", hash = "sha256:5ec3a0dd6f71cc4c304fbc024a10cc293d3e0b852c868014b9f233203e149bda"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomlkit = [ + {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, + {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, +] +traitlets = [ + {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, + {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +wemake-python-styleguide = [ + {file = "wemake-python-styleguide-0.15.3.tar.gz", hash = "sha256:8b89aedabae67b7b915908ed06c178b702068137c0d8afe1fb59cdc829cd2143"}, + {file = "wemake_python_styleguide-0.15.3-py3-none-any.whl", hash = "sha256:a382f6c9ec87d56daa08a11e47cab019c99b384f1393b32564ebc74c6da80441"}, +] +win32-setctime = [ + {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, + {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, +] +yamllint = [ + {file = "yamllint-1.26.1.tar.gz", hash = "sha256:87d9462b3ed7e9dfa19caa177f7a77cd9888b3dc4044447d6ae0ab233bcd1324"}, +] +zipp = [ + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, +] diff --git a/new-github-repos/pyproject.toml b/new-github-repos/pyproject.toml new file mode 100644 index 0000000..e602fb0 --- /dev/null +++ b/new-github-repos/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + + +[tool.nitpick] + + +[tool.poetry] +name = "github-repos" +description = "github repos" +version = "0.1.0" +readme = "README.md" +authors = ["balsh"] + + +[tool.poetry.dependencies] +python = "3.8.9" +django = "^3" +django-split-settings = "^1.0" +django-axes = "^5.20" +django-csp = "^3.7" +django-health-check = "^3.16" +django-http-referrer-policy = "^1.1" +django-permissions-policy = "^4.1" +django-stubs-ext = "^0.2" + +psycopg2-binary = "<2.9" +gunicorn = "^20.0" +python-decouple = "^3.4" +bcrypt = "^3.2" +structlog = "^21.1" + + +[tool.poetry.dev-dependencies] +django-debug-toolbar = "^3.2" +django-querycount = "^0.7" +django-migration-linter = "^3.0" +django-extra-checks = "^0.11" +django-coverage-plugin = "^2.0" +nplusone = "^1.0" + +wemake-python-styleguide = "^0.15" +flake8-pytest-style = "^1.5" +flake8-django = "^1.1" +flake8-logging-format = "^0.6" +nitpick = "^0.26" + +pytest = "^6.2" +pytest-django = "^4.4" +pytest-cov = "^2.12" +pytest-randomly = "^3.8" +pytest-deadfixtures = "^2.2" +pytest-testmon = "^1.1" +pytest-timeout = "^1.4" +django-test-migrations = "^1.1" +hypothesis = "^6.14" + +mypy = "^0.910" +django-stubs = "^1.8" + +sphinx = "^4.0" +sphinx-autodoc-typehints = "^1.12" +tomlkit = "^0.7" +doc8 = "^0.8" + +yamllint = "^1.26" +safety = "^1.10" +dotenv-linter = "^0.2" +polint = "^0.4" +dennis = "^0.9" +dump-env = "^1.3" +ipython = "^7.25" diff --git a/new-github-repos/server/__init__.py b/new-github-repos/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/server/apps/__init__.py b/new-github-repos/server/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/server/apps/main/__init__.py b/new-github-repos/server/apps/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/server/apps/main/admin.py b/new-github-repos/server/apps/main/admin.py new file mode 100644 index 0000000..8f3ce90 --- /dev/null +++ b/new-github-repos/server/apps/main/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from server.apps.main.models import BlogPost + + +@admin.register(BlogPost) +class BlogPostAdmin(admin.ModelAdmin[BlogPost]): + """Admin panel example for ``BlogPost`` model.""" diff --git a/new-github-repos/server/apps/main/logic/__init__.py b/new-github-repos/server/apps/main/logic/__init__.py new file mode 100644 index 0000000..f6d5d66 --- /dev/null +++ b/new-github-repos/server/apps/main/logic/__init__.py @@ -0,0 +1,11 @@ +""" +This package is a place for your business logic. + +Please, do not create any other files inside your app package. +Place all files here, including: logic, forms, serializers. + +Decoupling is a good thing. We need more of that. + +Try using https://github.com/dry-python/ +It is awesome for writing business logic! +""" diff --git a/new-github-repos/server/apps/main/migrations/0001_initial.py b/new-github-repos/server/apps/main/migrations/0001_initial.py new file mode 100644 index 0000000..a3003ed --- /dev/null +++ b/new-github-repos/server/apps/main/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.7 on 2019-11-24 11:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Initial migration that creates the example BlogPost model.""" + + initial = True + dependencies = [] + + operations = [ + migrations.CreateModel( + name='BlogPost', + fields=[ + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('title', models.CharField(max_length=80)), + ('body', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'BlogPost', + 'verbose_name_plural': 'BlogPosts', + }, + ), + ] diff --git a/new-github-repos/server/apps/main/migrations/__init__.py b/new-github-repos/server/apps/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-github-repos/server/apps/main/models.py b/new-github-repos/server/apps/main/models.py new file mode 100644 index 0000000..d1b3246 --- /dev/null +++ b/new-github-repos/server/apps/main/models.py @@ -0,0 +1,33 @@ +import textwrap +from typing import Final, final + +from django.db import models + +#: That's how constants should be defined. +_POST_TITLE_MAX_LENGTH: Final = 80 + + +@final +class BlogPost(models.Model): + """ + This model is used just as an example. + + With it we show how one can: + - Use fixtures and factories + - Use migrations testing + + """ + + title = models.CharField(max_length=_POST_TITLE_MAX_LENGTH) + body = models.TextField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta(object): + verbose_name = 'BlogPost' # You can probably use `gettext` for this + verbose_name_plural = 'BlogPosts' + + def __str__(self) -> str: + """All django models should have this method.""" + return textwrap.wrap(self.title, _POST_TITLE_MAX_LENGTH // 4)[0] diff --git a/new-github-repos/server/apps/main/static/main/css/index.css b/new-github-repos/server/apps/main/static/main/css/index.css new file mode 100644 index 0000000..3c274c2 --- /dev/null +++ b/new-github-repos/server/apps/main/static/main/css/index.css @@ -0,0 +1,29 @@ +body { + text-align: center; +} + +img { + max-width: 100%; + height: auto; +} + +.wemake-services-body { + height: 95vh; + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.wemake-services-logo { + max-width: 250px; + margin: 0 auto; +} + +.github-corner img { + position: absolute; + top: 0; + right: 0; + border: 0; +} diff --git a/new-github-repos/server/apps/main/static/main/images/favicon-16x16.png b/new-github-repos/server/apps/main/static/main/images/favicon-16x16.png new file mode 100644 index 0000000..9ec7aa6 Binary files /dev/null and b/new-github-repos/server/apps/main/static/main/images/favicon-16x16.png differ diff --git a/new-github-repos/server/apps/main/static/main/images/favicon-32x32.png b/new-github-repos/server/apps/main/static/main/images/favicon-32x32.png new file mode 100644 index 0000000..3fe008d Binary files /dev/null and b/new-github-repos/server/apps/main/static/main/images/favicon-32x32.png differ diff --git a/new-github-repos/server/apps/main/templates/main/index.html b/new-github-repos/server/apps/main/templates/main/index.html new file mode 100644 index 0000000..a4cf980 --- /dev/null +++ b/new-github-repos/server/apps/main/templates/main/index.html @@ -0,0 +1,46 @@ +{% load static %} + + + + + + wemake-django-template + + + + + + + + + + + + + + + + Fork me on GitHub + + + + +
+ + +

wemake-django-template

+
+ + diff --git a/new-github-repos/server/apps/main/urls.py b/new-github-repos/server/apps/main/urls.py new file mode 100644 index 0000000..a5c8968 --- /dev/null +++ b/new-github-repos/server/apps/main/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from server.apps.main.views import index + +app_name = 'main' + +urlpatterns = [ + path('hello/', index, name='hello'), +] diff --git a/new-github-repos/server/apps/main/views.py b/new-github-repos/server/apps/main/views.py new file mode 100644 index 0000000..cb9bb4f --- /dev/null +++ b/new-github-repos/server/apps/main/views.py @@ -0,0 +1,12 @@ +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render + + +def index(request: HttpRequest) -> HttpResponse: + """ + Main (or index) view. + + Returns rendered default page to the user. + Typed with the help of ``django-stubs`` project. + """ + return render(request, 'main/index.html') diff --git a/new-github-repos/server/settings/__init__.py b/new-github-repos/server/settings/__init__.py new file mode 100644 index 0000000..b36b882 --- /dev/null +++ b/new-github-repos/server/settings/__init__.py @@ -0,0 +1,39 @@ +""" +This is a django-split-settings main file. + +For more information read this: +https://github.com/sobolevn/django-split-settings +https://sobolevn.me/2017/04/managing-djangos-settings + +To change settings file: +`DJANGO_ENV=production python manage.py runserver` +""" + +from os import environ + +import django_stubs_ext +from split_settings.tools import include, optional + +# Monkeypatching Django, so stubs will work for all generics, +# see: https://github.com/typeddjango/django-stubs +django_stubs_ext.monkeypatch() + +# Managing environment via `DJANGO_ENV` variable: +environ.setdefault('DJANGO_ENV', 'development') +_ENV = environ['DJANGO_ENV'] + +_base_settings = ( + 'components/common.py', + 'components/logging.py', + 'components/csp.py', + 'components/caches.py', + + # Select the right env: + 'environments/{0}.py'.format(_ENV), + + # Optionally override some settings: + optional('environments/local.py'), +) + +# Include settings: +include(*_base_settings) diff --git a/new-github-repos/server/settings/components/__init__.py b/new-github-repos/server/settings/components/__init__.py new file mode 100644 index 0000000..e511469 --- /dev/null +++ b/new-github-repos/server/settings/components/__init__.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from decouple import AutoConfig + +# Build paths inside the project like this: BASE_DIR.joinpath('some') +# `pathlib` is better than writing: dirname(dirname(dirname(__file__))) +BASE_DIR = Path.cwd().parent.parent.parent.parent + +# Loading `.env` files +# See docs: https://gitlab.com/mkleehammer/autoconfig +config = AutoConfig(search_path=BASE_DIR.joinpath('config')) diff --git a/new-github-repos/server/settings/components/caches.py b/new-github-repos/server/settings/components/caches.py new file mode 100644 index 0000000..3c36014 --- /dev/null +++ b/new-github-repos/server/settings/components/caches.py @@ -0,0 +1,16 @@ +# Caching +# https://docs.djangoproject.com/en/2.2/topics/cache/ + +CACHES = { + 'default': { + # TODO: use some other cache in production, + # like https://github.com/jazzband/django-redis + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, +} + + +# django-axes +# https://django-axes.readthedocs.io/en/latest/4_configuration.html#configuring-caches + +AXES_CACHE = 'default' diff --git a/new-github-repos/server/settings/components/common.py b/new-github-repos/server/settings/components/common.py new file mode 100644 index 0000000..9488c60 --- /dev/null +++ b/new-github-repos/server/settings/components/common.py @@ -0,0 +1,201 @@ +""" +Django settings for server project. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their config, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +from typing import Dict, List, Tuple, Union + +from django.utils.translation import ugettext_lazy as _ + +from server.settings.components import BASE_DIR, config + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +SECRET_KEY = config('DJANGO_SECRET_KEY') + +# Application definition: + +INSTALLED_APPS: Tuple[str, ...] = ( + # Your apps go here: + 'server.apps.main', + + # Default django apps: + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # django-admin: + 'django.contrib.admin', + 'django.contrib.admindocs', + + # Security: + 'axes', + + # Health checks: + # You may want to enable other checks as well, + # see: https://github.com/KristianOellegaard/django-health-check + 'health_check', + 'health_check.db', + 'health_check.cache', + 'health_check.storage', +) + +MIDDLEWARE: Tuple[str, ...] = ( + # Content Security Policy: + 'csp.middleware.CSPMiddleware', + + # Django: + 'django.middleware.security.SecurityMiddleware', + # django-permissions-policy + 'django_permissions_policy.PermissionsPolicyMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + + # Axes: + 'axes.middleware.AxesMiddleware', + + # Django HTTP Referrer Policy: + 'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware', +) + +ROOT_URLCONF = 'server.urls' + +WSGI_APPLICATION = 'server.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': config('POSTGRES_DB'), + 'USER': config('POSTGRES_USER'), + 'PASSWORD': config('POSTGRES_PASSWORD'), + 'HOST': config('DJANGO_DATABASE_HOST'), + 'PORT': config('DJANGO_DATABASE_PORT', cast=int), + 'CONN_MAX_AGE': config('CONN_MAX_AGE', cast=int, default=60), + 'OPTIONS': { + 'connect_timeout': 10, + }, + }, +} + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +USE_I18N = True +USE_L10N = True + +LANGUAGES = ( + ('en', _('English')), + ('ru', _('Russian')), +) + +LOCALE_PATHS = ( + 'locale/', +) + +USE_TZ = True +TIME_ZONE = 'UTC' + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + + +# Templates +# https://docs.djangoproject.com/en/2.2/ref/templates/api + +TEMPLATES = [{ + 'APP_DIRS': True, + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + # Contains plain text templates, like `robots.txt`: + BASE_DIR.joinpath('server', 'templates'), + ], + 'OPTIONS': { + 'context_processors': [ + # Default template context processors: + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.request', + ], + }, +}] + + +# Media files +# Media root dir is commonly changed in production +# (see development.py and production.py). +# https://docs.djangoproject.com/en/2.2/topics/files/ + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR.joinpath('media') + + +# Django authentication system +# https://docs.djangoproject.com/en/2.2/topics/auth/ + +AUTHENTICATION_BACKENDS = ( + 'axes.backends.AxesBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', +] + + +# Security +# https://docs.djangoproject.com/en/2.2/topics/security/ + +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_BROWSER_XSS_FILTER = True + +X_FRAME_OPTIONS = 'DENY' + +# https://github.com/DmytroLitvinov/django-http-referrer-policy +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy +REFERRER_POLICY = 'same-origin' + +# https://github.com/adamchainz/django-permissions-policy#setting +PERMISSIONS_POLICY: Dict[str, Union[str, List[str]]] = {} # noqa: WPS234 + + +# Timeouts +# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-EMAIL_TIMEOUT + +EMAIL_TIMEOUT = 5 diff --git a/new-github-repos/server/settings/components/csp.py b/new-github-repos/server/settings/components/csp.py new file mode 100644 index 0000000..cb07478 --- /dev/null +++ b/new-github-repos/server/settings/components/csp.py @@ -0,0 +1,15 @@ +""" +This file contains a definition for Content-Security-Policy headers. + +Read more about it: +https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Content-Security-Policy + +We are using `django-csp` to provide these headers. +Docs: https://github.com/mozilla/django-csp +""" + +CSP_SCRIPT_SRC = ("'self'",) +CSP_IMG_SRC = ("'self'",) +CSP_FONT_SRC = ("'self'",) +CSP_STYLE_SRC = ("'self'",) +CSP_DEFAULT_SRC = ("'none'",) diff --git a/new-github-repos/server/settings/components/logging.py b/new-github-repos/server/settings/components/logging.py new file mode 100644 index 0000000..e7c9b09 --- /dev/null +++ b/new-github-repos/server/settings/components/logging.py @@ -0,0 +1,77 @@ +# Logging +# https://docs.djangoproject.com/en/2.2/topics/logging/ + +# See also: +# 'Do not log' by Nikita Sobolev (@sobolevn) +# https://sobolevn.me/2020/03/do-not-log + +import structlog + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + + # We use these formatters in our `'handlers'` configuration. + # Probably, you won't need to modify these lines. + # Unless, you know what you are doing. + 'formatters': { + 'json_formatter': { + '()': structlog.stdlib.ProcessorFormatter, + 'processor': structlog.processors.JSONRenderer(), + }, + 'console': { + '()': structlog.stdlib.ProcessorFormatter, + 'processor': structlog.processors.KeyValueRenderer( + key_order=['timestamp', 'level', 'event', 'logger'], + ), + }, + }, + + # You can easily swap `key/value` (default) output and `json` ones. + # Use `'json_console'` if you need `json` logs. + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'console', + }, + 'json_console': { + 'class': 'logging.StreamHandler', + 'formatter': 'json_formatter', + }, + }, + + # These loggers are required by our app: + # - django is required when using `logger.getLogger('django')` + # - security is required by `axes` + 'loggers': { + 'django': { + 'handlers': ['console'], + 'propagate': True, + 'level': 'INFO', + }, + 'security': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': False, + }, + }, +} + +structlog.configure( + processors=[ + structlog.stdlib.filter_by_level, + structlog.processors.TimeStamper(fmt='iso'), + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.processors.ExceptionPrettyPrinter(), + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + context_class=structlog.threadlocal.wrap_dict(dict), + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, +) diff --git a/new-github-repos/server/settings/environments/__init__.py b/new-github-repos/server/settings/environments/__init__.py new file mode 100644 index 0000000..5de2bea --- /dev/null +++ b/new-github-repos/server/settings/environments/__init__.py @@ -0,0 +1,2 @@ + +"""Overriding settings based on the environment.""" diff --git a/new-github-repos/server/settings/environments/development.py b/new-github-repos/server/settings/environments/development.py new file mode 100644 index 0000000..7aad66a --- /dev/null +++ b/new-github-repos/server/settings/environments/development.py @@ -0,0 +1,150 @@ +""" +This file contains all the settings that defines the development server. + +SECURITY WARNING: don't run with debug turned on in production! +""" + +import logging +from typing import List + +from server.settings.components import config +from server.settings.components.common import ( + DATABASES, + INSTALLED_APPS, + MIDDLEWARE, +) + +# Setting the development status: + +DEBUG = True + +ALLOWED_HOSTS = [ + config('DOMAIN_NAME'), + 'localhost', + '0.0.0.0', # noqa: S104 + '127.0.0.1', + '[::1]', +] + + +# Installed apps for development only: + +INSTALLED_APPS += ( + # Better debug: + 'debug_toolbar', + 'nplusone.ext.django', + + # Linting migrations: + 'django_migration_linter', + + # django-test-migrations: + 'django_test_migrations.contrib.django_checks.AutoNames', + # This check might be useful in production as well, + # so it might be a good idea to move `django-test-migrations` + # to prod dependencies and use this check in the main `settings.py`. + # This will check that your database is configured properly, + # when you run `python manage.py check` before deploy. + 'django_test_migrations.contrib.django_checks.DatabaseConfiguration', + + # django-extra-checks: + 'extra_checks', +) + + +# Static files: +# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-STATICFILES_DIRS + +STATICFILES_DIRS: List[str] = [] + + +# Django debug toolbar: +# https://django-debug-toolbar.readthedocs.io + +MIDDLEWARE += ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', + + # https://github.com/bradmontgomery/django-querycount + # Prints how many queries were executed, useful for the APIs. + 'querycount.middleware.QueryCountMiddleware', +) + + +def _custom_show_toolbar(request): + """Only show the debug toolbar to users with the superuser flag.""" + return DEBUG and request.user.is_superuser + + +DEBUG_TOOLBAR_CONFIG = { + 'SHOW_TOOLBAR_CALLBACK': + 'server.settings.environments.development._custom_show_toolbar', +} + +# This will make debug toolbar to work with django-csp, +# since `ddt` loads some scripts from `ajax.googleapis.com`: +CSP_SCRIPT_SRC = ("'self'", 'ajax.googleapis.com') +CSP_IMG_SRC = ("'self'", 'data:') +CSP_CONNECT_SRC = ("'self'",) + + +# nplusone +# https://github.com/jmcarp/nplusone + +# Should be the first in line: +MIDDLEWARE = ( # noqa: WPS440 + 'nplusone.ext.django.NPlusOneMiddleware', +) + MIDDLEWARE + +# Logging N+1 requests: +NPLUSONE_RAISE = True # comment out if you want to allow N+1 requests +NPLUSONE_LOGGER = logging.getLogger('django') +NPLUSONE_LOG_LEVEL = logging.WARN +NPLUSONE_WHITELIST = [ + {'model': 'admin.*'}, +] + + +# django-test-migrations +# https://github.com/wemake-services/django-test-migrations + +# Set of badly named migrations to ignore: +DTM_IGNORED_MIGRATIONS = frozenset(( + ('axes', '*'), +)) + + +# django-extra-checks +# https://github.com/kalekseev/django-extra-checks + +EXTRA_CHECKS = { + 'checks': [ + # Forbid `unique_together`: + 'no-unique-together', + # Require non empty `upload_to` argument: + 'field-file-upload-to', + # Use the indexes option instead: + 'no-index-together', + # Each model must be registered in admin: + 'model-admin', + # FileField/ImageField must have non empty `upload_to` argument: + 'field-file-upload-to', + # Text fields shouldn't use `null=True`: + 'field-text-null', + # Prefer using BooleanField(null=True) instead of NullBooleanField: + 'field-boolean-null', + # Don't pass `null=False` to model fields (this is django default) + 'field-null', + # ForeignKey fields must specify db_index explicitly if used in + # other indexes: + {'id': 'field-foreign-key-db-index', 'when': 'indexes'}, + # If field nullable `(null=True)`, + # then default=None argument is redundant and should be removed: + 'field-default-null', + # Fields with choices must have companion CheckConstraint + # to enforce choices on database level + 'field-choices-constraint', + ], +} + +# Disable persistent DB connections +# https://docs.djangoproject.com/en/2.2/ref/databases/#caveats +DATABASES['default']['CONN_MAX_AGE'] = 0 diff --git a/new-github-repos/server/settings/environments/local.py.template b/new-github-repos/server/settings/environments/local.py.template new file mode 100644 index 0000000..13e9d81 --- /dev/null +++ b/new-github-repos/server/settings/environments/local.py.template @@ -0,0 +1 @@ +"""Override any custom settings here.""" diff --git a/new-github-repos/server/settings/environments/production.py b/new-github-repos/server/settings/environments/production.py new file mode 100644 index 0000000..1919e3c --- /dev/null +++ b/new-github-repos/server/settings/environments/production.py @@ -0,0 +1,75 @@ +""" +This file contains all the settings used in production. + +This file is required and if development.py is present these +values are overridden. +""" + +from server.settings.components import config + +# Production flags: +# https://docs.djangoproject.com/en/2.2/howto/deployment/ + +DEBUG = False + +ALLOWED_HOSTS = [ + # TODO: check production hosts + config('DOMAIN_NAME'), + + # We need this value for `healthcheck` to work: + 'localhost', +] + + +# Staticfiles +# https://docs.djangoproject.com/en/2.2/ref/contrib/staticfiles/ + +# This is a hack to allow a special flag to be used with `--dry-run` +# to test things locally. +_COLLECTSTATIC_DRYRUN = config( + 'DJANGO_COLLECTSTATIC_DRYRUN', cast=bool, default=False, +) +# Adding STATIC_ROOT to collect static files via 'collectstatic': +STATIC_ROOT = '.static' if _COLLECTSTATIC_DRYRUN else '/var/www/django/static' + +STATICFILES_STORAGE = ( + # This is a string, not a tuple, + # but it does not fit into 80 characters rule. + 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' +) + + +# Media files +# https://docs.djangoproject.com/en/2.2/topics/files/ + +MEDIA_ROOT = '/var/www/django/media' + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +_PASS = 'django.contrib.auth.password_validation' # noqa: S105 +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': '{0}.UserAttributeSimilarityValidator'.format(_PASS)}, + {'NAME': '{0}.MinimumLengthValidator'.format(_PASS)}, + {'NAME': '{0}.CommonPasswordValidator'.format(_PASS)}, + {'NAME': '{0}.NumericPasswordValidator'.format(_PASS)}, +] + + +# Security +# https://docs.djangoproject.com/en/2.2/topics/security/ + +SECURE_HSTS_SECONDS = 31536000 # the same as Caddy has +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True +SECURE_REDIRECT_EXEMPT = [ + # This is required for healthcheck to work: + '^health/', +] + +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True diff --git a/new-github-repos/server/templates/txt/humans.txt b/new-github-repos/server/templates/txt/humans.txt new file mode 100644 index 0000000..6153120 --- /dev/null +++ b/new-github-repos/server/templates/txt/humans.txt @@ -0,0 +1,14 @@ +# The humans responsible & technology colophon +# http://humanstxt.org/ + + +## balsh + +Team: + + +## Technologies + +Language: English +Doctype: HTML5 +Technologies: Python, Django diff --git a/new-github-repos/server/templates/txt/robots.txt b/new-github-repos/server/templates/txt/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/new-github-repos/server/templates/txt/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/new-github-repos/server/urls.py b/new-github-repos/server/urls.py new file mode 100644 index 0000000..8be36c2 --- /dev/null +++ b/new-github-repos/server/urls.py @@ -0,0 +1,60 @@ +""" +Main URL mapping configuration file. + +Include other URLConfs from external apps using method `include()`. + +It is also a good practice to keep a single URL to the root index page. + +This examples uses Django's default media +files serving technique in development. +""" + +from django.conf import settings +from django.contrib import admin +from django.contrib.admindocs import urls as admindocs_urls +from django.urls import include, path +from django.views.generic import TemplateView +from health_check import urls as health_urls + +from server.apps.main import urls as main_urls +from server.apps.main.views import index + +admin.autodiscover() + +urlpatterns = [ + # Apps: + path('main/', include(main_urls, namespace='main')), + + # Health checks: + path('health/', include(health_urls)), # noqa: DJ05 + + # django-admin: + path('admin/doc/', include(admindocs_urls)), # noqa: DJ05 + path('admin/', admin.site.urls), + + # Text and xml static files: + path('robots.txt', TemplateView.as_view( + template_name='txt/robots.txt', + content_type='text/plain', + )), + path('humans.txt', TemplateView.as_view( + template_name='txt/humans.txt', + content_type='text/plain', + )), + + # It is a good practice to have explicit index view: + path('', index, name='index'), +] + +if settings.DEBUG: # pragma: no cover + import debug_toolbar # noqa: WPS433 + from django.conf.urls.static import static # noqa: WPS433 + + urlpatterns = [ + # URLs specific only to django-debug-toolbar: + path('__debug__/', include(debug_toolbar.urls)), # noqa: DJ05 + ] + urlpatterns + static( # type: ignore + # Serving media files in development only: + settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT, + ) diff --git a/new-github-repos/server/wsgi.py b/new-github-repos/server/wsgi.py new file mode 100644 index 0000000..52a14ea --- /dev/null +++ b/new-github-repos/server/wsgi.py @@ -0,0 +1,15 @@ +""" +WSGI config for server 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/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') +application = get_wsgi_application() diff --git a/new-github-repos/setup.cfg b/new-github-repos/setup.cfg new file mode 100644 index 0000000..7966869 --- /dev/null +++ b/new-github-repos/setup.cfg @@ -0,0 +1,144 @@ +# All configuration for plugins and other utils is defined here. +# Read more about `setup.cfg`: +# https://docs.python.org/3/distutils/configfile.html + + +[flake8] +# flake8 configuration: +# https://flake8.pycqa.org/en/latest/user/configuration.html +format = wemake +show-source = True +statistics = False +doctests = True +enable-extensions = G + +# darglint configuration: +# https://github.com/terrencepreilly/darglint +strictness = long +docstring-style = numpy + +# Flake plugins: +max-line-length = 80 +max-complexity = 6 + +# Excluding some directories: +exclude = .git,__pycache__,.venv,.eggs,*.egg + +# Disable some pydocstyle checks: +ignore = D100, D104, D106, D401, X100, W504, RST303, RST304, DAR103, DAR203 + +# Docs: https://github.com/snoack/flake8-per-file-ignores +# You can completely or partially disable our custom checks, +# to do so you have to ignore `WPS` letters for all python files: +per-file-ignores = + # Allow `__init__.py` with logic for configuration: + server/settings/*.py: WPS226, WPS407, WPS412, WPS432 + # Allow to have magic numbers inside migrations and wrong module names: + server/*/migrations/*.py: WPS102, WPS114, WPS432 + # Enable `assert` keyword and magic numbers for tests: + tests/*.py: S101, WPS432 + + +[isort] +# isort configuration: +# https://github.com/timothycrosley/isort/wiki/isort-Settings +include_trailing_comma = true +use_parentheses = true +# See https://github.com/timothycrosley/isort#multi-line-output-modes +multi_line_output = 3 +line_length = 80 + + +[tool:pytest] +# pytest configuration: +# https://docs.pytest.org/en/stable/customize.html + +# pytest-django configuration: +# https://pytest-django.readthedocs.io/en/latest/ +DJANGO_SETTINGS_MODULE = server.settings + +# Timeout for tests, so they can not take longer +# than this amount of seconds. +# You should adjust this value to be as low as possible. +# Configuration: +# https://pypi.org/project/pytest-timeout/ +timeout = 5 + +# Directories that are not visited by pytest collector: +norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ + +# You will need to measure your tests speed with `-n auto` and without it, +# so you can see whether it gives you any performance gain, or just gives +# you an overhead. See `docs/template/development-process.rst`. +addopts = + --strict-markers + --strict-config + --doctest-modules + --fail-on-template-vars + --dup-fixtures + # Output: + --tb=short + # Parallelism: + # -n auto + # --boxed + # Coverage: + --cov=server + --cov=tests + --cov-branch + --cov-report=term-missing:skip-covered + --cov-report=html + --cov-fail-under=100 + + +[coverage:run] +# Coverage configuration: +# https://coverage.readthedocs.io/en/latest/config.html +plugins = + # Docs: https://github.com/nedbat/django_coverage_plugin + django_coverage_plugin + + +[mypy] +# Mypy configuration: +# https://mypy.readthedocs.io/en/latest/config_file.html +allow_redefinition = False +check_untyped_defs = True +disallow_untyped_decorators = True +disallow_any_explicit = True +disallow_any_generics = True +disallow_untyped_calls = True +ignore_errors = False +ignore_missing_imports = True +implicit_reexport = False +local_partial_types = True +strict_optional = True +strict_equality = True +no_implicit_optional = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unreachable = True +warn_no_return = True + +plugins = + mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = server.settings + +[mypy-server.apps.*.migrations.*] +# Django migrations should not produce any errors: +ignore_errors = True + +[mypy-server.apps.*.models] +# FIXME: remove this line, when `django-stubs` will stop +# using `Any` inside. +disallow_any_explicit = False + + +[doc8] +# doc8 configuration: +# https://github.com/pycqa/doc8 +ignore-path = docs/_build +max-line-length = 80 +sphinx = True diff --git a/new-github-repos/tests/conftest.py b/new-github-repos/tests/conftest.py new file mode 100644 index 0000000..1e673a3 --- /dev/null +++ b/new-github-repos/tests/conftest.py @@ -0,0 +1,45 @@ +""" +This module is used to provide configuration, fixtures, and plugins for pytest. + +It may be also used for extending doctest's context: +1. https://docs.python.org/3/library/doctest.html +2. https://docs.pytest.org/en/latest/doctest.html +""" + +import pytest + + +@pytest.fixture(autouse=True) +def _media_root(settings, tmpdir_factory) -> None: + """Forces django to save media files into temp folder.""" + settings.MEDIA_ROOT = tmpdir_factory.mktemp('media', numbered=True) + + +@pytest.fixture(autouse=True) +def _password_hashers(settings) -> None: + """Forces django to use fast password hashers for tests.""" + settings.PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.MD5PasswordHasher', + ] + + +@pytest.fixture(autouse=True) +def _auth_backends(settings) -> None: + """Deactivates security backend from Axes app.""" + settings.AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + ) + + +@pytest.fixture(autouse=True) +def _debug(settings) -> None: + """Sets proper DEBUG and TEMPLATE debug mode for coverage.""" + settings.DEBUG = False + for template in settings.TEMPLATES: + template['OPTIONS']['debug'] = True + + +@pytest.fixture() +def main_heading() -> str: + """An example fixture containing some html fragment.""" + return '

wemake-django-template

' diff --git a/new-github-repos/tests/test_apps/test_main/test_models/test_blog_post_model.py b/new-github-repos/tests/test_apps/test_main/test_models/test_blog_post_model.py new file mode 100644 index 0000000..ba4a494 --- /dev/null +++ b/new-github-repos/tests/test_apps/test_main/test_models/test_blog_post_model.py @@ -0,0 +1,16 @@ +from hypothesis import given +from hypothesis.extra import django + +from server.apps.main.models import BlogPost + + +class TestBlogPost(django.TestCase): + """This is a property-based test that ensures model correctness.""" + + @given(django.from_model(BlogPost)) + def test_model_properties(self, instance: BlogPost) -> None: + """Tests that instance can be saved and has correct representation.""" + instance.save() + + assert instance.id > 0 + assert len(str(instance)) <= 20 diff --git a/new-github-repos/tests/test_apps/test_main/test_models/test_migrations/test_blog_post_migrations.py b/new-github-repos/tests/test_apps/test_main/test_models/test_migrations/test_blog_post_migrations.py new file mode 100644 index 0000000..d153dc2 --- /dev/null +++ b/new-github-repos/tests/test_apps/test_main/test_models/test_migrations/test_blog_post_migrations.py @@ -0,0 +1,17 @@ +import pytest +from django_test_migrations.migrator import Migrator + +from server.apps.main.urls import app_name + + +def test_initial0001(migrator: Migrator) -> None: + """Tests the initial migration forward application.""" + old_state = migrator.apply_initial_migration((app_name, None)) + with pytest.raises(LookupError): + # This model does not exist before this migration: + old_state.apps.get_model(app_name, 'BlogPost') + + new_state = migrator.apply_tested_migration((app_name, '0001_initial')) + model = new_state.apps.get_model(app_name, 'BlogPost') + + assert model.objects.create(title='test', body='some body') diff --git a/new-github-repos/tests/test_apps/test_main/test_views/test_index_view.py b/new-github-repos/tests/test_apps/test_main/test_views/test_index_view.py new file mode 100644 index 0000000..d2603ce --- /dev/null +++ b/new-github-repos/tests/test_apps/test_main/test_views/test_index_view.py @@ -0,0 +1,18 @@ +from django.test import Client +from django.urls import reverse + + +def test_main_page(client: Client, main_heading: str) -> None: + """This test ensures that main page works.""" + response = client.get('/') + + assert response.status_code == 200 + assert main_heading in str(response.content) + + +def test_hello_page(client: Client, main_heading: str) -> None: + """This test ensures that hello page works.""" + response = client.get(reverse('main:hello')) + + assert response.status_code == 200 + assert main_heading in str(response.content) diff --git a/new-github-repos/tests/test_server/test_urls.py b/new-github-repos/tests/test_server/test_urls.py new file mode 100644 index 0000000..7e2bef7 --- /dev/null +++ b/new-github-repos/tests/test_server/test_urls.py @@ -0,0 +1,55 @@ +import pytest +from django.test import Client + + +@pytest.mark.django_db() +def test_health_check(client: Client) -> None: + """This test ensures that health check is accessible.""" + response = client.get('/health/') + + assert response.status_code == 200 + + +def test_admin_unauthorized(client: Client) -> None: + """This test ensures that admin panel requires auth.""" + response = client.get('/admin/') + + assert response.status_code == 302 + + +def test_admin_authorized(admin_client: Client) -> None: + """This test ensures that admin panel is accessible.""" + response = admin_client.get('/admin/') + + assert response.status_code == 200 + + +def test_admin_docs_unauthorized(client: Client) -> None: + """This test ensures that admin panel docs requires auth.""" + response = client.get('/admin/doc/') + + assert response.status_code == 302 + + +def test_admin_docs_authorized(admin_client: Client) -> None: + """This test ensures that admin panel docs are accessible.""" + response = admin_client.get('/admin/doc/') + + assert response.status_code == 200 + assert b'docutils' not in response.content + + +def test_robots_txt(client: Client) -> None: + """This test ensures that `robots.txt` is accessible.""" + response = client.get('/robots.txt') + + assert response.status_code == 200 + assert response.get('Content-Type') == 'text/plain' + + +def test_humans_txt(client: Client) -> None: + """This test ensures that `humans.txt` is accessible.""" + response = client.get('/humans.txt') + + assert response.status_code == 200 + assert response.get('Content-Type') == 'text/plain'