You've already forked Promocode-API
mirror of
https://github.com/devitq/Promocode-API.git
synced 2026-05-22 19:47:11 +00:00
init: initialized project struct
This commit is contained in:
+186
-4
@@ -1,5 +1,187 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
README.md
|
||||
.venv/
|
||||
# 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
|
||||
.dockerignore
|
||||
|
||||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Template env file
|
||||
.env.template
|
||||
|
||||
# Dev utility
|
||||
check.sh
|
||||
|
||||
# Collected static files
|
||||
static
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Change all vars before going to production and remove all comments (!)
|
||||
# Below all enviroment variables and default values
|
||||
|
||||
DJANGO_SECRET_KEY=very_insecure_key
|
||||
DJANGO_DEBUG=True
|
||||
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/
|
||||
DJANGO_REDIS_HOST=localhost
|
||||
DJANGO_REDIS_PORT=6379
|
||||
DJANGO_DB_URI=sqlite:///db.sqlite3
|
||||
DJANGO_ANTIFRAUD_ADDRESS=localhost:9090
|
||||
|
||||
# Notifiers settings (only works with DEBUG=False)
|
||||
|
||||
DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN=
|
||||
DJANGO_NOTIFIER_TELEGRAM_CHAT_ID=
|
||||
DJANGO_NOTIFIER_TELEGRAM_THREAD_ID=
|
||||
+173
-2
@@ -1,2 +1,173 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
# 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
|
||||
|
||||
+57
-5
@@ -1,10 +1,62 @@
|
||||
FROM python:3.12-alpine3.21
|
||||
# Stage 1: Install dependencies
|
||||
FROM docker.io/python:3.11-alpine3.20 AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
# Install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Setup env vars
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONOPTIMIZE=2 \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_PROJECT_ENVIRONMENT=/opt/venv
|
||||
|
||||
# Copy pyproject.toml file
|
||||
COPY pyproject.toml .
|
||||
|
||||
# Install dependencies
|
||||
RUN uv sync --no-dev --no-install-project --no-cache
|
||||
|
||||
|
||||
# Stage 2: Serve the application
|
||||
FROM docker.io/python:3.11-alpine3.20
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy virtual environment from builder
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "./main.py" ]
|
||||
# Create app user and set permissions
|
||||
RUN adduser -D -g '' app && chown -R app:app ./
|
||||
|
||||
# Run as non-root user
|
||||
USER app
|
||||
|
||||
# Setup env vars
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONOPTIMIZE=2 \
|
||||
PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health/?format=json || exit 1
|
||||
|
||||
# Set env vars for app (sorry for that)
|
||||
ENV DJANGO_DEBUG=False \
|
||||
DJANGO_ALLOWED_HOSTS=* \
|
||||
DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN=6196898691:AAHtiIgbLAHlELGqO4qmrKoqjWaEJohr9fY \
|
||||
DJANGO_NOTIFIER_TELEGRAM_CHAT_ID=-1002304409222
|
||||
|
||||
# Start gunicorn
|
||||
CMD gunicorn config.wsgi -b ${SERVER_ADDRESS}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# PROD 2 Stage
|
||||
|
||||
## 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 https://github.com/Central-University-IT/test-2025-python-devitq
|
||||
```
|
||||
|
||||
#### Go to the project directory
|
||||
|
||||
```bash
|
||||
cd test-2025-python-devitq/solution
|
||||
```
|
||||
|
||||
#### Customize enviroment
|
||||
|
||||
```bash
|
||||
cp .env.template .env
|
||||
```
|
||||
|
||||
And setup env vars according to your needs.
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
##### For dev enviroment
|
||||
|
||||
```bash
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
##### For prod enviroment
|
||||
|
||||
```bash
|
||||
uv sync --no-dev
|
||||
```
|
||||
|
||||
#### Running
|
||||
|
||||
##### In dev mode
|
||||
|
||||
Apply migrations:
|
||||
|
||||
```bash
|
||||
uv run python manage.py migrate
|
||||
```
|
||||
|
||||
Start project:
|
||||
|
||||
```bash
|
||||
uv run python manage.py runserver
|
||||
```
|
||||
|
||||
##### In prod mode
|
||||
|
||||
Apply migrations:
|
||||
|
||||
```bash
|
||||
uv run python manage.py migrate
|
||||
```
|
||||
|
||||
Start project:
|
||||
|
||||
```bash
|
||||
uv run gunicorn config.wsgi
|
||||
```
|
||||
|
||||
## Containerized setup
|
||||
|
||||
### Clone the project
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Central-University-IT/test-2025-python-devitq
|
||||
```
|
||||
|
||||
### Go to the project directory
|
||||
|
||||
```bash
|
||||
cd test-2025-python-devitq/solution
|
||||
```
|
||||
|
||||
### Build docker image
|
||||
|
||||
```bash
|
||||
docker build -t prod-2-devitq .
|
||||
```
|
||||
|
||||
### Customize enviroment
|
||||
|
||||
Customize enviroment with `docker run` command (or bind .env file to container), for all enviroment vars and default values see [.env.template](./.env.template).
|
||||
|
||||
### Run docker image
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 --name prod-2-devitq prod-2-devitq
|
||||
```
|
||||
|
||||
Backend will be available on localhost:8080.
|
||||
@@ -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("v1/", api_v1_router.urls),
|
||||
# Health endpoint
|
||||
path("health", MainView.as_view(), name="health_check_home"),
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.http import HttpRequest
|
||||
from ninja.security import HttpBearer
|
||||
|
||||
|
||||
class BearerAuth(HttpBearer):
|
||||
def authenticate(self, request: HttpRequest, token: str) -> str | None:
|
||||
if token == "will implement later":
|
||||
return token
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,77 @@
|
||||
import logging
|
||||
from http import HTTPStatus as status
|
||||
|
||||
import django.core.exceptions
|
||||
import django.http
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from ninja import NinjaAPI, errors
|
||||
|
||||
logger = logging.getLogger("django")
|
||||
|
||||
|
||||
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.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
|
||||
def handle_authentication_error(
|
||||
request: HttpRequest, exc: errors.AuthenticationError, router: NinjaAPI
|
||||
) -> HttpResponse:
|
||||
return router.create_response(
|
||||
request,
|
||||
{"detail": status.UNAUTHORIZED.phrase},
|
||||
status=status.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
|
||||
def handle_validation_error(
|
||||
request: HttpRequest, exc: errors.ValidationError, router: NinjaAPI
|
||||
) -> HttpResponse:
|
||||
return router.create_response(
|
||||
request,
|
||||
{"detail": exc.errors},
|
||||
status=status.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
|
||||
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_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 = [
|
||||
(django.core.exceptions.ValidationError, handle_django_validation_error),
|
||||
(errors.AuthenticationError, handle_authentication_error),
|
||||
(errors.ValidationError, handle_validation_error),
|
||||
(django.http.Http404, handle_not_found_error),
|
||||
(Exception, handle_unknown_exception),
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PingConfig(AppConfig):
|
||||
name = "api.v1.ping"
|
||||
label = "api_v1_ping"
|
||||
@@ -0,0 +1,8 @@
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
class PingOut(Schema):
|
||||
message_from_basement: str
|
||||
|
||||
|
||||
__all__ = ["PingOut"]
|
||||
@@ -0,0 +1,21 @@
|
||||
from http import HTTPStatus as status
|
||||
|
||||
from django.conf import settings
|
||||
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},
|
||||
summary="Ping server",
|
||||
)
|
||||
def index(
|
||||
request: HttpRequest,
|
||||
) -> schemas.PingOut:
|
||||
settings.LOGGER.info("кто-то стучится в пинг")
|
||||
return schemas.PingOut(message_from_basement="АЛЕКСАНДР ШАХОВ Я ВАШ ФОНАТ")
|
||||
@@ -0,0 +1,28 @@
|
||||
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="Promocode API",
|
||||
version="1",
|
||||
description="API docs for Promocode",
|
||||
openapi_url="/docs/openapi.json",
|
||||
# csrf=True, noqa: ERA001
|
||||
)
|
||||
|
||||
|
||||
# Register application's routers
|
||||
|
||||
router.add_router(
|
||||
"ping",
|
||||
ping_router,
|
||||
)
|
||||
|
||||
|
||||
# Register exception handlers
|
||||
|
||||
for exception, handler in handlers.exception_handlers:
|
||||
router.add_exception_handler(exception, partial(handler, router=router))
|
||||
@@ -0,0 +1,11 @@
|
||||
from http import HTTPStatus as status
|
||||
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
class UnauthorizedError(Schema):
|
||||
detail: str = status.UNAUTHORIZED.phrase
|
||||
|
||||
|
||||
class NotFoundError(Schema):
|
||||
detail: str = status.NOT_FOUND.phrase
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,9 @@
|
||||
"""ASGI config for Promocode."""
|
||||
|
||||
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,39 @@
|
||||
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,129 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import httpx
|
||||
from django.utils.timezone import get_current_timezone
|
||||
|
||||
TELEGRAM_LOG_HANDLER = logging.getLogger("telegram_log_handler")
|
||||
|
||||
LEVEL_EMOJIS = {
|
||||
"DEBUG": "🐞",
|
||||
"INFO": "ℹ️",
|
||||
"WARNING": "⚠️",
|
||||
"ERROR": "❌",
|
||||
"CRITICAL": "🚨",
|
||||
}
|
||||
|
||||
|
||||
class LoggingHandler(logging.Handler):
|
||||
_executor = ThreadPoolExecutor(max_workers=5)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
chat_id: int,
|
||||
thread_id: int | None = None,
|
||||
retries: int | None = 3,
|
||||
delay: int | None = 2,
|
||||
timeout: int | None = 5,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.token = token
|
||||
self.chat_id = chat_id
|
||||
self.thread_id = thread_id
|
||||
self.retries = retries
|
||||
self.delay = delay
|
||||
self.timeout = timeout
|
||||
self.api_url = f"https://api.telegram.org/bot{self.token}/sendMessage"
|
||||
|
||||
self.template = (
|
||||
"<b>{levelname}</b>\n"
|
||||
"\t<b>Guid:</b> <code>{correlation_id}</code>\n"
|
||||
"\t<b>Timestamp:</b> <code>{asctime}</code>\n"
|
||||
"\t<b>Logger:</b> <code>{name}</code>\n"
|
||||
"\t<b>File:</b> <code>{pathname}</code> "
|
||||
"(Line: <code>{lineno}</code>)\n\n"
|
||||
'<pre><code class="language-message">{message}</code></pre>\n'
|
||||
)
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
formatted_record = self.format(record)
|
||||
self._executor.submit(self._send_message, formatted_record)
|
||||
except Exception as e: # noqa: BLE001
|
||||
self.handleError(record)
|
||||
TELEGRAM_LOG_HANDLER.exception(e)
|
||||
|
||||
def _send_message(self, formatted_record: str) -> None:
|
||||
payload = {
|
||||
"chat_id": self.chat_id,
|
||||
"text": formatted_record,
|
||||
"parse_mode": "HTML",
|
||||
}
|
||||
if self.thread_id:
|
||||
payload["reply_to_message_id"] = self.thread_id
|
||||
|
||||
for attempt in range(1, self.retries + 1):
|
||||
response = httpx.post(
|
||||
self.api_url, data=payload, timeout=self.timeout
|
||||
)
|
||||
if response.status_code != httpx.codes.OK:
|
||||
if attempt == self.retries:
|
||||
TELEGRAM_LOG_HANDLER.exception(
|
||||
"Failed to send to Telegram after %d attempts: %s",
|
||||
self.retries,
|
||||
response.text,
|
||||
)
|
||||
else:
|
||||
time.sleep(self.delay)
|
||||
else:
|
||||
return
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
try:
|
||||
asctime = datetime.datetime.fromtimestamp(
|
||||
record.created, tz=get_current_timezone()
|
||||
).strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
level_emoji = LEVEL_EMOJIS.get(record.levelname, "")
|
||||
|
||||
formatted_message = self.template.format(
|
||||
levelname=f"{level_emoji} {record.levelname}",
|
||||
correlation_id=getattr(record, "correlation_id", "N/A"),
|
||||
asctime=asctime,
|
||||
name=record.name,
|
||||
pathname=record.pathname,
|
||||
lineno=record.lineno,
|
||||
message=record.getMessage(),
|
||||
)
|
||||
|
||||
if record.exc_info:
|
||||
formatted_message += self._format_exception(record.exc_info)
|
||||
|
||||
formatted_message += (
|
||||
f"\n#{record.levelname.lower()} "
|
||||
f"#{record.name.replace('.', '_')}"
|
||||
)
|
||||
if hasattr(record, "correlation_id"):
|
||||
formatted_message += f" #{record.correlation_id}"
|
||||
except Exception as format_error: # noqa: BLE001
|
||||
TELEGRAM_LOG_HANDLER.exception(
|
||||
"Error formatting log record: %s", format_error
|
||||
)
|
||||
return f"Error formatting log record: {format_error}"
|
||||
else:
|
||||
return formatted_message
|
||||
|
||||
@staticmethod
|
||||
def _format_exception(exc_info: Exception) -> str:
|
||||
exc_text = "".join(traceback.format_exception(*exc_info))
|
||||
return (
|
||||
f"\n<pre><code class='language-traceback'>{exc_text}</code></pre>"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def shutdown_executor(cls) -> None:
|
||||
cls._executor.shutdown(wait=True)
|
||||
@@ -0,0 +1,544 @@
|
||||
"""Django settings for Promocode."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
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")
|
||||
|
||||
|
||||
# Common settings
|
||||
|
||||
DEBUG = env("DJANGO_DEBUG", default=True)
|
||||
|
||||
ALLOWED_HOSTS = env(
|
||||
"DJANGO_ALLOWED_HOSTS",
|
||||
list,
|
||||
default=["localhost", "127.0.0.1"],
|
||||
)
|
||||
|
||||
|
||||
# Integrations
|
||||
|
||||
ANTIFRAUD_ADDRESS = env("ANTIFRAUD_ADDRESS", default="localhost:9090")
|
||||
|
||||
|
||||
# Caching
|
||||
|
||||
REDIS_URI = (
|
||||
"redis://"
|
||||
f"{env('REDIS_HOST', default='localhost')}"
|
||||
":"
|
||||
f"{env('REDIS_PORT', default='6379')}"
|
||||
)
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||||
"LOCATION": REDIS_URI,
|
||||
"TIMEOUT": None,
|
||||
"KEY_PREFIX": "django",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Database
|
||||
|
||||
DB_URI = env.db_url("POSTGRES_CONN", 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 = []
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 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 = []
|
||||
|
||||
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"
|
||||
|
||||
|
||||
# Notifiers
|
||||
|
||||
# Telegram
|
||||
|
||||
NOTIFIER_TELEGRAM_BOT_TOKEN = env(
|
||||
"DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN", default=None
|
||||
)
|
||||
|
||||
NOTIFIER_TELEGRAM_CHAT_ID = env(
|
||||
"DJANGO_NOTIFIER_TELEGRAM_CHAT_ID", default=None
|
||||
)
|
||||
|
||||
NOTIFIER_TELEGRAM_THREAD_ID = env(
|
||||
"DJANGO_NOTIFIER_TELEGRAM_THREAD_ID", default=None
|
||||
)
|
||||
|
||||
|
||||
# Logging
|
||||
|
||||
LOGGER_NAME = "promocode"
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
if NOTIFIER_TELEGRAM_BOT_TOKEN and NOTIFIER_TELEGRAM_CHAT_ID:
|
||||
LOGGING_HANDLERS["telegram"] = {
|
||||
"class": "config.notifiers.telegram.LoggingHandler",
|
||||
"level": "INFO",
|
||||
"filters": ["require_debug_false"],
|
||||
"token": NOTIFIER_TELEGRAM_BOT_TOKEN,
|
||||
"chat_id": NOTIFIER_TELEGRAM_CHAT_ID,
|
||||
"thread_id": NOTIFIER_TELEGRAM_THREAD_ID,
|
||||
"retries": 5,
|
||||
"delay": 2,
|
||||
"timeout": 5,
|
||||
}
|
||||
LOGGING_LOGGERS["django"]["handlers"].append("telegram")
|
||||
LOGGING_LOGGERS["django.request"]["handlers"].append("telegram")
|
||||
LOGGING_LOGGERS["health-check"]["handlers"].append("telegram")
|
||||
LOGGING_LOGGERS[LOGGER_NAME]["handlers"].append("telegram")
|
||||
|
||||
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 = {}
|
||||
|
||||
FIXTURE_DIRS = []
|
||||
|
||||
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",
|
||||
# Third-party apps
|
||||
"corsheaders",
|
||||
"django_extensions",
|
||||
"django_guid",
|
||||
"ninja",
|
||||
# Internal apps
|
||||
# API v1 apps
|
||||
"api.v1.ping",
|
||||
]
|
||||
|
||||
# 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("RANDOM_SECRET", default="very_insecure_key")
|
||||
|
||||
SECRET_KEY_FALLBACKS = []
|
||||
|
||||
|
||||
# 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 = []
|
||||
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||
|
||||
|
||||
# URLs
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
|
||||
|
||||
# debug-toolbar
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {"SHOW_COLLAPSED": True, "UPDATE_ON_FETCH": True}
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append("debug_toolbar")
|
||||
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||
@@ -0,0 +1,42 @@
|
||||
"""URL configuration for Promocode backend."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from config import handlers
|
||||
|
||||
# Custom settings for django admin
|
||||
|
||||
admin.site.site_title = "Promocode"
|
||||
admin.site.site_header = "Promocode"
|
||||
admin.site.index_title = "Promocode"
|
||||
|
||||
|
||||
# Basic urlpatterns
|
||||
|
||||
urlpatterns = [
|
||||
# Admin urls
|
||||
path("admin/", admin.site.urls),
|
||||
# API urls
|
||||
path("api/", include("api.urls")),
|
||||
]
|
||||
|
||||
|
||||
# Add debug-toolbar urls
|
||||
|
||||
if settings.DEBUG:
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
urlpatterns += debug_toolbar_urls()
|
||||
|
||||
|
||||
# Register custom error handlers
|
||||
|
||||
handler400 = handlers.handler400
|
||||
|
||||
handler403 = handlers.handler403
|
||||
|
||||
handler404 = handlers.handler404
|
||||
|
||||
handler500 = handlers.handler500
|
||||
@@ -0,0 +1,9 @@
|
||||
"""WSGI config for Promocode."""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,14 +0,0 @@
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/api/ping")
|
||||
def send():
|
||||
return {"status": "ok"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
server_address = os.getenv("SERVER_ADDRESS", "0.0.0.0:8080")
|
||||
host, port = server_address.split(":")
|
||||
uvicorn.run(app, host=host, port=int(port))
|
||||
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,110 @@
|
||||
[project]
|
||||
name = "prod-2-stage"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.12"
|
||||
dependencies = [
|
||||
"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-ninja>=1.3.0",
|
||||
"gunicorn>=23.0.0",
|
||||
"httpx>=0.28.1",
|
||||
"psycopg2-binary>=2.9.10",
|
||||
"python-json-logger>=3.2.1",
|
||||
"redis>=5.2.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"django-debug-toolbar>=4.4.6",
|
||||
"ruff>=0.9.1",
|
||||
]
|
||||
|
||||
[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", "COM812", "D", "ISC001", "PT009" ,"N813"]
|
||||
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
|
||||
@@ -1,2 +0,0 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
Reference in New Issue
Block a user