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