feat(registries): add SME and budget imports and fix source API workflows
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 9m37s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 4m18s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m48s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-13 23:13:47 +02:00
parent c863563851
commit 49cbfd265c
50 changed files with 5021 additions and 452 deletions

View File

@@ -12,6 +12,7 @@ from typing import Any
from apps.core.mixins import TimestampMixin
from django.db import models
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -141,81 +142,69 @@ class BackgroundJob(TimestampMixin, models.Model):
# ==================== Методы обновления статуса ====================
def _unfinished_queryset(self):
"""Условие проверяется в UPDATE, включая события от старого экземпляра."""
return type(self).objects.filter(
pk=self.pk,
status__in=[JobStatus.PENDING, JobStatus.STARTED, JobStatus.RETRY],
)
def _update_unfinished(self, **values: Any) -> None:
self._unfinished_queryset().update(updated_at=timezone.now(), **values)
self.refresh_from_db()
def mark_started(self) -> None:
"""Отметить задачу как начатую."""
self.status = JobStatus.STARTED
self.started_at = timezone.now()
self.save(update_fields=["status", "started_at", "updated_at"])
"""Начать задачу, сохранив время первого запуска после retry."""
self._update_unfinished(
status=JobStatus.STARTED,
started_at=Coalesce("started_at", timezone.now()),
)
def update_progress(self, progress: int, message: str = "") -> None:
"""
Обновить прогресс выполнения.
Args:
progress: Процент выполнения (0-100)
message: Описание текущего этапа
"""
self.progress = min(max(progress, 0), 100)
self.progress_message = message
self.save(update_fields=["progress", "progress_message", "updated_at"])
def update_progress(
self, progress: int, message: str = "", *, meta: dict | None = None
) -> None:
"""Атомарно сохранить только неубывающий прогресс незавершённой задачи."""
progress = min(max(int(progress), 0), 100)
values = {
"progress": progress,
"progress_message": message,
"updated_at": timezone.now(),
}
if meta is not None:
values["meta"] = meta
self._unfinished_queryset().filter(progress__lte=progress).update(**values)
self.refresh_from_db()
def complete(self, result: Any = None) -> None:
"""
Отметить задачу как успешно завершённую.
Args:
result: Результат выполнения (сериализуемый в JSON)
"""
self.status = JobStatus.SUCCESS
self.progress = 100
self.result = result
self.error = ""
self.traceback = ""
self.completed_at = timezone.now()
self.save(
update_fields=[
"status",
"progress",
"result",
"error",
"traceback",
"completed_at",
"updated_at",
]
"""Первое финальное событие фиксирует успешный результат и 100%."""
self._update_unfinished(
status=JobStatus.SUCCESS,
progress=100,
result=result,
error="",
traceback="",
completed_at=timezone.now(),
)
def fail(self, error: str, traceback_str: str = "") -> None:
"""
Отметить задачу как завершённую с ошибкой.
Args:
error: Текст ошибки
traceback_str: Полный traceback
"""
self.status = JobStatus.FAILURE
self.error = str(error)
self.traceback = traceback_str
self.completed_at = timezone.now()
self.save(
update_fields=[
"status",
"error",
"traceback",
"completed_at",
"updated_at",
]
"""Зафиксировать ошибку без изменения достигнутого прогресса."""
self._update_unfinished(
status=JobStatus.FAILURE,
error=str(error),
traceback=traceback_str,
completed_at=timezone.now(),
)
def revoke(self) -> None:
"""Отметить задачу как отменённую."""
self.status = JobStatus.REVOKED
self.completed_at = timezone.now()
self.save(update_fields=["status", "completed_at", "updated_at"])
def revoke(self, message: str | None = None) -> None:
"""Отменить незавершённую задачу, сохранив её прогресс."""
values = {"status": JobStatus.REVOKED, "completed_at": timezone.now()}
if message is not None:
values["progress_message"] = message
self._update_unfinished(**values)
def mark_retry(self) -> None:
"""Отметить, что задача будет повторена."""
self.status = JobStatus.RETRY
self.save(update_fields=["status", "updated_at"])
"""Отметить повторную попытку, не открывая завершённую задачу заново."""
self._update_unfinished(status=JobStatus.RETRY)
# ==================== Свойства ====================

View File

@@ -395,7 +395,6 @@ class BackgroundJobControlView(BackgroundJobStatusView):
},
)
def post(self, request: Request, task_id: str) -> Response:
from apps.core.models import JobStatus
from apps.core.services import BackgroundJobService
from celery import current_app
@@ -415,10 +414,7 @@ class BackgroundJobControlView(BackgroundJobStatusView):
task_id,
terminate=bool(request.data.get("terminate", False)),
)
if not job.is_finished:
job.status = JobStatus.REVOKED
job.progress_message = "Задача отозвана пользователем"
job.save(update_fields=["status", "progress_message", "updated_at"])
job.revoke(message="Задача отозвана пользователем")
return Response(BackgroundJobSerializer(job).data)