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

@@ -2,13 +2,17 @@
from __future__ import annotations
import json
from contextlib import suppress
from typing import Any
from apps.exchange.models import ExchangeConnection
from django.apps import apps as django_apps
from django.db import connections, transaction
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, connections, transaction
from django.utils import timezone
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask
class ExchangeServiceError(ValueError):
@@ -481,3 +485,163 @@ class ExchangeConnectionService:
connection.save(
update_fields=["last_checked_at", "last_error", "updated_at"]
)
class ExchangePeriodicTaskService:
"""Сервис управления периодическими задачами обмена."""
TASK_NAME = "apps.exchange.tasks.dispatch_periodic_exchange_copy"
@classmethod
def get_queryset(cls):
return (
PeriodicTask.objects.filter(task=cls.TASK_NAME)
.select_related("interval", "crontab")
.order_by("name")
)
@classmethod
@transaction.atomic
def create_periodic_task(
cls,
*,
name: str,
payload: dict[str, Any],
schedule: dict[str, Any],
description: str = "",
enabled: bool = True,
) -> PeriodicTask:
task = PeriodicTask(
name=name,
task=cls.TASK_NAME,
kwargs=json.dumps({"payload": payload}, ensure_ascii=False),
description=description,
enabled=enabled,
)
cls._assign_schedule(task=task, schedule=schedule)
return cls._save_task(task)
@classmethod
@transaction.atomic
def update_periodic_task(
cls,
*,
task: PeriodicTask,
payload: dict[str, Any],
schedule: dict[str, Any],
name: str | None = None,
description: str | None = None,
enabled: bool | None = None,
) -> PeriodicTask:
old_interval_id = task.interval_id
old_crontab_id = task.crontab_id
if name is not None:
task.name = name
if description is not None:
task.description = description
if enabled is not None:
task.enabled = enabled
task.kwargs = json.dumps({"payload": payload}, ensure_ascii=False)
cls._assign_schedule(task=task, schedule=schedule)
task = cls._save_task(task)
cls._cleanup_unused_interval(old_interval_id)
cls._cleanup_unused_crontab(old_crontab_id)
return task
@classmethod
def _assign_schedule(cls, *, task: PeriodicTask, schedule: dict[str, Any]) -> None:
if schedule["type"] == "interval":
task.interval = cls._get_or_create_interval(schedule)
task.crontab = None
return
task.crontab = cls._get_or_create_crontab(schedule)
task.interval = None
@classmethod
def _get_or_create_interval(cls, schedule: dict[str, Any]) -> IntervalSchedule:
interval = IntervalSchedule(
every=schedule["every"],
period=schedule["period"],
)
cls._validate_model(interval)
interval, _ = IntervalSchedule.objects.get_or_create(
every=interval.every,
period=interval.period,
)
return interval
@classmethod
def _get_or_create_crontab(cls, schedule: dict[str, Any]) -> CrontabSchedule:
crontab = CrontabSchedule(
minute=schedule["minute"],
hour=schedule["hour"],
day_of_week=schedule["day_of_week"],
day_of_month=schedule["day_of_month"],
month_of_year=schedule["month_of_year"],
timezone=settings.TIME_ZONE,
)
cls._validate_model(crontab)
crontab, _ = CrontabSchedule.objects.get_or_create(
minute=crontab.minute,
hour=crontab.hour,
day_of_week=crontab.day_of_week,
day_of_month=crontab.day_of_month,
month_of_year=crontab.month_of_year,
timezone=crontab.timezone,
)
return crontab
@classmethod
def _save_task(cls, task: PeriodicTask) -> PeriodicTask:
try:
task.full_clean()
task.save()
except DjangoValidationError as exc:
raise ExchangeServiceError(cls._format_validation_error(exc)) from exc
except IntegrityError as exc:
raise ExchangeServiceError(
"Периодическая задача с таким именем уже существует"
) from exc
return task
@classmethod
def _validate_model(cls, instance) -> None:
try:
instance.full_clean()
except DjangoValidationError as exc:
raise ExchangeServiceError(cls._format_validation_error(exc)) from exc
@classmethod
def _format_validation_error(cls, exc: DjangoValidationError) -> str:
if hasattr(exc, "message_dict"):
messages = []
for field, field_errors in exc.message_dict.items():
messages.extend(f"{field}: {error}" for error in field_errors)
return "; ".join(messages)
return "; ".join(exc.messages)
@classmethod
def _cleanup_unused_interval(cls, interval_id: int | None) -> None:
if not interval_id:
return
if PeriodicTask.objects.filter(interval_id=interval_id).exists():
return
IntervalSchedule.objects.filter(id=interval_id).delete()
@classmethod
def _cleanup_unused_crontab(cls, crontab_id: int | None) -> None:
if not crontab_id:
return
if PeriodicTask.objects.filter(crontab_id=crontab_id).exists():
return
CrontabSchedule.objects.filter(id=crontab_id).delete()