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
362 lines
13 KiB
Python
362 lines
13 KiB
Python
"""Сериализаторы приложения обмена данными."""
|
||
|
||
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,
|
||
}
|