mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-22 23:17:09 +00:00
init: added backend boilerplate
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Ruff files
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile
|
||||||
|
Dockerfile.staticfiles
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Template env file
|
||||||
|
.env.template
|
||||||
|
|
||||||
|
# Collected static files
|
||||||
|
static
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Change all vars before going to production and remove all comments (!)
|
||||||
|
# Below all environment variables and default values
|
||||||
|
|
||||||
|
DJANGO_SECRET_KEY=very_insecure_key
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
||||||
|
DJANGO_CORS_ALLOWED_ORIGINS=*
|
||||||
|
DJANGO_INTERNAL_IPS=127.0.0.1
|
||||||
|
DJANGO_LANGUAGE_CODE=en-us
|
||||||
|
DJANGO_STATIC_URL=static/
|
||||||
|
REDIS_URI=redis://localhost:6379
|
||||||
|
DJANGO_DB_URI=sqlite:///db.sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
# Storages
|
||||||
|
|
||||||
|
MINIO_ENDPOINT=
|
||||||
|
MINIO_CUSTOM_ENDPOINT_URL=
|
||||||
|
MINIO_ACCESS_KEY=
|
||||||
|
MINIO_SECRET_KEY=
|
||||||
|
MINIO_USE_HTTPS=False
|
||||||
|
MINIO_MEDIA_BUCKET_NAME=project_name-media
|
||||||
|
|
||||||
|
|
||||||
|
# Applyable if you installing using docker compose
|
||||||
|
|
||||||
|
DJANGO_CREATE_SUPERUSER=False
|
||||||
|
DJANGO_SUPERUSER_USERNAME=
|
||||||
|
DJANGO_SUPERUSER_EMAIL=
|
||||||
|
DJANGO_SUPERUSER_PASSWORD=
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Ruff files
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
# Collected static files
|
||||||
|
static
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Stage 1: Install dependencies
|
||||||
|
FROM docker.io/python:3.11-alpine3.20 AS builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONOPTIMIZE=2 \
|
||||||
|
UV_COMPILE_BYTECODE=1 \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/opt/venv
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
|
||||||
|
RUN uv sync --no-dev --no-install-project --no-cache
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 2: Start the application
|
||||||
|
FROM docker.io/python:3.11-alpine3.20
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN adduser -D -g '' app && chown -R app:app ./
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONOPTIMIZE=2 \
|
||||||
|
PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health?format=json || exit 1
|
||||||
|
|
||||||
|
CMD gunicorn config.wsgi --workers=8 -b 0.0.0.0:8080 --access-logfile - --error-logfile -
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Stage 1: Install dependencies and compile staticfiles
|
||||||
|
FROM docker.io/python:3.11-alpine3.20 AS builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONOPTIMIZE=2 \
|
||||||
|
UV_COMPILE_BYTECODE=1 \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/opt/venv
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
|
||||||
|
RUN uv sync --no-dev --no-install-project --no-cache
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN uv run python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Stage 2: Start nginx and serve staticfiles
|
||||||
|
FROM docker.io/nginx:latest
|
||||||
|
|
||||||
|
COPY --from=builder /app/static /usr/share/nginx/html
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# project_name Backend
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the following installed on your system:
|
||||||
|
|
||||||
|
- [Python](https://www.python.org/) (>=3.10,<3.12)
|
||||||
|
- [uv](https://docs.astral.sh/uv/)
|
||||||
|
- [Docker](https://www.docker.com/) (for containerized setup)
|
||||||
|
|
||||||
|
## Basic setup
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Clone the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone project_name
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Go to the project directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd project_name/services/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Customize environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
|
||||||
|
And setup env vars according to your needs.
|
||||||
|
|
||||||
|
#### Install dependencies
|
||||||
|
|
||||||
|
##### For dev environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --all-extras
|
||||||
|
```
|
||||||
|
|
||||||
|
##### For prod environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --no-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running
|
||||||
|
|
||||||
|
##### Apply migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Start celery worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
celery -A config worker -l INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Start server
|
||||||
|
|
||||||
|
In dev mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
In prod mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run gunicorn config.wsgi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Containerized setup
|
||||||
|
|
||||||
|
### Clone the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone project_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go to the project directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd project_name/services/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t project_name-backend .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize environment
|
||||||
|
|
||||||
|
Customize environment with `docker run` command (or bind .env file to container), for all environment vars and default values see [.env.template](./.env.template).
|
||||||
|
|
||||||
|
### Run docker image
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 --name project_name-backend project_name-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Celery worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name project_name-celery-worker project_name-backend celery -A config worker -l INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend will be available on [127.0.0.1:8080](http://127.0.0.1:8080).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Clone the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone project_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go to the project directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd project_name/services/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --all-extras
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run coverage run --source="." manage.py test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run coverage report
|
||||||
|
```
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from health_check.views import MainView
|
||||||
|
|
||||||
|
from api.v1.router import router as api_v1_router
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", api_v1_router.urls),
|
||||||
|
# Health endpoint
|
||||||
|
path("health", MainView.as_view(), name="health_check_home"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from http import HTTPStatus as status
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import django.core.exceptions
|
||||||
|
import django.http
|
||||||
|
import ninja.errors
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
|
from config.errors import ConflictError, ForbiddenError
|
||||||
|
|
||||||
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_validation_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ninja.errors.ValidationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": exc.errors},
|
||||||
|
status=status.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_django_validation_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: django.core.exceptions.ValidationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
detail = list(exc)
|
||||||
|
|
||||||
|
if hasattr(exc, "error_dict"):
|
||||||
|
detail = dict(exc)
|
||||||
|
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": detail},
|
||||||
|
status=status.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_authentication_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ninja.errors.AuthenticationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": status.UNAUTHORIZED.phrase},
|
||||||
|
status=status.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_forbidden_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ForbiddenError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": exc.message},
|
||||||
|
status=status.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_not_found_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": status.NOT_FOUND.phrase},
|
||||||
|
status=status.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_conflict_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ConflictError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
detail = list(exc.validation_error)
|
||||||
|
|
||||||
|
if hasattr(exc, "error_dict"):
|
||||||
|
detail = dict(exc.validation_error)
|
||||||
|
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": detail},
|
||||||
|
status=status.CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_unknown_exception(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
logger.exception(exc)
|
||||||
|
|
||||||
|
return router.create_response(
|
||||||
|
request,
|
||||||
|
{"detail": status.INTERNAL_SERVER_ERROR.phrase},
|
||||||
|
status=status.INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
exception_handlers: list[tuple[Any, Callable]] = [
|
||||||
|
(ninja.errors.ValidationError, handle_validation_error),
|
||||||
|
(django.core.exceptions.ValidationError, handle_django_validation_error),
|
||||||
|
(ninja.errors.AuthenticationError, handle_authentication_error),
|
||||||
|
(ForbiddenError, handle_forbidden_error),
|
||||||
|
(django.http.Http404, handle_not_found_error),
|
||||||
|
(ConflictError, handle_conflict_error),
|
||||||
|
(Exception, handle_unknown_exception),
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from ninja import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class PingOut(Schema):
|
||||||
|
status: str = "ok"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from ninja import Router
|
||||||
|
|
||||||
|
from api.v1.ping import schemas
|
||||||
|
|
||||||
|
router = Router(tags=["ping"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"",
|
||||||
|
response={
|
||||||
|
status.OK: schemas.PingOut,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def ping(request: HttpRequest) -> tuple[status, schemas.PingOut]:
|
||||||
|
return status.OK, schemas.PingOut
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
|
from api.v1 import handlers
|
||||||
|
from api.v1.ping.views import router as ping_router
|
||||||
|
|
||||||
|
router = NinjaAPI(
|
||||||
|
title="project_name API",
|
||||||
|
version="1",
|
||||||
|
description="API docs for project_name",
|
||||||
|
openapi_url="/docs/openapi.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
router.add_router(
|
||||||
|
"ping",
|
||||||
|
ping_router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for exception, handler in handlers.exception_handlers:
|
||||||
|
router.add_exception_handler(exception, partial(handler, router=router))
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ninja import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestError(Schema):
|
||||||
|
detail: Any
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(Schema):
|
||||||
|
detail: str = status.UNAUTHORIZED.phrase
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenError(Schema):
|
||||||
|
detail: str = status.FORBIDDEN.phrase
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(Schema):
|
||||||
|
detail: str = status.NOT_FOUND.phrase
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictError(Schema):
|
||||||
|
detail: Any
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import contextlib
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
name = "apps.core"
|
||||||
|
label = "core"
|
||||||
|
|
||||||
|
def ready(self) -> None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
cache.add("current_date", 0, timeout=None)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from config.errors import ConflictError
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.validate()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
self,
|
||||||
|
validate_unique: bool = True,
|
||||||
|
validate_constraints: bool = True,
|
||||||
|
include: list[models.Field] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.full_clean(
|
||||||
|
validate_unique=False,
|
||||||
|
validate_constraints=False,
|
||||||
|
exclude=(
|
||||||
|
field.name
|
||||||
|
for field in set(self._meta.get_fields()) - set(include)
|
||||||
|
)
|
||||||
|
if include
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if validate_unique:
|
||||||
|
try:
|
||||||
|
self.validate_unique()
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ConflictError(e) from None
|
||||||
|
|
||||||
|
if validate_constraints:
|
||||||
|
try:
|
||||||
|
self.validate_constraints()
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ConflictError(e) from None
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from config.celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ("celery_app",)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""ASGI config for project_name."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
app = Celery("project_name")
|
||||||
|
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictError(Exception):
|
||||||
|
def __init__(self, validation_error: ValidationError) -> None:
|
||||||
|
self.validation_error = validation_error
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenError(Exception):
|
||||||
|
def __init__(self, message: str = status.FORBIDDEN.phrase) -> None:
|
||||||
|
self.message = message
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from django.http import HttpRequest, JsonResponse
|
||||||
|
|
||||||
|
|
||||||
|
def handler400(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return JsonResponse(
|
||||||
|
status=status.BAD_REQUEST,
|
||||||
|
data={"detail": status.BAD_REQUEST.phrase},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler403(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return JsonResponse(
|
||||||
|
status=status.FORBIDDEN,
|
||||||
|
data={"detail": status.FORBIDDEN.phrase},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler404(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return JsonResponse(
|
||||||
|
status=status.NOT_FOUND,
|
||||||
|
data={"detail": status.NOT_FOUND.phrase},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler500(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return JsonResponse(
|
||||||
|
status=status.INTERNAL_SERVER_ERROR,
|
||||||
|
data={"detail": status.INTERNAL_SERVER_ERROR.phrase},
|
||||||
|
)
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
"""Django settings for project_name."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import django_stubs_ext
|
||||||
|
import environ
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
env = environ.Env()
|
||||||
|
environ.Env.read_env(BASE_DIR / ".env")
|
||||||
|
|
||||||
|
django_stubs_ext.monkeypatch()
|
||||||
|
|
||||||
|
|
||||||
|
# Common settings
|
||||||
|
|
||||||
|
DEBUG = env("DJANGO_DEBUG", default=False)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = env(
|
||||||
|
"DJANGO_ALLOWED_HOSTS",
|
||||||
|
list,
|
||||||
|
default=["localhost", "127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Integrations
|
||||||
|
|
||||||
|
YANDEX_CLOUD_FOLDER_ID = env("YANDEX_CLOUD_FOLDER_ID", default=None)
|
||||||
|
|
||||||
|
YANDEX_CLOUD_API_KEY = env("YANDEX_CLOUD_API_KEY", default=None)
|
||||||
|
|
||||||
|
YANDEX_CLOUD_INTEGRATION_ENABLED = (
|
||||||
|
YANDEX_CLOUD_FOLDER_ID and YANDEX_CLOUD_API_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Register healthchecks
|
||||||
|
|
||||||
|
# plugin_dir.register(SomeHealthCheckClass)
|
||||||
|
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
|
||||||
|
REDIS_URI = env("REDIS_URI", default="redis://localhost:6379")
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||||||
|
"LOCATION": REDIS_URI,
|
||||||
|
"TIMEOUT": None,
|
||||||
|
"KEY_PREFIX": "backend",
|
||||||
|
"VERSION": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = REDIS_URI
|
||||||
|
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URI
|
||||||
|
|
||||||
|
CELERY_TIMEZONE = "UTC"
|
||||||
|
|
||||||
|
CELERY_WORKER_SEND_TASK_EVENTS = True
|
||||||
|
|
||||||
|
CELERY_TASK_SEND_SENT_EVENT = True
|
||||||
|
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
|
||||||
|
DB_URI = env.db_url("DJANGO_DB_URI", default="sqlite:///db.sqlite3")
|
||||||
|
|
||||||
|
DATABASES = {"default": {**DB_URI, "CONN_MAX_AGE": 50}}
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
|
||||||
|
STATIC_ROOT = BASE_DIR / "static"
|
||||||
|
|
||||||
|
STATIC_URL = env("DJANGO_STATIC_URL", default="static/")
|
||||||
|
|
||||||
|
STATICFILES_DIRS: list[str] = []
|
||||||
|
|
||||||
|
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Files
|
||||||
|
|
||||||
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440
|
||||||
|
|
||||||
|
# Minio
|
||||||
|
|
||||||
|
MINIO_STORAGE_ENDPOINT = env("MINIO_ENDPOINT", default=None)
|
||||||
|
|
||||||
|
MINIO_STORAGE_ACCESS_KEY = env("MINIO_ACCESS_KEY", default=None)
|
||||||
|
|
||||||
|
MINIO_STORAGE_SECRET_KEY = env("MINIO_SECRET_KEY", default=None)
|
||||||
|
|
||||||
|
MINIO_STORAGE_USE_HTTPS = env("MINIO_USE_HTTPS", default=False)
|
||||||
|
|
||||||
|
MINIO_STORAGE_MEDIA_BUCKET_NAME = env(
|
||||||
|
"MINIO_MEDIA_BUCKET_NAME", default="project_name-media"
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIO_STORAGE_AUTO_CREATE_MEDIA_BUCKET = True
|
||||||
|
|
||||||
|
MINIO_STORAGE_AUTO_CREATE_MEDIA_POLICY = "GET_ONLY"
|
||||||
|
|
||||||
|
MINIO_DEFAULT_CUSTOM_ENDPOINT_URL = (
|
||||||
|
"https://"
|
||||||
|
if MINIO_STORAGE_USE_HTTPS
|
||||||
|
else "http://" + str(MINIO_STORAGE_ENDPOINT)
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIO_STORAGE_MEDIA_URL = (
|
||||||
|
env("MINIO_CUSTOM_ENDPOINT_URL", default=MINIO_DEFAULT_CUSTOM_ENDPOINT_URL)
|
||||||
|
+ "/"
|
||||||
|
f"{MINIO_STORAGE_MEDIA_BUCKET_NAME}"
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIO_STORAGE_DEFAULT_ACL = "public-read"
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "minio_storage.storage.MinioMediaStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Cors
|
||||||
|
|
||||||
|
CORS_ALLOWED_ORIGINS_FROM_ENV = env("DJANGO_CORS_ALLOWED_ORIGINS", list, ["*"])
|
||||||
|
|
||||||
|
if CORS_ALLOWED_ORIGINS_FROM_ENV == ["*"]:
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
else:
|
||||||
|
CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS_FROM_ENV
|
||||||
|
|
||||||
|
|
||||||
|
# Forms
|
||||||
|
|
||||||
|
FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
|
||||||
|
|
||||||
|
FORMS_URLFIELD_ASSUME_HTTPS = False
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
|
||||||
|
DATE_FORMAT = "N j, Y"
|
||||||
|
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d", # '2006-10-25'
|
||||||
|
"%m/%d/%Y", # '10/25/2006'
|
||||||
|
"%m/%d/%y", # '10/25/06'
|
||||||
|
"%b %d %Y", # 'Oct 25 2006'
|
||||||
|
"%b %d, %Y", # 'Oct 25, 2006'
|
||||||
|
"%d %b %Y", # '25 Oct 2006'
|
||||||
|
"%d %b, %Y", # '25 Oct, 2006'
|
||||||
|
"%B %d %Y", # 'October 25 2006'
|
||||||
|
"%B %d, %Y", # 'October 25, 2006'
|
||||||
|
"%d %B %Y", # '25 October 2006'
|
||||||
|
"%d %B, %Y", # '25 October, 2006'
|
||||||
|
]
|
||||||
|
|
||||||
|
DATETIME_FORMAT = "N j, Y, H:i:s"
|
||||||
|
|
||||||
|
DATETIME_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200'
|
||||||
|
"%Y-%m-%d %H:%M", # '2006-10-25 14:30'
|
||||||
|
"%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
|
||||||
|
"%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200'
|
||||||
|
"%m/%d/%Y %H:%M", # '10/25/2006 14:30'
|
||||||
|
"%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
|
||||||
|
"%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200'
|
||||||
|
"%m/%d/%y %H:%M", # '10/25/06 14:30'
|
||||||
|
]
|
||||||
|
|
||||||
|
DECIMAL_SEPARATOR = "."
|
||||||
|
|
||||||
|
FIRST_DAY_OF_WEEK = 1
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH = None
|
||||||
|
|
||||||
|
LANGUAGE_CODE = env("DJANGO_LANGUAGE_CODE", default="en-us")
|
||||||
|
|
||||||
|
LANGUAGES = [("en", _("English")), ("ru", _("Russian"))]
|
||||||
|
|
||||||
|
LOCALE_PATHS: list[str] = []
|
||||||
|
|
||||||
|
MONTH_DAY_FORMAT = "F j"
|
||||||
|
|
||||||
|
NUMBER_GROUPING = 0
|
||||||
|
|
||||||
|
SHORT_DATE_FORMAT = "m/d/Y"
|
||||||
|
|
||||||
|
SHORT_DATETIME_FORMAT = "m/d/Y H:i:s"
|
||||||
|
|
||||||
|
THOUSAND_SEPARATOR = ","
|
||||||
|
|
||||||
|
TIME_FORMAT = "H:i:s"
|
||||||
|
|
||||||
|
TIME_INPUT_FORMATS = [
|
||||||
|
"%H:%M:%S", # '14:30:59'
|
||||||
|
"%H:%M:%S.%f", # '14:30:59.000200'
|
||||||
|
"%H:%M", # '14:30'
|
||||||
|
]
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_THOUSAND_SEPARATOR = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
YEAR_MONTH_FORMAT = "F Y"
|
||||||
|
|
||||||
|
|
||||||
|
# HTTP
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_MEMORY_SIZE = None
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FILES = None
|
||||||
|
|
||||||
|
DEFAULT_CHARSET = "utf-8"
|
||||||
|
|
||||||
|
FORCE_SCRIPT_NAME = None
|
||||||
|
|
||||||
|
INTERNAL_IPS = env(
|
||||||
|
"DJANGO_INTERNAL_IPS",
|
||||||
|
list,
|
||||||
|
default=["127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django_guid.middleware.guid_middleware",
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
SIGNING_BACKEND = "django.core.signing.TimestampSigner"
|
||||||
|
|
||||||
|
USE_X_FORWARDED_HOST = False
|
||||||
|
|
||||||
|
USE_X_FORWARDED_PORT = False
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
|
||||||
|
LOGGER_NAME = "project_name"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(LOGGER_NAME)
|
||||||
|
|
||||||
|
LOGGING_FILTERS = {
|
||||||
|
"require_debug_true": {
|
||||||
|
"()": "django.utils.log.RequireDebugTrue",
|
||||||
|
},
|
||||||
|
"require_debug_false": {
|
||||||
|
"()": "django.utils.log.RequireDebugFalse",
|
||||||
|
},
|
||||||
|
"correlation_id": {
|
||||||
|
"()": "django_guid.log_filters.CorrelationId",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_FORMATTERS = {
|
||||||
|
"json": {
|
||||||
|
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||||
|
"format": (
|
||||||
|
"{levelname}{correlation_id}{asctime}"
|
||||||
|
"{name}{pathname}{lineno}{message}"
|
||||||
|
),
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"()": "colorlog.ColoredFormatter",
|
||||||
|
"format": (
|
||||||
|
"{log_color}[{levelname}]{reset} "
|
||||||
|
"{light_black}{asctime} {name} | {pathname}:{lineno}{reset}\n"
|
||||||
|
"{bold_black}{message}{reset}"
|
||||||
|
),
|
||||||
|
"log_colors": {
|
||||||
|
"DEBUG": "bold_green",
|
||||||
|
"INFO": "bold_cyan",
|
||||||
|
"WARNING": "bold_yellow",
|
||||||
|
"ERROR": "bold_red",
|
||||||
|
"CRITICAL": "bold_purple",
|
||||||
|
},
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_HANDLERS = {
|
||||||
|
"console_debug": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "DEBUG",
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"formatter": "text",
|
||||||
|
},
|
||||||
|
"console_prod": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "INFO",
|
||||||
|
"filters": ["require_debug_false", "correlation_id"],
|
||||||
|
"formatter": "json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_LOGGERS = {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.request": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.server": {
|
||||||
|
"handlers": ["console_debug"],
|
||||||
|
"level": "INFO",
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.template": {"handlers": []},
|
||||||
|
"django.db.backends.schema": {"handlers": []},
|
||||||
|
"django.security": {"handlers": [], "propagate": True},
|
||||||
|
"django.db.backends": {
|
||||||
|
"handlers": ["console_debug"],
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"health-check": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
LOGGER_NAME: {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "DEBUG" if DEBUG else "INFO",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"filters": LOGGING_FILTERS,
|
||||||
|
"formatters": LOGGING_FORMATTERS,
|
||||||
|
"handlers": LOGGING_HANDLERS,
|
||||||
|
"loggers": LOGGING_LOGGERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_CONFIG = "logging.config.dictConfig"
|
||||||
|
|
||||||
|
|
||||||
|
# Models
|
||||||
|
|
||||||
|
ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {}
|
||||||
|
|
||||||
|
FIXTURE_DIRS: list[str] = []
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# Build-in apps
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
# Healthcheck
|
||||||
|
"health_check",
|
||||||
|
"health_check.db",
|
||||||
|
"health_check.cache",
|
||||||
|
"health_check.storage",
|
||||||
|
"health_check.contrib.migrations",
|
||||||
|
"health_check.contrib.celery",
|
||||||
|
"health_check.contrib.celery_ping",
|
||||||
|
# Third-party apps
|
||||||
|
"corsheaders",
|
||||||
|
"django_extensions",
|
||||||
|
"django_guid",
|
||||||
|
"ninja",
|
||||||
|
"minio_storage",
|
||||||
|
# Internal apps
|
||||||
|
"apps.core",
|
||||||
|
]
|
||||||
|
|
||||||
|
# GUID
|
||||||
|
|
||||||
|
DJANGO_GUID = {
|
||||||
|
"GUID_HEADER_NAME": "Correlation-ID",
|
||||||
|
"VALIDATE_GUID": True,
|
||||||
|
"RETURN_HEADER": True,
|
||||||
|
"EXPOSE_HEADER": True,
|
||||||
|
"INTEGRATIONS": [],
|
||||||
|
"IGNORE_URLS": [],
|
||||||
|
"UUID_LENGTH": 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_AGE = 31449600
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_DOMAIN = None
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_HTTPONLY = False
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_NAME = "django_language"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = None
|
||||||
|
|
||||||
|
CSRF_COOKIE_AGE = 31449600
|
||||||
|
|
||||||
|
CSRF_COOKIE_DOMAIN = None
|
||||||
|
|
||||||
|
CSRF_COOKIE_HTTPONLY = False
|
||||||
|
|
||||||
|
CSRF_COOKIE_NAME = "djangocsrftoken"
|
||||||
|
|
||||||
|
CSRF_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
CSRF_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
CSRF_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
CSRF_FAILURE_VIEW = "django.views.csrf.csrf_failure"
|
||||||
|
|
||||||
|
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = env(
|
||||||
|
"DJANGO_CSRF_TRUSTED_ORIGINS",
|
||||||
|
list,
|
||||||
|
default=["http://localhost", "http://127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
CSRF_USE_SESSIONS = False
|
||||||
|
|
||||||
|
SECRET_KEY = env("DJANGO_SECRET_KEY", default="very_insecure_key")
|
||||||
|
|
||||||
|
SECRET_KEY_FALLBACKS: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
# Sessions
|
||||||
|
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
|
SESSION_COOKIE_AGE = 1209600
|
||||||
|
|
||||||
|
SESSION_COOKIE_DOMAIN = None
|
||||||
|
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
|
||||||
|
SESSION_COOKIE_NAME = "djangosessionid"
|
||||||
|
|
||||||
|
SESSION_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
SESSION_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.db"
|
||||||
|
|
||||||
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||||
|
|
||||||
|
SESSION_FILE_PATH = None
|
||||||
|
|
||||||
|
SESSION_SAVE_EVERY_REQUEST = False
|
||||||
|
|
||||||
|
SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
|
||||||
|
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"autoescape": True,
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
"debug": DEBUG,
|
||||||
|
"string_if_invalid": "",
|
||||||
|
"file_charset": "utf-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
TEST_NON_SERIALIZED_APPS: list[str] = []
|
||||||
|
|
||||||
|
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
|
||||||
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
|
|
||||||
|
# debug-toolbar
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_ENABLED = False
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
import debug_toolbar # noqa: F401
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_ENABLED = True
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_CONFIG = {"SHOW_COLLAPSED": True, "UPDATE_ON_FETCH": True}
|
||||||
|
|
||||||
|
if DEBUG and DEBUG_TOOLBAR_ENABLED:
|
||||||
|
INSTALLED_APPS.append("debug_toolbar")
|
||||||
|
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""URL configuration for project_name."""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from config import handlers
|
||||||
|
|
||||||
|
admin.site.site_title = "project_name"
|
||||||
|
admin.site.site_header = "project_name"
|
||||||
|
admin.site.index_title = "project_name"
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Admin urls
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
# API urls
|
||||||
|
path("", include("api.urls")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if settings.DEBUG and settings.DEBUG_TOOLBAR_ENABLED:
|
||||||
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
|
||||||
|
urlpatterns += debug_toolbar_urls()
|
||||||
|
|
||||||
|
|
||||||
|
handler400 = handlers.handler400
|
||||||
|
|
||||||
|
handler403 = handlers.handler403
|
||||||
|
|
||||||
|
handler404 = handlers.handler404
|
||||||
|
|
||||||
|
handler500 = handlers.handler500
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""WSGI config for project_name."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
e = """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?"""
|
||||||
|
raise ImportError(e) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
[project]
|
||||||
|
name = "project_name-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10,<3.12"
|
||||||
|
dependencies = [
|
||||||
|
"celery>=5.4.0",
|
||||||
|
"colorlog>=6.9.0",
|
||||||
|
"django-cors-headers>=4.6.0",
|
||||||
|
"django-environ>=0.11.2",
|
||||||
|
"django-extensions>=3.2.3",
|
||||||
|
"django-guid>=3.5.0",
|
||||||
|
"django-health-check>=3.18.3",
|
||||||
|
"django-minio-storage>=0.5.7",
|
||||||
|
"django-ninja>=1.3.0",
|
||||||
|
"django-stubs-ext>=5.1.3",
|
||||||
|
"gunicorn>=23.0.0",
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"pillow>=11.1.0",
|
||||||
|
"psycopg2-binary>=2.9.10",
|
||||||
|
"pydantic>=2.10.5",
|
||||||
|
"pyjwt>=2.10.1",
|
||||||
|
"python-json-logger>=3.2.1",
|
||||||
|
"pytz>=2024.2",
|
||||||
|
"redis>=5.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"coverage>=7.6.12",
|
||||||
|
"django-debug-toolbar>=4.4.6",
|
||||||
|
"django-stubs[compatible-mypy]>=5.1.3",
|
||||||
|
"mypy>=1.15.0",
|
||||||
|
"ruff>=0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
builtins = []
|
||||||
|
cache-dir = ".ruff_cache"
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"dist",
|
||||||
|
"migrations",
|
||||||
|
"node_modules",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
extend-exclude = []
|
||||||
|
extend-include = []
|
||||||
|
fix = false
|
||||||
|
fix-only = false
|
||||||
|
force-exclude = true
|
||||||
|
include = ["*.py", "*.pyi", "*.ipynb", "**/pyproject.toml"]
|
||||||
|
indent-width = 4
|
||||||
|
line-length = 79
|
||||||
|
namespace-packages = []
|
||||||
|
output-format = "full"
|
||||||
|
preview = false
|
||||||
|
required-version = ">=0.8.4"
|
||||||
|
respect-gitignore = true
|
||||||
|
show-fixes = true
|
||||||
|
src = [".", "src"]
|
||||||
|
target-version = "py310"
|
||||||
|
unsafe-fixes = false
|
||||||
|
|
||||||
|
[tool.ruff.analyze]
|
||||||
|
detect-string-imports = true
|
||||||
|
direction = "Dependencies"
|
||||||
|
exclude = []
|
||||||
|
include-dependencies = {}
|
||||||
|
preview = false
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = 79
|
||||||
|
exclude = []
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "lf"
|
||||||
|
preview = false
|
||||||
|
quote-style = "double"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
allowed-confusables = ["ℹ"]
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
exclude = ["tests.py"]
|
||||||
|
explicit-preview-rules = false
|
||||||
|
extend-fixable = []
|
||||||
|
extend-per-file-ignores = {}
|
||||||
|
extend-safe-fixes = []
|
||||||
|
extend-select = []
|
||||||
|
extend-unsafe-fixes = []
|
||||||
|
external = []
|
||||||
|
fixable = ["ALL"]
|
||||||
|
ignore = [
|
||||||
|
"ARG",
|
||||||
|
"D",
|
||||||
|
"ANN401",
|
||||||
|
"COM812",
|
||||||
|
"DJ001",
|
||||||
|
"DJ007",
|
||||||
|
"FBT001",
|
||||||
|
"FBT002",
|
||||||
|
"N813",
|
||||||
|
"PLR2004",
|
||||||
|
"PT009",
|
||||||
|
"PT027",
|
||||||
|
"RUF001",
|
||||||
|
"S311",
|
||||||
|
]
|
||||||
|
logger-objects = []
|
||||||
|
per-file-ignores = {}
|
||||||
|
preview = false
|
||||||
|
select = ["ALL"]
|
||||||
|
task-tags = ["TODO", "FIXME", "HACK", "WORKOUT"]
|
||||||
|
typing-modules = []
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
[tool.ruff.lint.pylint]
|
||||||
|
max-args = 6
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
strict = false
|
||||||
|
show_error_context = false
|
||||||
|
no_implicit_optional = false
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "config.settings"
|
||||||
|
strict_settings = false
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
"manage.py",
|
||||||
|
"config/wsgi.py",
|
||||||
|
"config/asgi.py",
|
||||||
|
"config/urls.py",
|
||||||
|
"config/settings.py",
|
||||||
|
"config/handlers.py",
|
||||||
|
"config/errors.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
skip_covered = true
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
GREEN='\033[1;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
uvx ruff format .
|
||||||
|
uvx ruff check . --fix
|
||||||
|
printf "${GREEN}Linters/formatters runned${NC}\n"
|
||||||
|
|
||||||
|
uv run python manage.py makemigrations --check
|
||||||
|
uv run python manage.py test
|
||||||
|
printf "${GREEN}Tests runned${NC}\n"
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Migration failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then
|
||||||
|
python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user