feat(registry): add new endpoints for registers, exchange, and backups; update routing and configurations
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 3m10s
CI/CD Pipeline / Run Tests (push) Successful in 3m35s
CI/CD Pipeline / Telegram Notify Success (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m26s
CI/CD Pipeline / Run Tests (pull_request) Successful in 2m46s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped

This commit is contained in:
2026-03-04 15:36:57 +01:00
parent 052389d921
commit a91ed1f1ae
90 changed files with 5488 additions and 622 deletions

157
src/apps/exchange/views.py Normal file
View File

@@ -0,0 +1,157 @@
"""API views для обмена данными с внешней БД."""
from contextlib import suppress
from apps.core.openapi import CommonResponses, ErrorResponses, swagger_tag
from apps.core.response import api_created_response, api_response
from apps.core.services import BackgroundJobService
from apps.exchange.models import ExchangeConnection
from apps.exchange.serializers import (
ExchangeConnectionCreateSerializer,
ExchangeConnectionSerializer,
ExchangeCopyRequestSerializer,
)
from apps.exchange.services import ExchangeConnectionService, ExchangeServiceError
from apps.exchange.tasks import copy_parsers_data_async
from django.db import IntegrityError
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView
EXCHANGE_TAG = swagger_tag("Обмен данными", "exchange")
class ExchangeConnectionListCreateView(APIView):
"""API списка и создания подключений обмена."""
permission_classes = [IsAdminUser]
@swagger_auto_schema(
tags=[EXCHANGE_TAG],
operation_summary="Список подключений",
operation_description=(
"Возвращает список всех сохранённых подключений для обмена.\n"
"Пароль в ответ не возвращается."
),
responses={
200: ExchangeConnectionSerializer(many=True),
**ErrorResponses.ADMIN,
},
)
def get(self, request):
queryset = ExchangeConnection.objects.all().order_by("-is_active", "-created_at")
serializer = ExchangeConnectionSerializer(queryset, many=True)
return api_response(serializer.data, status_code=status.HTTP_200_OK)
@swagger_auto_schema(
tags=[EXCHANGE_TAG],
operation_summary="Создать активное подключение",
operation_description=(
"Создаёт новое подключение к целевой БД как активное.\n"
"Перед созданием деактивирует текущее активное подключение.\n"
"После сохранения проверяет соединение и валидирует структуру целевой БД."
),
request_body=ExchangeConnectionCreateSerializer,
responses={
201: ExchangeConnectionSerializer,
400: CommonResponses.BAD_REQUEST,
**ErrorResponses.ADMIN,
},
)
def post(self, request):
serializer = ExchangeConnectionCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
connection = ExchangeConnectionService.create_active_connection_and_prepare(
**serializer.validated_data
)
except ExchangeServiceError as exc:
raise ValidationError({"connection": str(exc)}) from exc
output = ExchangeConnectionSerializer(connection)
return api_created_response(output.data)
class ExchangeCopyDataView(APIView):
"""API запуска копирования данных в целевую БД."""
permission_classes = [IsAdminUser]
@swagger_auto_schema(
tags=[EXCHANGE_TAG],
operation_summary="Копировать данные parsers в target DB",
operation_description=(
"Асинхронно запускает копирование данных из локальной БД "
"в активную целевую БД.\n"
"Перед копированием выполняется только проверка структуры "
"(без изменения схемы/миграций).\n"
"Поддерживает режимы: all / single / selected."
),
request_body=ExchangeCopyRequestSerializer,
responses={
202: openapi.Response(
description="Копирование поставлено в очередь",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"status": openapi.Schema(type=openapi.TYPE_STRING),
"message": openapi.Schema(type=openapi.TYPE_STRING),
"task_id": openapi.Schema(type=openapi.TYPE_STRING),
"connection_id": openapi.Schema(type=openapi.TYPE_INTEGER),
"mode": openapi.Schema(type=openapi.TYPE_STRING),
"truncate_before_copy": openapi.Schema(
type=openapi.TYPE_BOOLEAN
),
},
),
),
400: CommonResponses.BAD_REQUEST,
**ErrorResponses.ADMIN,
},
)
def post(self, request):
serializer = ExchangeCopyRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
active_connection = ExchangeConnectionService.get_active_connection()
task = copy_parsers_data_async.delay(
connection_id=active_connection.id,
payload=serializer.validated_data,
requested_by_id=request.user.id if request.user.is_authenticated else None,
)
# Предсоздаём запись для мгновенного отслеживания в /api/v1/jobs/{task_id}/
with suppress(IntegrityError):
BackgroundJobService.create_job(
task_id=task.id,
task_name="apps.exchange.tasks.copy_parsers_data_async",
user_id=request.user.id if request.user.is_authenticated else None,
meta={
"connection_id": active_connection.id,
"mode": serializer.validated_data["mode"],
"table": serializer.validated_data.get("table"),
"tables": serializer.validated_data.get("tables"),
"truncate_before_copy": serializer.validated_data.get(
"truncate_before_copy"
),
},
)
except ExchangeServiceError as exc:
raise ValidationError({"copy": str(exc)}) from exc
return api_response(
{
"status": "started",
"message": "Копирование запущено в фоне.",
"task_id": task.id,
"connection_id": active_connection.id,
"mode": serializer.validated_data["mode"],
"truncate_before_copy": serializer.validated_data["truncate_before_copy"],
},
status_code=status.HTTP_202_ACCEPTED,
)