Files
mostovik-backend/src/apps/exchange/serializers.py
Aleksandr Meshchriakov 3de66cc25c
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m16s
CI/CD Pipeline / Run Tests (push) Successful in 3m26s
CI/CD Pipeline / Telegram Notify Success (push) Failing after 1m29s
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 20m19s
CI/CD Pipeline / Telegram Notify Success (pull_request) Failing after 1m34s
Add periodic exchange task management API
2026-03-19 17:03:47 +01:00

362 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Сериализаторы приложения обмена данными."""
import json
from typing import Any
from apps.exchange.models import ExchangeConnection
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from rest_framework import serializers
def validate_exchange_copy_payload(attrs: dict[str, Any]) -> dict[str, Any]:
"""Проверить совместимость параметров запуска копирования."""
mode = attrs["mode"]
table = attrs.get("table")
tables = attrs.get("tables")
if mode == "single" and not table:
raise serializers.ValidationError(
{"table": "Для mode=single нужно указать table"}
)
if mode == "selected" and not tables:
raise serializers.ValidationError(
{"tables": "Для mode=selected нужно указать tables"}
)
if mode != "single" and table:
raise serializers.ValidationError(
{"table": "Поле table допустимо только для mode=single"}
)
if mode != "selected" and tables:
raise serializers.ValidationError(
{"tables": "Поле tables допустимо только для mode=selected"}
)
return attrs
def get_periodic_task_payload(task: PeriodicTask) -> dict[str, Any]:
"""Извлечь payload exchange-задачи из kwargs django_celery_beat."""
try:
kwargs = json.loads(task.kwargs or "{}")
except json.JSONDecodeError:
return {}
payload = kwargs.get("payload")
return payload if isinstance(payload, dict) else {}
class ExchangeConnectionSerializer(serializers.ModelSerializer):
"""Сериализатор подключения без выдачи пароля в ответах."""
class Meta:
model = ExchangeConnection
fields = [
"id",
"server",
"port",
"username",
"database_name",
"schema_name",
"is_active",
"last_checked_at",
"last_error",
"created_at",
"updated_at",
]
read_only_fields = fields
class ExchangeConnectionCreateSerializer(serializers.Serializer):
"""Входные данные для создания активного подключения."""
server = serializers.CharField(max_length=255)
port = serializers.IntegerField(min_value=1, max_value=65535)
username = serializers.CharField(max_length=255)
password = serializers.CharField()
database_name = serializers.CharField(max_length=255)
schema_name = serializers.RegexField(
regex=r"^[A-Za-z_][A-Za-z0-9_]*$",
max_length=255,
error_messages={
"invalid": (
"Имя схемы должно начинаться с буквы/_, "
"содержать только буквы, цифры и _"
)
},
)
class ExchangeCopyRequestSerializer(serializers.Serializer):
"""Параметры запуска копирования данных."""
mode = serializers.ChoiceField(
choices=["all", "single", "selected"],
default="all",
)
table = serializers.CharField(required=False)
tables = serializers.ListField(
child=serializers.CharField(),
required=False,
allow_empty=False,
)
truncate_before_copy = serializers.BooleanField(default=True)
def validate(self, attrs):
return validate_exchange_copy_payload(attrs)
class ExchangePeriodicTaskSerializer(serializers.ModelSerializer):
"""Сериализатор периодической задачи обмена."""
schedule_type = serializers.SerializerMethodField()
interval_every = serializers.SerializerMethodField()
interval_period = serializers.SerializerMethodField()
crontab_minute = serializers.SerializerMethodField()
crontab_hour = serializers.SerializerMethodField()
crontab_day_of_week = serializers.SerializerMethodField()
crontab_day_of_month = serializers.SerializerMethodField()
crontab_month_of_year = serializers.SerializerMethodField()
crontab_timezone = serializers.SerializerMethodField()
mode = serializers.SerializerMethodField()
table = serializers.SerializerMethodField()
tables = serializers.SerializerMethodField()
truncate_before_copy = serializers.SerializerMethodField()
class Meta:
model = PeriodicTask
fields = [
"id",
"name",
"description",
"enabled",
"schedule_type",
"interval_every",
"interval_period",
"crontab_minute",
"crontab_hour",
"crontab_day_of_week",
"crontab_day_of_month",
"crontab_month_of_year",
"crontab_timezone",
"mode",
"table",
"tables",
"truncate_before_copy",
"last_run_at",
"total_run_count",
"date_changed",
]
read_only_fields = fields
def get_schedule_type(self, obj: PeriodicTask) -> str | None:
if obj.interval_id:
return "interval"
if obj.crontab_id:
return "crontab"
return None
def get_interval_every(self, obj: PeriodicTask) -> int | None:
return obj.interval.every if obj.interval_id else None
def get_interval_period(self, obj: PeriodicTask) -> str | None:
return obj.interval.period if obj.interval_id else None
def get_crontab_minute(self, obj: PeriodicTask) -> str | None:
return obj.crontab.minute if obj.crontab_id else None
def get_crontab_hour(self, obj: PeriodicTask) -> str | None:
return obj.crontab.hour if obj.crontab_id else None
def get_crontab_day_of_week(self, obj: PeriodicTask) -> str | None:
return obj.crontab.day_of_week if obj.crontab_id else None
def get_crontab_day_of_month(self, obj: PeriodicTask) -> str | None:
return obj.crontab.day_of_month if obj.crontab_id else None
def get_crontab_month_of_year(self, obj: PeriodicTask) -> str | None:
return obj.crontab.month_of_year if obj.crontab_id else None
def get_crontab_timezone(self, obj: PeriodicTask) -> str | None:
if not obj.crontab_id:
return None
timezone = obj.crontab.timezone
return str(timezone) if timezone is not None else None
def get_mode(self, obj: PeriodicTask) -> str | None:
return get_periodic_task_payload(obj).get("mode")
def get_table(self, obj: PeriodicTask) -> str | None:
return get_periodic_task_payload(obj).get("table")
def get_tables(self, obj: PeriodicTask) -> list[str] | None:
return get_periodic_task_payload(obj).get("tables")
def get_truncate_before_copy(self, obj: PeriodicTask) -> bool | None:
return get_periodic_task_payload(obj).get("truncate_before_copy")
class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
"""Входные данные для создания и изменения периодической задачи обмена."""
name = serializers.CharField(max_length=200, required=False)
description = serializers.CharField(required=False, allow_blank=True)
enabled = serializers.BooleanField(required=False)
schedule_type = serializers.ChoiceField(
choices=["interval", "crontab"],
required=False,
)
interval_every = serializers.IntegerField(min_value=1, required=False)
interval_period = serializers.ChoiceField(
choices=[choice[0] for choice in IntervalSchedule.PERIOD_CHOICES],
required=False,
)
crontab_minute = serializers.CharField(max_length=64, required=False)
crontab_hour = serializers.CharField(max_length=64, required=False)
crontab_day_of_week = serializers.CharField(max_length=64, required=False)
crontab_day_of_month = serializers.CharField(max_length=64, required=False)
crontab_month_of_year = serializers.CharField(max_length=64, required=False)
mode = serializers.ChoiceField(
choices=["all", "single", "selected"],
required=False,
)
table = serializers.CharField(required=False)
tables = serializers.ListField(
child=serializers.CharField(),
required=False,
allow_empty=False,
)
truncate_before_copy = serializers.BooleanField(required=False)
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
if not self.instance and "name" not in attrs:
raise serializers.ValidationError({"name": "Обязательное поле."})
schedule_type = self._resolve_schedule_type(attrs)
if not schedule_type:
raise serializers.ValidationError(
{"schedule_type": "Нужно указать тип расписания."}
)
payload = self._build_payload(attrs)
schedule = self._build_schedule(attrs, schedule_type)
attrs["payload"] = validate_exchange_copy_payload(payload)
attrs["schedule"] = schedule
return attrs
def _resolve_schedule_type(self, attrs: dict[str, Any]) -> str | None:
if "schedule_type" in attrs:
return attrs["schedule_type"]
if self.instance and self.instance.interval_id:
return "interval"
if self.instance and self.instance.crontab_id:
return "crontab"
return None
def _build_payload(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_payload = (
get_periodic_task_payload(self.instance) if self.instance else {}
)
mode = attrs.get("mode", current_payload.get("mode", "all"))
truncate_before_copy = attrs.get(
"truncate_before_copy",
current_payload.get("truncate_before_copy", True),
)
if "mode" in attrs and attrs["mode"] != "single" and "table" not in attrs:
table = None
else:
table = attrs.get("table", current_payload.get("table"))
if "mode" in attrs and attrs["mode"] != "selected" and "tables" not in attrs:
tables = None
else:
tables = attrs.get("tables", current_payload.get("tables"))
return {
"mode": mode,
"table": table,
"tables": tables,
"truncate_before_copy": truncate_before_copy,
}
def _build_schedule(
self,
attrs: dict[str, Any],
schedule_type: str,
) -> dict[str, Any]:
if schedule_type == "interval":
return self._build_interval_schedule(attrs)
return self._build_crontab_schedule(attrs)
def _build_interval_schedule(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_schedule = self.instance.interval if self.instance else None
interval_every = attrs.get(
"interval_every",
current_schedule.every if current_schedule else None,
)
interval_period = attrs.get(
"interval_period",
current_schedule.period if current_schedule else None,
)
errors = {}
if interval_every is None:
errors["interval_every"] = "Обязательное поле для interval."
if interval_period is None:
errors["interval_period"] = "Обязательное поле для interval."
if errors:
raise serializers.ValidationError(errors)
return {
"type": "interval",
"every": interval_every,
"period": interval_period,
}
def _build_crontab_schedule(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_schedule = self.instance.crontab if self.instance else None
fields = {
"minute": attrs.get(
"crontab_minute",
current_schedule.minute if current_schedule else None,
),
"hour": attrs.get(
"crontab_hour",
current_schedule.hour if current_schedule else None,
),
"day_of_week": attrs.get(
"crontab_day_of_week",
current_schedule.day_of_week if current_schedule else None,
),
"day_of_month": attrs.get(
"crontab_day_of_month",
current_schedule.day_of_month if current_schedule else None,
),
"month_of_year": attrs.get(
"crontab_month_of_year",
current_schedule.month_of_year if current_schedule else None,
),
}
errors = {
f"crontab_{field_name}": "Обязательное поле для crontab."
for field_name, value in fields.items()
if value is None
}
if errors:
raise serializers.ValidationError(errors)
return {
"type": "crontab",
**fields,
}