commit 8ff2ae26e74ecaca150818b6c7e9db91a37065d5 Author: godd0t Date: Wed May 10 09:02:59 2023 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f42bd8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Application Directories +src/staticfiles/ +src/mediafiles/ +src/media + +.venv +.idea +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +*.pot +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.env +.env.prod +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a2b13f9 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +PROJECTNAME := project_name +SHELL := /bin/sh +APP_NAME := project_name +BACKEND_APP_NAME := $(APP_NAME)-backend + +define HELP + +Manage $(PROJECTNAME). Usage: + +make lint Run linter +make format Run formatter +make test Run tests +make super-user Create super user +make make-migrations Make migrations +make migrate Migrate +make all Show help + +endef + +export HELP + +help: + @echo "$$HELP" + +lint: + @bash ./scripts/lint.sh + +format: + @bash ./scripts/format.sh + +test: + @bash ./scripts/test.sh + +super-user: + docker exec -it $(BACKEND_APP_NAME) sh "-c" \ + "python manage.py createsuperuser" + +make-migrations: + docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \ + "python manage.py makemigrations" + +migrate: + docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \ + "python manage.py migrate" + +all: help + +.PHONY: help lint format test super-user make-migrations migrate all \ No newline at end of file diff --git a/deployment/Dockerfile b/deployment/Dockerfile new file mode 100644 index 0000000..0ef795c --- /dev/null +++ b/deployment/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +WORKDIR /usr/src/app + +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install --no-install-recommends -y build-essential libpq-dev \ + && rm -rf /var/lib/apt/lists/* + + +COPY src/requirements.txt ./requirements.txt + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install pip --upgrade \ + && pip install -r requirements.txt + + +COPY deployment/scripts /app/deployment/scripts + +RUN chmod -R +x /app/deployment/scripts/* + +COPY src/ ./ diff --git a/deployment/scripts/backend/start.sh b/deployment/scripts/backend/start.sh new file mode 100644 index 0000000..30c1b1b --- /dev/null +++ b/deployment/scripts/backend/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Run migrations, collect static files and start server +if [ "$APP_ENV" != "prod" ]; then + python manage.py makemigrations --noinput + python manage.py migrate --noinput +else + python manage.py makemigrations --noinput + python manage.py migrate --noinput + python manage.py collectstatic --noinput +fi + +gunicorn "$APP_NAME".wsgi:application --bind 0.0.0.0:"$APP_PORT" --workers 3 --timeout 60 --graceful-timeout 60 --log-level=info diff --git a/deployment/scripts/backend/wait-for-it.sh b/deployment/scripts/backend/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/deployment/scripts/backend/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/deployment/scripts/celery/start-beat.sh b/deployment/scripts/celery/start-beat.sh new file mode 100644 index 0000000..0740f91 --- /dev/null +++ b/deployment/scripts/celery/start-beat.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +celery -A "$APP_NAME" beat -l info diff --git a/deployment/scripts/celery/start-worker.sh b/deployment/scripts/celery/start-worker.sh new file mode 100644 index 0000000..1b4eb4c --- /dev/null +++ b/deployment/scripts/celery/start-worker.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +celery -A "$APP_NAME" worker -l info diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..817967a --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,87 @@ +version: '3.9' + +services: + backend: + container_name: "${APP_NAME}-backend" + extra_hosts: + - "localhost:${APP_HOST}" + build: + context: . + dockerfile: deployment/Dockerfile + args: + - APP_NAME=${APP_NAME} + - APP_HOST=${APP_HOST} + - APP_PORT=${APP_PORT} + volumes: + - ./src:/usr/src/app/ + - ./deployment/scripts:/app/deployment/scripts/ + labels: + - "traefik.enable=true" + - "traefik.http.routers.${APP_NAME}-backend.rule=Host(`${APP_HOST}`)" + - "traefik.http.routers.${APP_NAME}-backend.entrypoints=web-secure" + - "traefik.http.services.${APP_NAME}-backend.loadbalancer.server.port=${APP_PORT}" +# ports: +# - "${APP_PORT}:${APP_PORT}" + env_file: .env + depends_on: + db: + condition: service_healthy + command: [ "/bin/sh", "/app/deployment/scripts/backend/start.sh" ] + + db: + image: postgres:15.2-alpine + container_name: "${APP_NAME}-db" + hostname: "${POSTGRES_HOST:-db}" + volumes: + - postgres_data_dir:/var/lib/postgresql/data/ + env_file: .env + ports: + - "5432:5432" + shm_size: 1g + healthcheck: + test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"] + + redis: + container_name: "${APP_NAME}-redis" + image: redis:latest + volumes: + - redis_data:/data + + celery-worker: &celery-worker + container_name: "${APP_NAME}-celery-worker" + build: + context: . + dockerfile: deployment/Dockerfile + volumes: + - ./src:/usr/src/app/ + - ./deployment/scripts:/app/deployment/scripts/ + env_file: .env + depends_on: + - db + - redis + - backend + command: [ "/bin/sh", "/app/deployment/scripts/celery/start-worker.sh" ] + + celery-beat: + <<: *celery-worker + container_name: "${APP_NAME}-celery-beat" + command: [ "/bin/sh", "/app/deployment/scripts/celery/start-beat.sh" ] + + traefik: + image: traefik:v2.5 + container_name: "${APP_NAME}-traefik" + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + networks: + - default + +volumes: + postgres_data_dir: + redis_data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2c3db7a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,93 @@ +version: '3.9' + +services: + backend: + container_name: "${APP_NAME}-backend" + build: + context: . + dockerfile: deployment/Dockerfile + args: + - APP_NAME=${APP_NAME} + - APP_HOST=${APP_HOST} + - APP_PORT=${APP_PORT} + volumes: + - ./src:/usr/src/app/ + - ./deployment/scripts:/app/deployment/scripts/ + labels: + - "traefik.enable=true" + - "traefik.http.routers.${APP_NAME}-backend.rule=Host(`${APP_HOST}`)" + - "traefik.http.routers.${APP_NAME}-backend.entrypoints=web-secure" + - "traefik.http.services.${APP_NAME}-backend.loadbalancer.server.port=${APP_PORT}" + - "traefik.http.routers.${APP_NAME}-backend.tls.certresolver=letsencrypt" + env_file: .env.prod +# depends_on: +# db: +# condition: service_healthy + command: [ "/bin/sh", "/app/deployment/scripts/backend/start.sh" ] + + db: + image: postgres:15.2-alpine + container_name: "${APP_NAME}-db" + hostname: "${POSTGRES_HOST:-db}" + volumes: + - postgres_data_dir:/var/lib/postgresql/data/ + env_file: .env.prod + ports: + - "5432:5432" + shm_size: 1g +# healthcheck: +# test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"] + + redis: + container_name: "${APP_NAME}-redis" + image: redis:latest + volumes: + - redis_data:/data + + celery-worker: &celery-worker + container_name: "${APP_NAME}-celery-worker" + build: + context: . + dockerfile: deployment/Dockerfile + volumes: + - ./src:/usr/src/app/ + - ./deployment/scripts:/app/deployment/scripts/ + env_file: .env.prod + depends_on: + - db + - redis + - backend + command: [ "/bin/sh", "/app/deployment/scripts/celery/start-worker.sh" ] + + celery-beat: + <<: *celery-worker + container_name: "${APP_NAME}-celery-beat" + command: [ "/bin/sh", "/app/deployment/scripts/celery/start-beat.sh" ] + + traefik: + image: traefik:v2.5 + container_name: "${APP_NAME}-traefik" + env_file: + - .env.prod + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.web-secure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.email=app@dev-test.com" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" + ports: + - "80:80" + - "443:443" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "./letsencrypt:/letsencrypt" + networks: + - default + +volumes: + postgres_data_dir: + redis_data: + letsencrypt: \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..92c17de --- /dev/null +++ b/env.example @@ -0,0 +1,15 @@ +# Application configuration variables +APP_NAME=project_name +APP_PORT=8000 +APP_ENV=dev + +# Postgres configuration variables +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres +POSTGRES_PORT=5432 +POSTGRES_HOST=db + +# Celery configuration variables +CELERY_BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..32b6d08 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,93 @@ +[project] +name = "project" +version = "0.1.0" +authors = ["Your Name "] + + +# TESTING +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --force-sugar --no-migrations --reuse-db --log-cli-level=INFO" +testpaths = [ + "tests", +] +pythonpath = [".", "src"] +python_files = "tests.py test_*.py *_tests.py" +DJANGO_SETTINGS_MODULE = "conf.settings.test" + +[tool.coverage.report] +fail_under = 85 +show_missing = "true" +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", + "import*", + "def __str__", + "def on_success", + "def clean", + "if missing", + "if relations.exists()", + "(FileDoesNotExistException, FileNotSupportedException)", +] + + +[tool.coverage.run] +omit = [ + "*/tests/*", + "*/migrations/*", + "*/urls.py", + "*/settings/*", + "*/wsgi.py", + "manage.py", + "*__init__.py", +] +source = ["src"] + + +# LINTING +[tool.black] +line-length = 88 +target-version = ['py311'] +include = '\.pyi?$' +# 'extend-exclude' excludes files or directories in addition to the defaults +extend-exclude = ''' +^/migrations/ +''' + + +[tool.ruff] +line-length = 88 # black default +extend-exclude = [ + "*/migrations/*", + "src/media/*", + "src/static/*", + "src/manage.py", + "*/test_data/*", + "*__init__.py", +] + +select = ["E", "F"] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "F405", # name may be undefined, or defined from star imports +] + + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.11. +target-version = "py311" + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + + +[tool.ruff.isort] +force-to-top = ["src"] +known-first-party = ["src"] diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 0000000..78ed311 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +APP_PATH="src" + +black $APP_PATH +ruff $APP_PATH --fix diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..a9d2fd3 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +APP_PATH="src" + +black $APP_PATH --check +ruff $APP_PATH diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..c6ac06d --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +coverage run -m pytest -v +exit 0 diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..1a1453b --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/src/project_name/__init__.py b/src/project_name/__init__.py new file mode 100644 index 0000000..15d7c50 --- /dev/null +++ b/src/project_name/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/src/project_name/asgi.py b/src/project_name/asgi.py new file mode 100644 index 0000000..bfc120f --- /dev/null +++ b/src/project_name/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for project_name project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings') + +application = get_asgi_application() diff --git a/src/project_name/celery.py b/src/project_name/celery.py new file mode 100644 index 0000000..60de013 --- /dev/null +++ b/src/project_name/celery.py @@ -0,0 +1,22 @@ +import os + +from celery import Celery + +# Set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings.dev') + +app = Celery('project_name') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print(f'Request: {self.request!r}') diff --git a/src/project_name/settings/__init__.py b/src/project_name/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/project_name/settings/base.py b/src/project_name/settings/base.py new file mode 100644 index 0000000..3ac33f9 --- /dev/null +++ b/src/project_name/settings/base.py @@ -0,0 +1,144 @@ +from os import getenv as os_getenv, path as os_path # noqa +from pathlib import Path + +from django.core.management.utils import get_random_secret_key + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = os_getenv("APP_SECRET_KEY", get_random_secret_key()) # If SECRET_KEY is not set, generate a random one +APP_ENV = os_getenv("APP_ENV", "dev") +DEBUG = os_getenv("DEBUG", "true").lower() in ["True", "true", "1", "yes", "y"] + +ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",") + +CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split(",") + +if DEBUG: + CORS_ORIGIN_ALLOW_ALL = True +else: + CORS_ALLOWED_ORIGINS = os_getenv("CORS_ALLOWED_ORIGINS", "http://localhost").split(",") + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + "corsheaders", + "test_app", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + "corsheaders.middleware.CorsMiddleware", # CorsMiddleware should be placed as high as possible, + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'project_name.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'project_name.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + "ENGINE": "django.db.backends.postgresql", + 'NAME': os_getenv("POSTGRES_DB", "postgres"), + "USER": os_getenv("POSTGRES_USER", "postgres"), + "PASSWORD": os_getenv("POSTGRES_PASSWORD", "postgres"), + "HOST": os_getenv("POSTGRES_HOST", "db"), + "PORT": os_getenv("POSTGRES_PORT", "5432"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +MEDIA_URL = 'media/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# Redis +REDIS_DB_KEYS = { + "dev": 0, + "test": 1, + "prod": 2, +} + +# Redis settings + +REDIS_HOST = os_getenv("REDIS_HOST", "redis") +REDIS_PORT = os_getenv("REDIS_PORT", 6379) + +REDIS_DB = REDIS_DB_KEYS.get(APP_ENV, 0) +REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}" + +# Celery settings + +CELERY_BROKER_URL = REDIS_URL +CELERY_RESULT_BACKEND = REDIS_URL diff --git a/src/project_name/settings/dev.py b/src/project_name/settings/dev.py new file mode 100644 index 0000000..44c8b96 --- /dev/null +++ b/src/project_name/settings/dev.py @@ -0,0 +1 @@ +from .base import * # noqa diff --git a/src/project_name/settings/prod.py b/src/project_name/settings/prod.py new file mode 100644 index 0000000..79c5393 --- /dev/null +++ b/src/project_name/settings/prod.py @@ -0,0 +1 @@ +from .base import * # noqa \ No newline at end of file diff --git a/src/project_name/settings/test.py b/src/project_name/settings/test.py new file mode 100644 index 0000000..44c8b96 --- /dev/null +++ b/src/project_name/settings/test.py @@ -0,0 +1 @@ +from .base import * # noqa diff --git a/src/project_name/urls.py b/src/project_name/urls.py new file mode 100644 index 0000000..8063e4b --- /dev/null +++ b/src/project_name/urls.py @@ -0,0 +1,14 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path("test_app/", include("test_app.urls")), +] + +if settings.DEBUG: + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/src/project_name/wsgi.py b/src/project_name/wsgi.py new file mode 100644 index 0000000..461bec5 --- /dev/null +++ b/src/project_name/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for project_name 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/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings.dev') + +application = get_wsgi_application() diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..c2a652d --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,21 @@ +amqp==5.1.1 +asgiref==3.6.0 +billiard==3.6.4.0 +celery==5.2.7 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +Django==4.2.1 +django-cors-headers==3.14.0 +gunicorn==20.1.0 +kombu==5.2.4 +prompt-toolkit==3.0.38 +psycopg==3.1.9 +pytz==2023.3 +six==1.16.0 +sqlparse==0.4.4 +typing_extensions==4.5.0 +vine==5.0.0 +wcwidth==0.2.6 +redis==4.5.5 diff --git a/src/test_app/__init__.py b/src/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_app/admin.py b/src/test_app/admin.py new file mode 100644 index 0000000..a4e11e9 --- /dev/null +++ b/src/test_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin # noqa + +# Register your models here. diff --git a/src/test_app/apps.py b/src/test_app/apps.py new file mode 100644 index 0000000..7635468 --- /dev/null +++ b/src/test_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'test_app' diff --git a/src/test_app/migrations/__init__.py b/src/test_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_app/models.py b/src/test_app/models.py new file mode 100644 index 0000000..56b48a5 --- /dev/null +++ b/src/test_app/models.py @@ -0,0 +1,3 @@ +from django.db import models # noqa + +# Create your models here. diff --git a/src/test_app/tests.py b/src/test_app/tests.py new file mode 100644 index 0000000..b8ab89e --- /dev/null +++ b/src/test_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase # noqa + +# Create your tests here. diff --git a/src/test_app/urls.py b/src/test_app/urls.py new file mode 100644 index 0000000..c49391a --- /dev/null +++ b/src/test_app/urls.py @@ -0,0 +1,5 @@ +from django.urls import path # noqa + +urlpatterns = [ + # ... other urls +] diff --git a/src/test_app/views.py b/src/test_app/views.py new file mode 100644 index 0000000..6921674 --- /dev/null +++ b/src/test_app/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render # noqa + +# Create your views here. diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/conftest.py b/src/tests/conftest.py new file mode 100644 index 0000000..3e57bcd --- /dev/null +++ b/src/tests/conftest.py @@ -0,0 +1,19 @@ +import pytest +from django.contrib.auth.models import User + + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + """ + This fixture enables database access for all tests. + """ + pass + + +@pytest.fixture +def test_user(): + return User.objects.create_user( + username='test_user', + email="test_user@test.com", + password="test_password" + ) diff --git a/src/tests/test_app_1/__init__.py b/src/tests/test_app_1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/test_app_1/test_example.py b/src/tests/test_app_1/test_example.py new file mode 100644 index 0000000..4c218a1 --- /dev/null +++ b/src/tests/test_app_1/test_example.py @@ -0,0 +1,5 @@ + +def test_example(test_user): + assert test_user.username == 'test_user' + assert test_user.email is not None +