Add periodic exchange task management API
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

This commit is contained in:
2026-03-19 17:03:47 +01:00
parent 941c268d32
commit 3de66cc25c
8 changed files with 867 additions and 24 deletions

View File

@@ -1,9 +1,53 @@
"""Сериализаторы приложения обмена данными."""
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):
"""Сериализатор подключения без выдачи пароля в ответах."""
@@ -61,28 +105,257 @@ class ExchangeCopyRequestSerializer(serializers.Serializer):
truncate_before_copy = serializers.BooleanField(default=True)
def validate(self, attrs):
mode = attrs["mode"]
table = attrs.get("table")
tables = attrs.get("tables")
return validate_exchange_copy_payload(attrs)
if mode == "single" and not table:
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(
{"table": "Для mode=single нужно указать table"}
{"schedule_type": "Нужно указать тип расписания."}
)
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"}
)
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,
}