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
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:
157
src/apps/exchange/views.py
Normal file
157
src/apps/exchange/views.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user