add async search to celery task and logger

This commit is contained in:
Dmitry Afanasyev 2021-10-07 02:06:51 +03:00
parent d3d862eaef
commit 9f8c186128
94 changed files with 526 additions and 7035 deletions

View File

@ -1 +0,0 @@
!.env

View File

@ -1,34 +0,0 @@
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/

View File

@ -1,6 +0,0 @@
# celery first example
Steps:
1. Run `docker-compose up`
2. Show logs
3. In a new terminal run `docker-compose exec worker python`

View File

@ -1,3 +0,0 @@
# from app_celery import app as my_celery_app
#
# __all__ = ('my_celery_app', )

View File

@ -1,25 +0,0 @@
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://',
)

View File

@ -1,6 +0,0 @@
# RabbitMQ settings:
RABBITMQ_DEFAULT_USER=rabbit_admin
RABBITMQ_DEFAULT_PASS=mypass
RABBITMQ_PORT=5672
RABBITMQ_HOST=rabbitmq_host

View File

@ -1,28 +0,0 @@
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

View File

@ -1,14 +0,0 @@
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

View File

@ -1,2 +0,0 @@
celery==5.0.2
python-decouple==3.3

View File

@ -4,7 +4,7 @@ DOMAIN_NAME=localhost
TLS_EMAIL=webmaster@localhost
GITHUB_USERNAME=
GITHUB_PASSWORD=
GITHUB_TOKEN=
# === Django ===

View File

@ -13,7 +13,7 @@ services:
- webnet
env_file: ./config/.env
ports:
- 5433:5432
- "5433:5432"
rabbitmq:
hostname: rabbitmq_host
@ -24,8 +24,8 @@ services:
networks:
- webnet
ports:
- 8080:15672
- 5672:5672
- "8080:15672"
- "5672:5672"
web:
image: "github-repos"
@ -45,7 +45,7 @@ services:
volumes:
- .:/code
ports:
- 8000:8000
- "8000:8000"
depends_on:
- db
networks:
@ -93,7 +93,7 @@ services:
- web
restart: unless-stopped
ports:
- 5555:5555
- "5555:5555"
networks:
- webnet

View File

@ -1,4 +1,4 @@
FROM python:3.8.9-slim-buster
FROM python:3.9.7-slim-buster
ENV BUILD_ONLY_PACKAGES='wget' \
# python:
@ -11,7 +11,7 @@ ENV BUILD_ONLY_PACKAGES='wget' \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
# poetry:
POETRY_VERSION=1.1.4 \
POETRY_VERSION=1.1.11 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \

626
github-stars/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ authors = ["balsh"]
[tool.poetry.dependencies]
python = "3.8.9"
python = "3.9.7"
django = "^3"
django-split-settings = "^1.0"
django-axes = "^5.20"
@ -34,7 +34,7 @@ structlog = "^21.1"
celery = "5.1.2"
flower = "^1.0.0"
celery_progress = "0.1.1"
aiohttp = "3.7.4.post0"
[tool.poetry.dev-dependencies]
django-debug-toolbar = "^3.2"

View File

@ -1,93 +1,120 @@
import requests
from requests.models import Response
from requests.auth import HTTPBasicAuth
import re
import time
from functools import lru_cache
from typing import Dict, Optional
import sys
from typing import Dict, Optional, Tuple
from server.apps.main.celery_config import celery_app
from server.settings.components.common import GIT_API_URL
from celery_progress.backend import ProgressRecorder
from celery import shared_task
from server.settings.components import config
import aiohttp
import asyncio
import logging
console_logger = logging.getLogger(__name__)
formatter = logging.Formatter(datefmt="%Y.%m.%d %H:%M:%S",
fmt='%(asctime)s | message: %(message)s')
# fmt='%(asctime)s | %(levelname)s | process: %(process)d | module name: %(name)s | '
# 'func name: %(funcName)s | line number: %(lineno)s | message: %(message)s',)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
console_logger.setLevel(logging.INFO)
console_logger.addHandler(handler)
def current_page(response: Response, link: str) -> int:
url = str(response.links[f'{link}']['url'])
page_count = int(str(re.findall(pattern=r'page=\d+', string=url)[1])
.replace('page=', ''))
return page_count
class GitHubScanner:
def __init__(self, user: str, token: str):
self.auth = aiohttp.BasicAuth(user, token)
self.data = {}
self.semaphore = asyncio.Semaphore(200)
def github_request(url: str) -> Response:
auth = HTTPBasicAuth(config('GITHUB_USERNAME'), config('GITHUB_PASSWORD'))
counter = 0
while True:
def _data_count(self) -> int:
repos_count = 0
try:
counter += 1
if auth == HTTPBasicAuth('', ''):
response = requests.get(url)
else:
response = requests.get(url, auth=auth)
return response
except ConnectionError as connection_error:
if counter < 5:
time.sleep(10)
else:
raise connection_error
for data_set in self.data.values():
repos_count += len(data_set['data'])
except ValueError:
console_logger.info(f'Data is empty')
return repos_count
@staticmethod
def _page_count(url: str) -> int:
page = int(str(re.findall(pattern=r'&page=\d+', string=url)[-1]).replace('&page=', ''))
return page
async def _github_request(self, session: aiohttp.ClientSession, url: str) -> Dict:
async with self.semaphore:
counter = 0
while True:
try:
counter += 1
resp = await session.get(url)
async with resp:
if resp.status == 200:
self.data[self._page_count(url)] = {'response': resp, 'data': await resp.json()}
return self.data[self._page_count(url)]
if resp.status >= 400:
return {'response': None, 'data': None}
except Exception as connection_error:
if counter < 5:
await asyncio.sleep(10)
else:
raise connection_error
async def get_data(self, celery_task, username: str) -> None:
base_url = f'{GIT_API_URL}/{username}/repos?per_page=100&page=' + '{}'
progress_recorder = ProgressRecorder(celery_task)
connector = aiohttp.TCPConnector(limit=500)
async with aiohttp.ClientSession(auth=self.auth, connector=connector) as session:
url = base_url.format(1)
tasks = []
try:
resp = await self._github_request(session, url)
self.data[1] = resp
last_page = self._page_count(dict(resp['response'].headers).get('Link'))
last_page_url = str(resp['response'].links['last']['url'])
if last_page:
data_last_page = await self._github_request(session, last_page_url)
repos_count = (last_page - 1) * 100 + len(data_last_page['data'])
for i in range(1, last_page):
url = base_url.format(i + 1)
current_repos_count = self._data_count()
percent = round(current_repos_count / repos_count * 100)
progress_recorder.set_progress(current_repos_count, repos_count,
description=f'Processing: {percent}%')
task = asyncio.create_task(self._github_request(session, url))
tasks.append(task)
else:
tasks.append(asyncio.create_task(self._github_request(session, url)))
except Exception as e:
console_logger.error(e)
await asyncio.gather(*tasks)
@shared_task(bind=True)
def get_github_stars(self, username: str) -> Dict[str, Optional[int]]:
url = f'{GIT_API_URL}/{username}/repos?per_page=100&page=1'
print(url)
progress_recorder = ProgressRecorder(self)
response = github_request(url)
if response.status_code >= 400:
@celery_app.task(bind=True)
def get_github_stars(celery_task, username: str) -> Dict[str, Optional[int]]:
github = GitHubScanner(config('GITHUB_USERNAME'), config('GITHUB_TOKEN'))
loop = asyncio.get_event_loop()
loop.run_until_complete(github.get_data(celery_task, username))
repos_data = github.data
data = {}
try:
for value in repos_data.values():
for item in value['data']:
data[item["name"]] = item["stargazers_count"]
result = dict(sorted(data.items(), key=lambda x: x[1], reverse=True))
except TypeError:
result = {}
else:
repos = response.json()
try:
page_count = current_page(response, 'last')
repos_count = (page_count - 1) * 100 + \
len(github_request(response.links['last']['url']).json())
except KeyError as e:
page_count = 1
repos_count = len(repos)
i = 0
while 'next' in response.links.keys():
i += 1
response = github_request(response.links['next']['url'])
repos.extend(response.json())
current = i * 100 + len(response.json())
# Progress bar
percent = round(100 / page_count * i)
progress_recorder.set_progress(current, repos_count,
description=f'Processing: {percent}%')
# Fetching repos and stars in dict
data: Dict[str, int] = {}
try:
for item in repos:
data[item['name']] = int(item['stargazers_count'])
result = dict(sorted(data.items(), key=lambda x: x[1], reverse=True))
except TypeError:
result = {}
return result
# Demo task
@shared_task(bind=True)
def process_download(self):
def process_download(task) -> str:
print('Task started')
# Create the progress recorder instance
# which we'll use to update the web page
progress_recorder = ProgressRecorder(self)
progress_recorder = ProgressRecorder(task)
print('Start')
for i in range(5):

View File

@ -11,7 +11,8 @@
</head>
<body>
{% block demo %}{% endblock %}
{% block demo %}
{% endblock %}
<!-- JQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
@ -19,7 +20,8 @@
<!-- Celery Progress -->
<script src="{% static 'celery_progress/celery_progress.js' %}"></script>
{% block progress_bar_js %}{% endblock progress_bar_js %}
{% block progress_bar_js %}
{% endblock progress_bar_js %}
<div class="container text-center" style="padding-top: 20px;">
{{ message }}
{% for repo, stars in data.items %}

View File

@ -34,7 +34,8 @@
<!-- Download Status -->
<div class="container" style="padding-top: 20px;">
<div class="card" style="height: 120px;">
{% block progress %}{% endblock progress %}
{% block progress %}
{% endblock progress %}
</div>
</div>
{% endblock %}

View File

@ -1,13 +1,14 @@
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from .forms import GithubForm
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from .commands import get_github_stars, process_download
from .commands import get_github_stars, console_logger, process_download
from django.views.decorators.http import require_http_methods
from celery.result import AsyncResult
from functools import lru_cache
task_id = {}
@ -29,10 +30,9 @@ def github(request: HttpRequest) -> HttpResponse:
try:
email = getattr((User.objects.get(username=username)),
'email', 'default@email.ru')
except ObjectDoesNotExist as e:
error = 'That user doesnt exists or not log on'
print(error, e)
console_logger.error(error, e)
if request.method == 'POST':
@ -40,7 +40,7 @@ def github(request: HttpRequest) -> HttpResponse:
result = get_github_stars.delay(github_username)
task_id[username] = result.task_id
return redirect(reverse('github_result'))
return redirect(reverse_lazy('github_result'))
form = GithubForm
return render(request, 'main/github.html',
@ -53,18 +53,21 @@ def github_result(request: HttpRequest) -> HttpResponse:
username = str(request.user.username)
data = AsyncResult(task_id[username])
result = {}
message = ''
if data.ready():
message = "Result Ready"
result = data.get()
print('result ready')
console_logger.info('result ready')
else:
print('result not ready')
console_logger.info('result not ready')
return render(request, 'main/github_result.html',
context={'data': result,
'message': message})
@lru_cache(maxsize=10)
def demo_view(request: HttpRequest) -> HttpResponse:
username = str(request.user.username)
form = GithubForm
@ -79,11 +82,11 @@ def demo_view(request: HttpRequest) -> HttpResponse:
message = f'Total repos: {len(result)}\n'
if len(result) == 0:
result = {'Error': 'User has no repositories!'}
print('Result ready! Please refresh page')
console_logger.info('Result ready! Please refresh page')
else:
print('result not ready')
console_logger.info('result not ready')
except KeyError as e:
print(e)
console_logger.error(e)
finally:
# Return demo view
return render(request, 'progress.html',
@ -99,7 +102,7 @@ def demo_view(request: HttpRequest) -> HttpResponse:
# Get ID
task_id[username] = result.task_id
# Print Task ID
print(f'Celery Task ID: {task_id[username]}')
console_logger.info(f'Celery Task ID: {task_id[username]}')
# Return demo view with Task ID
return render(request, 'progress.html',
context={'task_id': task_id[username],

View File

@ -1,67 +0,0 @@
# 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/

View File

@ -1,25 +0,0 @@
# 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

View File

@ -1,247 +0,0 @@
#### 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/

View File

@ -1,96 +0,0 @@
---
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'

View File

@ -1 +0,0 @@
3.8.9

View File

@ -1,32 +0,0 @@
# 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).

View File

@ -1,43 +0,0 @@
# === 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

View File

@ -1,70 +0,0 @@
#!/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:

View File

@ -1,40 +0,0 @@
# 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
}
}

View File

@ -1,96 +0,0 @@
#!/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

View File

@ -1,46 +0,0 @@
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

View File

@ -1,24 +0,0 @@
#!/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

View File

@ -1,41 +0,0 @@
#!/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'

View File

@ -1,65 +0,0 @@
---
# 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:

View File

@ -1,20 +0,0 @@
# 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)

View File

@ -1,18 +0,0 @@
# 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.

View File

@ -1,28 +0,0 @@
<h3>
Links
</h3>
<ul>
<li>
<a href="https://github.com/wemake-services/wemake-django-template">
GitHub
</a>
</li>
<li>
<a href="http://wemake.services/meta">
Meta
</a>
</li>
<li>
<a href="http://wemake.services/">
wemake.services
</a>
</li>
</ul>
<ul>
<li>
<a aria-label="Star wemake-services/wemake-django-template on GitHub" data-count-aria-label="# stargazers on GitHub" data-count-api="/repos/wemake-services/wemake-django-template#stargazers_count" data-count-href="wemake-services/wemake-django-template/stargazers" data-style="mega" data-icon="octicon-star" href="https://github.com/wemake-services/wemake-django-template" class="github-button">Star</a>
</li>
</ul>
<script async defer src="https://buttons.github.io/buttons.js"></script>

View File

@ -1,136 +0,0 @@
# 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',
],
}

View File

@ -1,102 +0,0 @@
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 <https://github.com/wemake-services/meta>`_,
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 <https://github.com/audreyr/cookiecutter#python-django>`_.
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`

View File

@ -1,36 +0,0 @@
@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

View File

@ -1,178 +0,0 @@
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 <https://poetry.eustace.io/>`_
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

View File

@ -1,167 +0,0 @@
.. _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 <https://docs.gitlab.com/ce/ci/variables/README.html#secret-variables>`_.
Then we use `dump-env <https://github.com/sobolevn/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 <https://docs.ansible.com/ansible/2.4/vault.html>`_
- `git-secret <https://github.com/sobolevn/git-secret>`_
- `Vault <https://www.vaultproject.io/>`_
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 <https://medium.com/wemake-services/managing-djangos-settings-e2b7f496120d>`_
- `docker env-file docs <https://docs.docker.com/compose/env-file/>`_
Django admin
~~~~~~~~~~~~
- `Django Admin Cookbook <https://books.agiliq.com/projects/django-admin-cookbook/en/latest/>`_

View File

@ -1,98 +0,0 @@
Documentation
=============
`We <https://github.com/wemake-services/meta>`_ 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 <https://github.com/wemake-services/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 <https://github.com/wemake-services/wemake-django-template/issues>`_
- ``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 <https://pypi.python.org/pypi/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 <https://github.com/mgaitan/sphinxcontrib-mermaid>`_ - sphinx plugin to create general flowcharts, sequence and gantt diagrams
- `sphinxcontrib-plantuml <https://github.com/sphinx-contrib/plantuml/>`_ - sphinx plugin to create UML diagrams
- `nbsphinx <https://github.com/spatialaudio/nbsphinx>`_ - sphinx plugin to embed ``ipython`` notebooks into your docs
Further reading
---------------
- `sphinx <http://www.sphinx-doc.org/en/stable/>`_
- `sphinx with django <https://docs.djangoproject.com/en/2.2/internals/contributing/writing-documentation/#getting-started-with-sphinx>`_
- `sphinx-autodoc-typehints <https://github.com/agronholm/sphinx-autodoc-typehints>`_
- `Architecture Decision Record (ADR) <https://github.com/joelparkerhenderson/architecture_decision_record>`_
- `adr-tools <https://github.com/npryce/adr-tools>`_

View File

@ -1,29 +0,0 @@
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.

View File

@ -1,56 +0,0 @@
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 <https://github.com/dependabot/dependabot-script>`_
to enable automatic dependencies updates via Pull Requests to your repository.
Similar to the original template repository: `list of pull requests <https://github.com/wemake-services/wemake-django-template/pulls?q=is%3Apr+author%3Aapp%2Fdependabot>`_.
It is available to both Github and Gitlab.
But, for Gitlab version you currently have to update your `.gitlab-ci.yml <https://github.com/dependabot/dependabot-script/blob/master/.gitlab-ci.example.yml>`_.
Secret variables
----------------
If some real secret variables are required, then you can use `gitlab secrets <https://docs.gitlab.com/ee/ci/variables/#secret-variables>`_.
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 <https://docs.gitlab.com/ee/user/project/pages/>`_.
Further reading
---------------
- `Container Registry <https://gitlab.com/help/user/project/container_registry>`_
- `Gitlab CI/CD <https://about.gitlab.com/features/gitlab-ci-cd/>`_

View File

@ -1,130 +0,0 @@
.. _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 <https://wemake-python-styleguide.readthedocs.io/en/latest/>`_
docs.
Things that are included in the linting process:
- `flake8 <http://flake8.pycqa.org/>`_ is used a general tool for linting
- `isort <https://github.com/timothycrosley/isort>`_ is used to validate ``import`` order
- `bandit <https://github.com/PyCQA/bandit>`_ for static security checks
- `eradicate <https://github.com/myint/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 <https://github.com/vikingco/flake8-pytest>`_ - ensures that ``pytest`` best practices are used
- `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ - ensures that ``pytest`` tests and fixtures are written in a single style
- `flake8-django <https://github.com/rocioar/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 <https://github.com/3YOURMIND/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 <https://github.com/adrienverge/yamllint>`_ docs.
.. code:: bash
yamllint -d '{"extends": "default", "ignore": ".venv"}' -s .
dotenv-linter
-------------
Is used to lint your ``.env`` files.
See `dotenv-linter <https://github.com/wemake-services/dotenv-linter>`_ docs.
.. code:: bash
dotenv-linter config/.env config/.env.template
polint and dennis
-----------------
Are used to lint your ``.po`` files.
See `polint <https://github.com/ziima/polint>`_ docs.
Also see `dennis <https://dennis.readthedocs.io/en/latest/linting.html>`_ 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 <https://wemake-python-stylegui.de/en/latest/pages/usage/integrations/extras.html>`_.
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 <https://www.shellcheck.net/>`_ docs.
hadolint
~~~~~~~~
This linter is used to lint your ``Dockerfile`` syntax.
See `hadolint <https://github.com/hadolint/hadolint>`_

View File

@ -1,135 +0,0 @@
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 <https://docs.djangoproject.com/en/2.2/topics/http/urls/>`_
- ``server/wsgi.py`` - ``django`` `wsgi definition <https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface>`_
- ``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 <https://medium.com/wemake-services/managing-djangos-settings-e2b7f496120d>`_
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 <https://github.com/jwilder/dockerize>`_.
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 <https://github.com/krallin/tini>`_ to know more.

View File

@ -1,186 +0,0 @@
.. _`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 <http://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html>`_
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 <https://myopsblog.wordpress.com/2017/02/06/why-databases-is-not-for-containers/>`_.
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 <https://docs.docker.com/compose/compose-file/#extra_hosts>`_
to contain your ``postgresql`` address.
Lastly, you would need to add new hosts to ``pg_hba.conf``.
`Here <http://winstonkotzan.com/blog/2017/06/01/connecting-to-external-postgres-database-with-docker.html>`_
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 <https://caddyserver.com/docs/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://caddyserver.com/docs/tls>`_
``https`` inside ``Caddy`` and in production settings for Django.
Because Django itself also redirects to `https`.
See `docs <https://docs.djangoproject.com/en/2.2/ref/settings/#secure-ssl-redirect>`_.
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 <https://docs.djangoproject.com/en/dev/howto/deployment/checklist/#deployment-checklist>`_

View File

@ -1,77 +0,0 @@
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 <https://docs.gitlab.com/ee/user/project/container_registry.html>`_.
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 <https://docs.docker.com/engine/reference/commandline/service_update/>`_.
Updating existing `stack <https://docs.docker.com/engine/reference/commandline/stack_deploy/>`_.
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 <https://docs.docker.com/compose/production>`_
- `Full tutorial <https://docs.docker.com/get-started>`_

View File

@ -1,114 +0,0 @@
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 <https://docs.djangoproject.com/en/2.2/topics/security/>`_
that are all turned on by default in this template.
We also :ref:`enforce <going-to-production>` 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 <https://github.com/jazzband/django-axes>`_ to track and ban repeating access requests
- `django-csp <https://github.com/mozilla/django-csp>`_ to enforce `Content-Security Policy <https://www.w3.org/TR/CSP/>`_ for our webpages
- `django-http-referrer-policy <https://django-referrer-policy.readthedocs.io>`_ to enforce `Referrer Policy <https://www.w3.org/TR/referrer-policy/>`_ for our webpages
And there are also some awesome extensions that are not included:
- `django-honeypot <https://github.com/jamesturk/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 <https://poetry.eustace.io/>`_ 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 <https://github.com/pyupio/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 <https://help.github.com/articles/about-security-alerts-for-vulnerable-dependencies/>`_
for our main template repository.
Static analysis
---------------
We use ``wemake-python-styleguide`` which
includes `bandit <https://pypi.org/project/bandit/>`_ security checks inside.
You can also install `pyt <https://pyt.readthedocs.io>`_
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 <https://github.com/eliasgranderubio/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 <https://github.com/dxa4481/truffleHog>`_ or `detect-secrets <https://github.com/Yelp/detect-secrets>`_ inside your workflow.
You can also turn on `Gitlab secrets checker <https://docs.gitlab.com/ee/push_rules/push_rules.html#prevent-pushing-secrets-to-the-repository>`_ 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 <https://github.com/trailofbits/twa>`_ - tiny web auditor that has a lot of security checks for the webpages
- `XSStrike <https://github.com/s0md3v/XSStrike>`_ - automated tool to check that your application is not vulnerable to ``xss`` errors
- `docker-bench <https://github.com/docker/docker-bench-security>`_ - a script that checks for dozens of common best-practices around deploying Docker containers in production
- `lynis <https://cisofy.com/lynis/>`_ - a battle-tested security tool for systems running Linux, macOS, or Unix-based operating system
- `trivy <https://github.com/knqyf263/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 <https://www.owasp.org/images/3/33/OWASP_Application_Security_Verification_Standard_3.0.1.pdf>`_
- `Docker security <https://docs.docker.com/engine/security/security/>`_
- `AppArmor <https://docs.docker.com/engine/security/apparmor/>`_ and `bane <https://github.com/genuinetools/bane>`_

View File

@ -1,111 +0,0 @@
Testing
=======
We try to keep our quality standards high.
So, we use different tools to make this possible.
We use `mypy <http://mypy-lang.org/>`_ for optional
static typing.
We run tests with `pytest <https://pytest.org/>`_ 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 <https://pytest-django.readthedocs.io/en/latest/database.html#example-work-flow-with-reuse-db-and-create-db>`_,
so ``django`` won't recreate database on each test
- If there are a lot of migrations to perform you may also add
`--nomigrations option <https://pytest-django.readthedocs.io/en/latest/database.html#nomigrations-disable-django-1-7-migrations>`_,
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 <https://mypy.readthedocs.io/en/latest/>`_
for more information.
We also use `django-stubs <https://github.com/typeddjango/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.

View File

@ -1,47 +0,0 @@
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 `<https://github.com/python-pillow/Pillow/issues/1763>`_
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 `<https://github.com/wemake-services/wemake-django-template/issues/345>`_.
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 <https://docs.docker.com/docker-for-mac/osxfs-caching/>`_
and a good `article <https://medium.com/@TomKeur/how-get-better-disk-performance-in-docker-for-mac-2ba1244b5b70>`_.

View File

@ -1,51 +0,0 @@
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 <https://github.com/wemake-services/wemake-django-template/compare/91188fc4b89bd4989a0ead3d156a4619644965b0...master>`_.
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 <https://github.com/wemake-services/wemake-django-template/releases>`_.
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 <https://python-poetry.org/docs/dependency-specification/#caret-requirements>`_
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.

View File

@ -1,6 +0,0 @@
# 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

View File

@ -1,31 +0,0 @@
#!/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()

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
[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"

View File

@ -1,8 +0,0 @@
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."""

View File

@ -1,11 +0,0 @@
"""
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!
"""

View File

@ -1,35 +0,0 @@
# 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',
},
),
]

View File

@ -1,33 +0,0 @@
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]

View File

@ -1,29 +0,0 @@
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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
from django.urls import path
from server.apps.main.views import index
app_name = 'main'
urlpatterns = [
path('hello/', index, name='hello'),
]

View File

@ -1,12 +0,0 @@
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')

View File

@ -1,39 +0,0 @@
"""
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)

View File

@ -1,11 +0,0 @@
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'))

View File

@ -1,16 +0,0 @@
# 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'

View File

@ -1,201 +0,0 @@
"""
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

View File

@ -1,15 +0,0 @@
"""
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'",)

View File

@ -1,77 +0,0 @@
# 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,
)

View File

@ -1,2 +0,0 @@
"""Overriding settings based on the environment."""

View File

@ -1,150 +0,0 @@
"""
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

View File

@ -1 +0,0 @@
"""Override any custom settings here."""

View File

@ -1,75 +0,0 @@
"""
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

View File

@ -1,14 +0,0 @@
# The humans responsible & technology colophon
# http://humanstxt.org/
## balsh
Team:
## Technologies
Language: English
Doctype: HTML5
Technologies: Python, Django

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@ -1,60 +0,0 @@
"""
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,
)

View File

@ -1,15 +0,0 @@
"""
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()

View File

@ -1,144 +0,0 @@
# 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

View File

@ -1,45 +0,0 @@
"""
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 '<h1>wemake-django-template</h1>'

View File

@ -1,16 +0,0 @@
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

View File

@ -1,17 +0,0 @@
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')

View File

@ -1,18 +0,0 @@
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)

View File

@ -1,55 +0,0 @@
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'