feat(core): add core module with mixins, services, and background jobs

- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc.
- Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin
- Add Base ViewSets with bulk operations
- Add BackgroundJob model for Celery task tracking
- Add BaseAppCommand for management commands
- Add permissions, pagination, filters, cache, logging
- Migrate tests to factory_boy + faker
- Add CHANGELOG.md
- 297 tests passing
This commit is contained in:
2026-01-21 11:47:26 +01:00
parent 06b30fca02
commit f121445313
72 changed files with 9258 additions and 594 deletions

249
src/apps/core/views.py Normal file
View File

@@ -0,0 +1,249 @@
"""
Health check views for monitoring and orchestration.
Provides endpoints for:
- Basic liveness check (is the app running?)
- Readiness check (is the app ready to serve traffic?)
- Detailed health check (DB, Redis, Celery status)
"""
import logging
import time
from typing import Any
from django.conf import settings
from django.db import connection
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
logger = logging.getLogger(__name__)
class HealthCheckView(APIView):
"""
Comprehensive health check endpoint.
GET /api/health/
Returns detailed status of all dependencies.
Response:
{
"status": "healthy" | "degraded" | "unhealthy",
"version": "1.0.0",
"checks": {
"database": {"status": "up", "latency_ms": 5},
"redis": {"status": "up", "latency_ms": 2},
"celery": {"status": "up"}
}
}
"""
permission_classes = [AllowAny]
authentication_classes = [] # No auth required
def get(self, request: Request) -> Response:
"""Run all health checks and return status."""
checks = {}
overall_status = "healthy"
# Database check
db_check = self._check_database()
checks["database"] = db_check
if db_check["status"] != "up":
overall_status = "unhealthy"
# Redis check
redis_check = self._check_redis()
checks["redis"] = redis_check
if redis_check["status"] != "up" and overall_status == "healthy":
overall_status = "degraded"
# Celery check (optional, may be slow)
if request.query_params.get("include_celery", "").lower() == "true":
celery_check = self._check_celery()
checks["celery"] = celery_check
if celery_check["status"] != "up" and overall_status == "healthy":
overall_status = "degraded"
response_data = {
"status": overall_status,
"version": getattr(settings, "APP_VERSION", "1.0.0"),
"checks": checks,
}
# 503 only for unhealthy (critical services down)
# 200 for healthy and degraded (non-critical services down)
status_code = (
status.HTTP_503_SERVICE_UNAVAILABLE
if overall_status == "unhealthy"
else status.HTTP_200_OK
)
return Response(response_data, status=status_code)
def _check_database(self) -> dict[str, Any]:
"""Check database connectivity."""
start = time.time()
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
latency = (time.time() - start) * 1000
return {"status": "up", "latency_ms": round(latency, 2)}
except Exception as e:
logger.error(f"Database health check failed: {e}")
return {"status": "down", "error": str(e)}
def _check_redis(self) -> dict[str, Any]:
"""Check Redis connectivity."""
start = time.time()
try:
from django_redis import get_redis_connection
redis_conn = get_redis_connection("default")
redis_conn.ping()
latency = (time.time() - start) * 1000
return {"status": "up", "latency_ms": round(latency, 2)}
except ImportError:
return {"status": "skipped", "reason": "django_redis not installed"}
except Exception as e:
logger.warning(f"Redis health check failed: {e}")
return {"status": "down", "error": str(e)}
def _check_celery(self) -> dict[str, Any]:
"""Check Celery worker availability."""
try:
from config.celery import app as celery_app
inspector = celery_app.control.inspect(timeout=2.0)
active = inspector.active()
if active:
worker_count = len(active)
return {"status": "up", "workers": worker_count}
return {"status": "down", "error": "No active workers"}
except Exception as e:
logger.warning(f"Celery health check failed: {e}")
return {"status": "down", "error": str(e)}
class LivenessView(APIView):
"""
Kubernetes liveness probe endpoint.
GET /api/health/live/
Returns 200 if the application is running.
"""
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request: Request) -> Response:
"""Simple liveness check."""
return Response({"status": "alive"}, status=status.HTTP_200_OK)
class ReadinessView(APIView):
"""
Kubernetes readiness probe endpoint.
GET /api/health/ready/
Returns 200 if the application is ready to serve traffic.
"""
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request: Request) -> Response:
"""Check if app is ready to serve traffic."""
# Check database connection
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
except Exception as e:
logger.error(f"Readiness check failed - database: {e}")
return Response(
{"status": "not_ready", "reason": "database unavailable"},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
return Response({"status": "ready"}, status=status.HTTP_200_OK)
class BackgroundJobStatusView(APIView):
"""
Получение статуса фоновой задачи.
GET /api/v1/jobs/{task_id}/
Возвращает статус, прогресс и результат задачи.
Response:
{
"id": "uuid",
"task_id": "celery-task-id",
"status": "pending|started|success|failure|revoked",
"progress": 75,
"progress_message": "Обработка данных...",
"result": {...},
"error": "",
"is_finished": false
}
"""
from rest_framework.permissions import IsAuthenticated
permission_classes = [IsAuthenticated]
def get(self, request: Request, task_id: str) -> Response:
"""Получить статус задачи по task_id."""
from apps.core.serializers import BackgroundJobSerializer
from apps.core.services import BackgroundJobService
job = BackgroundJobService.get_by_task_id(task_id)
# Проверка доступа: только владелец или админ
if job.user_id and job.user_id != request.user.id and not request.user.is_staff:
return Response(
{"detail": "Нет доступа к этой задаче"},
status=status.HTTP_403_FORBIDDEN,
)
serializer = BackgroundJobSerializer(job)
return Response(serializer.data)
class BackgroundJobListView(APIView):
"""
Список фоновых задач пользователя.
GET /api/v1/jobs/
Возвращает список задач текущего пользователя.
Query params:
status: Фильтр по статусу (pending, started, success, failure)
limit: Количество записей (по умолчанию 50)
"""
from rest_framework.permissions import IsAuthenticated
permission_classes = [IsAuthenticated]
def get(self, request: Request) -> Response:
"""Получить список задач пользователя."""
from apps.core.serializers import BackgroundJobListSerializer
from apps.core.services import BackgroundJobService
status_filter = request.query_params.get("status")
limit = min(int(request.query_params.get("limit", 50)), 100)
jobs = BackgroundJobService.get_user_jobs(
user_id=request.user.id,
status=status_filter,
limit=limit,
)
serializer = BackgroundJobListSerializer(jobs, many=True)
return Response(serializer.data)