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
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user