feat: update finance exchange and source refresh jobs
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 31s
CI/CD Pipeline / Build and Push Images (push) Successful in 34s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 33s

This commit is contained in:
2026-08-09 12:23:36 +02:00
parent 4da56f3823
commit a198f7e960
19 changed files with 266 additions and 52 deletions

View File

@@ -169,12 +169,16 @@ class BackgroundJob(TimestampMixin, models.Model):
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",
]

View File

@@ -752,21 +752,21 @@ class BackgroundJobService(BaseReadOnlyService):
cls,
*,
max_age_minutes: int,
pending_max_age_minutes: int = 24 * 60,
task_names: set[str] | None = None,
meta_sources: set[str] | None = None,
) -> int:
"""Mark old active jobs as failed after worker restarts or hard kills."""
from apps.core.models import JobStatus
cutoff = timezone.now() - timedelta(minutes=max_age_minutes)
queryset = (
cls.get_queryset()
.filter(
status__in=[JobStatus.PENDING, JobStatus.STARTED, JobStatus.RETRY],
)
.filter(
Q(started_at__isnull=False, started_at__lt=cutoff)
| Q(started_at__isnull=True, created_at__lt=cutoff)
now = timezone.now()
cutoff = now - timedelta(minutes=max_age_minutes)
pending_cutoff = now - timedelta(minutes=pending_max_age_minutes)
queryset = cls.get_queryset().filter(
Q(status=JobStatus.PENDING, created_at__lt=pending_cutoff)
| Q(
status__in=[JobStatus.STARTED, JobStatus.RETRY],
updated_at__lt=cutoff,
)
)
if task_names:

View File

@@ -785,6 +785,10 @@ class StateCorpExchangeService:
if not case_number or decision_date is None:
continue
claim_amount = cls._serialize_decimal(record.amount)
if claim_amount is None:
claim_amount = str(payload.get("claim_amount") or "").strip() or None
items.append(
{
"organization_inn": cls._digits(record.inn),
@@ -795,6 +799,7 @@ class StateCorpExchangeService:
).strip(),
"status": str(payload.get("status") or record.status or "").strip(),
"decision_date": decision_date.isoformat(),
"claim_amount": claim_amount,
}
)
return items

View File

@@ -8,6 +8,7 @@ from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from apps.core.models import JobStatus
from apps.core.response import api_error_response, api_response
from apps.core.services import BackgroundJobService
from apps.parsers.models import (
@@ -46,7 +47,8 @@ SYSTEM_LOGS_TAG = "System Logs"
ACTIVE_JOB_STATUSES = {"pending", "started", "retry"}
SUCCESS_LOAD_STATUSES = {"success", "skipped"}
ERROR_LOAD_STATUSES = {"failed", "failure", "error"}
STALE_ACTIVE_MAX_AGE_MINUTES = 90
STALE_ACTIVE_MAX_AGE_MINUTES = 4 * 60
STALE_PENDING_MAX_AGE_MINUTES = 24 * 60
PARSING_SETTINGS_CACHE_KEY = "parsers:frontend_compat:parsing_settings"
PARSING_SETTINGS_FIELDS = {
@@ -297,15 +299,34 @@ def _active_tasks_for_definition(
for source_key in definition.source_keys
if source_key in PARSER_SOURCES
]
now = timezone.now()
active_cutoff = now - timedelta(
minutes=int(
getattr(
settings,
"PARSER_STALE_LOAD_MAX_AGE_MINUTES",
STALE_ACTIVE_MAX_AGE_MINUTES,
)
)
)
pending_cutoff = now - timedelta(
minutes=int(
getattr(
settings,
"PARSER_STALE_PENDING_JOB_MAX_AGE_MINUTES",
STALE_PENDING_MAX_AGE_MINUTES,
)
)
)
queryset = (
BackgroundJobService.get_queryset()
.filter(task_name__in=task_names)
.filter(
task_name__in=task_names,
status__in=ACTIVE_JOB_STATUSES,
)
.filter(
Q(started_at__isnull=False, started_at__gte=_stale_cutoff())
| Q(started_at__isnull=True, created_at__gte=_stale_cutoff())
Q(status=JobStatus.PENDING, created_at__gte=pending_cutoff)
| Q(
status__in=[JobStatus.STARTED, JobStatus.RETRY],
updated_at__gte=active_cutoff,
)
)
)
return [_serialize_active_job(job) for job in queryset.order_by("-created_at")[:10]]

View File

@@ -0,0 +1,39 @@
import json
from django.db import migrations
CLEANUP_TASK_NAME = "parser:cleanup-stale-loads"
ACTIVE_MAX_AGE_MINUTES = 4 * 60
PENDING_MAX_AGE_MINUTES = 24 * 60
def extend_stale_parser_job_timeouts(apps, schema_editor):
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
PeriodicTask.objects.filter(name=CLEANUP_TASK_NAME).update(
kwargs=json.dumps(
{
"max_age_minutes": ACTIVE_MAX_AGE_MINUTES,
"pending_max_age_minutes": PENDING_MAX_AGE_MINUTES,
}
)
)
def restore_stale_parser_job_timeouts(apps, schema_editor):
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
PeriodicTask.objects.filter(name=CLEANUP_TASK_NAME).update(
kwargs=json.dumps({"max_age_minutes": 90})
)
class Migration(migrations.Migration):
dependencies = [
("parsers", "0029_neutralize_external_collection_labels"),
]
operations = [
migrations.RunPython(
extend_stale_parser_job_timeouts,
reverse_code=restore_stale_parser_job_timeouts,
),
]

View File

@@ -945,6 +945,7 @@ class SourceCardItemSerializer(serializers.Serializer):
"""Подисточник внутри агрегированной карточки."""
code = serializers.CharField(read_only=True)
refresh_key = serializers.CharField(read_only=True)
title = serializers.CharField(read_only=True)
description = serializers.CharField(read_only=True)
parser_source = serializers.CharField(read_only=True, allow_null=True)

View File

@@ -419,11 +419,14 @@ class ParserLoadLogService(BaseService[ParserLoadLog]):
cls,
*,
max_age_minutes: int,
pending_max_age_minutes: int = 24 * 60,
) -> int:
"""Закрыть зависшие in_progress логи без живой свежей BackgroundJob."""
from apps.core.models import BackgroundJob, JobStatus
cutoff = timezone.now() - timedelta(minutes=max_age_minutes)
now = timezone.now()
cutoff = now - timedelta(minutes=max_age_minutes)
pending_cutoff = now - timedelta(minutes=pending_max_age_minutes)
stale_logs = list(
cls.model.objects.filter(
status=ParserLoadLog.Status.IN_PROGRESS,
@@ -442,8 +445,11 @@ class ParserLoadLogService(BaseService[ParserLoadLog]):
meta__source=log.source,
).filter(Q(meta__batch_id=log.batch_id) | Q(meta__batch_id__isnull=True))
fresh_jobs = active_jobs.filter(
Q(started_at__isnull=False, started_at__gte=cutoff)
| Q(started_at__isnull=True, created_at__gte=cutoff)
Q(status=JobStatus.PENDING, created_at__gte=pending_cutoff)
| Q(
status__in=[JobStatus.STARTED, JobStatus.RETRY],
updated_at__gte=cutoff,
)
)
if fresh_jobs.exists():
continue
@@ -451,8 +457,11 @@ class ParserLoadLogService(BaseService[ParserLoadLog]):
cls.mark_failed(log, stale_message)
updated += 1
stale_jobs = active_jobs.filter(
Q(started_at__isnull=False, started_at__lt=cutoff)
| Q(started_at__isnull=True, created_at__lt=cutoff)
Q(status=JobStatus.PENDING, created_at__lt=pending_cutoff)
| Q(
status__in=[JobStatus.STARTED, JobStatus.RETRY],
updated_at__lt=cutoff,
)
)
for job in stale_jobs.order_by("created_at"):
job.fail(error=stale_message)

View File

@@ -31,7 +31,8 @@ from rest_framework.exceptions import ValidationError
SUCCESSFUL_LOAD_STATUSES = {"success", "skipped"}
ACTIVE_JOB_STATUSES = [JobStatus.PENDING, JobStatus.STARTED, JobStatus.RETRY]
STALE_ACTIVE_MAX_AGE_MINUTES = 90
STALE_ACTIVE_MAX_AGE_MINUTES = 4 * 60
STALE_PENDING_MAX_AGE_MINUTES = 24 * 60
SOURCE_CARD_STATS_CACHE_TIMEOUT_SECONDS = 7 * 24 * 60 * 60
@@ -55,6 +56,7 @@ class SourceItemDefinition:
title: str
description: str
parser_source: str | None = None
refresh_key: str | None = None
@dataclass(frozen=True)
@@ -218,6 +220,7 @@ SOURCE_CARD_DEFINITIONS: tuple[SourceCardDefinition, ...] = (
"Реестр промышленной продукции, произведенной на территории РФ."
),
parser_source=ParserLoadLog.Source.INDUSTRIAL_PRODUCTS,
refresh_key="mpt_products",
),
SourceItemDefinition(
code="manufactures",
@@ -769,17 +772,12 @@ class SourceCardService:
active_tasks_by_slug: dict[str, list[dict[str, Any]]] = {
slug: [] for slug in task_names_by_slug
}
cutoff = cls._stale_cutoff()
queryset = (
BackgroundJobService.get_queryset()
.filter(
task_name__in=list(slugs_by_task_name),
status__in=ACTIVE_JOB_STATUSES,
)
.filter(
Q(started_at__isnull=False, started_at__gte=cutoff)
| Q(started_at__isnull=True, created_at__gte=cutoff)
)
.filter(cls._fresh_active_job_filter())
.order_by("-created_at")
)
@@ -1157,6 +1155,7 @@ class SourceCardService:
return {
"code": item.code,
"refresh_key": item.refresh_key or item.code,
"title": item.title,
"description": item.description,
"parser_source": item.parser_source,
@@ -1288,17 +1287,12 @@ class SourceCardService:
def _get_active_tasks(
cls, definition: SourceCardDefinition
) -> list[dict[str, Any]]:
cutoff = cls._stale_cutoff()
queryset = (
BackgroundJobService.get_queryset()
.filter(
task_name__in=definition.task_names,
status__in=ACTIVE_JOB_STATUSES,
)
.filter(
Q(started_at__isnull=False, started_at__gte=cutoff)
| Q(started_at__isnull=True, created_at__gte=cutoff)
)
.filter(cls._fresh_active_job_filter())
)
return [
cls._serialize_job(job) for job in queryset.order_by("-created_at")[:10]
@@ -1349,6 +1343,31 @@ class SourceCardService:
)
return timezone.now() - timedelta(minutes=max_age_minutes)
@classmethod
def _fresh_active_job_filter(cls) -> Q:
now = timezone.now()
active_max_age_minutes = int(
getattr(
settings,
"PARSER_STALE_LOAD_MAX_AGE_MINUTES",
STALE_ACTIVE_MAX_AGE_MINUTES,
)
)
pending_max_age_minutes = int(
getattr(
settings,
"PARSER_STALE_PENDING_JOB_MAX_AGE_MINUTES",
STALE_PENDING_MAX_AGE_MINUTES,
)
)
return Q(
status=JobStatus.PENDING,
created_at__gte=now - timedelta(minutes=pending_max_age_minutes),
) | Q(
status__in=[JobStatus.STARTED, JobStatus.RETRY],
updated_at__gte=now - timedelta(minutes=active_max_age_minutes),
)
@classmethod
def _is_stale_load(cls, latest_load: ParserLoadLog | None) -> bool:
if latest_load is None or latest_load.status != "in_progress":

View File

@@ -88,7 +88,8 @@ REGISTRY_INSPECTIONS_CHECKO_LIMIT = 1000
REGISTRY_CONTRACTS_CHECKO_LIMIT = 1000
REGISTRY_ENRICHMENT_BATCH_SIZE = 250
FSTEC_CHECKO_IDENTITY_LOOKUP_LIMIT = 1000
PARSER_STALE_LOAD_MAX_AGE_MINUTES = 90
PARSER_STALE_LOAD_MAX_AGE_MINUTES = 4 * 60
PARSER_STALE_PENDING_JOB_MAX_AGE_MINUTES = 24 * 60
PARSER_SOFT_TIME_LIMIT_SECONDS = 15 * 60
PARSER_TIME_LIMIT_SECONDS = 20 * 60
INDUSTRIAL_PRODUCTS_SOFT_TIME_LIMIT_SECONDS = 45 * 60
@@ -3604,7 +3605,10 @@ def parse_fstec_registers(
@shared_task
def cleanup_stale_parser_loads(max_age_minutes: int | None = None) -> dict:
def cleanup_stale_parser_loads(
max_age_minutes: int | None = None,
pending_max_age_minutes: int | None = None,
) -> dict:
"""Закрыть stale in_progress загрузки и jobs после рестартов worker/deploy."""
if max_age_minutes is None:
max_age_minutes = getattr(
@@ -3612,14 +3616,22 @@ def cleanup_stale_parser_loads(max_age_minutes: int | None = None) -> dict:
"PARSER_STALE_LOAD_MAX_AGE_MINUTES",
PARSER_STALE_LOAD_MAX_AGE_MINUTES,
)
if pending_max_age_minutes is None:
pending_max_age_minutes = getattr(
settings,
"PARSER_STALE_PENDING_JOB_MAX_AGE_MINUTES",
PARSER_STALE_PENDING_JOB_MAX_AGE_MINUTES,
)
source_values = {descriptor.source for descriptor in PARSER_SOURCES.values()}
task_names = {descriptor.task_name for descriptor in PARSER_SOURCES.values()}
task_names.add("apps.parsers.tasks.scan_fns_directory")
marked_failed = ParserLoadLogService.mark_stale_in_progress_failed(
max_age_minutes=int(max_age_minutes)
max_age_minutes=int(max_age_minutes),
pending_max_age_minutes=int(pending_max_age_minutes),
)
marked_jobs_failed = BackgroundJobService.mark_stale_active_jobs_failed(
max_age_minutes=int(max_age_minutes),
pending_max_age_minutes=int(pending_max_age_minutes),
task_names=task_names,
meta_sources=source_values,
)
@@ -3628,6 +3640,7 @@ def cleanup_stale_parser_loads(max_age_minutes: int | None = None) -> dict:
"marked_failed": marked_failed,
"marked_jobs_failed": marked_jobs_failed,
"max_age_minutes": int(max_age_minutes),
"pending_max_age_minutes": int(pending_max_age_minutes),
}

View File

@@ -151,6 +151,10 @@ TASKS_BY_NAME = {
"apps.parsers.tasks.parse_trudvsem_vacancies": tasks.parse_trudvsem_vacancies,
}
PARSER_SOURCE_ALIASES = {
"industrial_products": "mpt_products",
}
class MultipartFormSwaggerAutoSchema(SwaggerAutoSchema):
"""Document mixed JSON/multipart upload endpoints as form-data in Swagger."""
@@ -2642,21 +2646,25 @@ class ParserRunView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request: Request, source_key: str):
descriptor = PARSER_SOURCES.get(source_key)
canonical_source_key = PARSER_SOURCE_ALIASES.get(source_key, source_key)
descriptor = PARSER_SOURCES.get(canonical_source_key)
if descriptor is None:
return _source_not_found_response(source_key)
serializer = ParserRunRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
task = TASKS_BY_NAME[descriptor.task_name]
task_kwargs = build_task_kwargs(
source_key, serializer.validated_data, request.user.id
canonical_source_key, serializer.validated_data, request.user.id
)
task_id = str(uuid.uuid4())
BackgroundJobService.create_job(
task_id=task_id,
task_name=descriptor.task_name,
user_id=request.user.id,
meta={"source_key": source_key, "source": descriptor.source},
meta={
"source_key": canonical_source_key,
"source": descriptor.source,
},
)
async_result = task.apply_async(kwargs=task_kwargs, task_id=task_id)
return api_response(

View File

@@ -661,6 +661,7 @@ class TestCompanyDatasetService:
@staticmethod
def _refresh_financial_lines(*, record, index: int) -> None:
report_year = timezone.localdate().year
expected = {
("1", "1600", "Баланс (актив)", 10_000_000 + index * 100_000),
("1", "1300", "Капитал и резервы", 4_000_000 + index * 50_000),
@@ -669,12 +670,12 @@ class TestCompanyDatasetService:
}
expected_keys = []
for form_code, line_code, line_name, period_end in expected:
expected_keys.append((form_code, line_code, 2025))
expected_keys.append((form_code, line_code, report_year))
OrganizationSourceFinancialLine.objects.update_or_create(
source_record=record,
form_code=form_code,
line_code=line_code,
year=2025,
year=report_year,
defaults={
"line_name": line_name,
"period_start": period_end - 100_000,

View File

@@ -61,6 +61,20 @@ class BackgroundJobModelTest(TestCase):
self.assertEqual(job.result, result)
self.assertIsNotNone(job.completed_at)
def test_complete_clears_previous_failure_details(self):
job = BackgroundJob.objects.create(
task_id=fake.uuid4(),
task_name="test.task",
error="stale error",
traceback="stale traceback",
)
job.complete(result={"processed": 1})
self.assertEqual(job.status, JobStatus.SUCCESS)
self.assertEqual(job.error, "")
self.assertEqual(job.traceback, "")
def test_fail(self):
"""Тест завершения с ошибкой."""
job = BackgroundJob.objects.create(
@@ -288,7 +302,7 @@ class BackgroundJobServiceTest(TestCase):
task_name="apps.other.tasks.task",
meta={"source": "industrial_products"},
)
old_timestamp = timezone.now() - timedelta(hours=3)
old_timestamp = timezone.now() - timedelta(hours=25)
BackgroundJob.objects.filter(
task_id__in=[stale_job.task_id, unrelated_job.task_id]
).update(created_at=old_timestamp, updated_at=timezone.now())
@@ -307,3 +321,37 @@ class BackgroundJobServiceTest(TestCase):
self.assertIn("Stale background job", stale_job.error)
self.assertEqual(fresh_job.status, JobStatus.PENDING)
self.assertEqual(unrelated_job.status, JobStatus.PENDING)
def test_mark_stale_active_jobs_uses_updated_at_for_started_job(self):
job = BackgroundJobService.create_job(
task_id="job-heartbeat",
task_name="apps.parsers.tasks.parse_industrial_products",
meta={"source": "industrial_products"},
)
job.mark_started()
old_timestamp = timezone.now() - timedelta(hours=5)
BackgroundJob.objects.filter(pk=job.pk).update(
started_at=old_timestamp,
updated_at=timezone.now(),
)
updated = BackgroundJobService.mark_stale_active_jobs_failed(
max_age_minutes=240,
task_names={"apps.parsers.tasks.parse_industrial_products"},
meta_sources={"industrial_products"},
)
self.assertEqual(updated, 0)
job.refresh_from_db()
self.assertEqual(job.status, JobStatus.STARTED)
BackgroundJob.objects.filter(pk=job.pk).update(updated_at=old_timestamp)
updated = BackgroundJobService.mark_stale_active_jobs_failed(
max_age_minutes=240,
task_names={"apps.parsers.tasks.parse_industrial_products"},
meta_sources={"industrial_products"},
)
self.assertEqual(updated, 1)
job.refresh_from_db()
self.assertEqual(job.status, JobStatus.FAILURE)

View File

@@ -249,10 +249,12 @@ class StateCorpExchangeServiceTest(TestCase):
ogrn=organization.ogrn,
title="А40-1/2026",
record_date="2026-03-25",
amount="1250000.50",
status="in_progress",
payload={
"case_number": "А40-1/2026",
"court_name": "АС города Москвы",
"claim_amount": "1.00",
"target": {"role": "ответчик"},
},
)
@@ -429,6 +431,10 @@ class StateCorpExchangeServiceTest(TestCase):
payload["data"]["arbitration_cases"][0]["case_number"],
"А40-1/2026",
)
self.assertEqual(
payload["data"]["arbitration_cases"][0]["claim_amount"],
"1250000.50",
)
self.assertEqual(
payload["data"]["bankruptcy_procedures"][0]["case_number"],
"А40-555/2026",

View File

@@ -15,6 +15,7 @@ from apps.parsers.models import (
)
from django.core.management import call_command
from django.test import TestCase, override_settings
from django.utils import timezone
from organizations.models import (
Organization,
OrganizationSourceExtension,
@@ -88,6 +89,14 @@ class TestCompaniesCommandsTest(TestCase):
).count(),
20 * 4,
)
self.assertEqual(
set(
OrganizationSourceFinancialLine.objects.filter(
source_record__extension__organization__in=companies
).values_list("year", flat=True)
),
{timezone.localdate().year},
)
self.assertEqual(IndustrialCertificateRecord.objects.count(), 20)
self.assertEqual(IndustrialProductRecord.objects.count(), 20)
self.assertEqual(ManufacturerRecord.objects.count(), 20)

View File

@@ -441,8 +441,8 @@ class ParserLoadLogServiceTest(TestCase):
self.assertEqual(updated, 0)
self.assertEqual(log.status, ParserLoadLog.Status.IN_PROGRESS)
def test_mark_stale_in_progress_failed_closes_precreated_job_without_batch(self):
"""Pre-created source-card jobs without batch_id are still linked by source."""
def test_mark_stale_in_progress_failed_keeps_recent_heartbeat_without_batch(self):
"""A recent heartbeat keeps a pre-created source-card job alive."""
log = ParserLoadLogFactory(
source=ParserLoadLog.Source.INDUSTRIAL_PRODUCTS,
batch_id=2,
@@ -465,9 +465,9 @@ class ParserLoadLogServiceTest(TestCase):
log.refresh_from_db()
job.refresh_from_db()
self.assertEqual(updated, 1)
self.assertEqual(log.status, ParserLoadLog.Status.FAILED)
self.assertEqual(job.status, JobStatus.FAILURE)
self.assertEqual(updated, 0)
self.assertEqual(log.status, ParserLoadLog.Status.IN_PROGRESS)
self.assertEqual(job.status, JobStatus.STARTED)
@unittest.skip(

View File

@@ -392,7 +392,7 @@ class SourceCardServiceUnitTest(SimpleTestCase):
)
stale_in_progress_load = SimpleNamespace(
status="in_progress",
updated_at=timezone.now() - timedelta(hours=3),
updated_at=timezone.now() - timedelta(hours=5),
)
self.assertEqual(
SourceCardService._get_status(
@@ -618,7 +618,7 @@ class SourceCardServiceDatabaseTest(TestCase):
)
self.assertEqual(procurements_card["records_count"], 1)
def test_get_active_tasks_ignores_old_jobs_even_when_updated_recently(self):
def test_get_active_tasks_keeps_old_job_with_recent_heartbeat(self):
job = BackgroundJob.objects.create(
task_id="old-source-task",
task_name="apps.parsers.tasks.parse_industrial_products",
@@ -637,7 +637,8 @@ class SourceCardServiceDatabaseTest(TestCase):
SourceCardService.get_definition("manufacturers-and-products")
)
self.assertEqual(tasks, [])
self.assertEqual(len(tasks), 1)
self.assertEqual(tasks[0]["task_id"], job.task_id)
def test_get_active_tasks_keeps_recent_pending_jobs(self):
BackgroundJob.objects.create(

View File

@@ -133,6 +133,11 @@ class SourcesApiE2ETest(APITestCase):
self.assertEqual(minprom_card["records_count"], 3)
self.assertEqual(minprom_card["organizations_count"], 1)
self.assertEqual(len(minprom_card["source_items"]), 3)
minprom_items = {item["code"]: item for item in minprom_card["source_items"]}
self.assertEqual(
minprom_items["industrial_products"]["refresh_key"],
"mpt_products",
)
statuses = {item["slug"]: item for item in statuses_response.data["data"]}
self.assertEqual(statuses["planned-inspections"]["progress"], 55)

View File

@@ -1086,9 +1086,16 @@ class GenericSourceFetchTestCase(TestCase):
) as jobs_cleanup_mock:
result = parser_tasks.cleanup_stale_parser_loads(max_age_minutes=45)
cleanup_mock.assert_called_once_with(max_age_minutes=45)
cleanup_mock.assert_called_once_with(
max_age_minutes=45,
pending_max_age_minutes=24 * 60,
)
jobs_cleanup_mock.assert_called_once()
self.assertEqual(jobs_cleanup_mock.call_args.kwargs["max_age_minutes"], 45)
self.assertEqual(
jobs_cleanup_mock.call_args.kwargs["pending_max_age_minutes"],
24 * 60,
)
self.assertIn(
"apps.parsers.tasks.scan_fns_directory",
jobs_cleanup_mock.call_args.kwargs["task_names"],
@@ -1101,6 +1108,7 @@ class GenericSourceFetchTestCase(TestCase):
self.assertEqual(result["marked_failed"], 2)
self.assertEqual(result["marked_jobs_failed"], 3)
self.assertEqual(result["max_age_minutes"], 45)
self.assertEqual(result["pending_max_age_minutes"], 24 * 60)
def test_get_or_create_background_job_merges_meta_for_precreated_job(self):
BackgroundJobService.create_job(

View File

@@ -11,6 +11,7 @@ import zipfile
from datetime import date
from unittest.mock import Mock, patch
from apps.core.models import BackgroundJob
from apps.parsers.models import (
FinancialReport,
FinancialReportLine,
@@ -1411,3 +1412,19 @@ class ParsersViewSetTest(APITestCase):
for key, value in payload.items():
self.assertEqual(task_kwargs[key], value)
self.assertEqual(task_kwargs["requested_by_id"], self.user.id)
def test_run_industrial_products_uses_legacy_alias(self):
self.client.force_authenticate(self.user)
url = reverse("api_v1:parsers:run-parser", args=["industrial_products"])
with patch(
"apps.parsers.views.tasks.parse_industrial_products.apply_async",
return_value=Mock(id="task-products"),
) as apply_async_mock:
response = self.client.post(url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
apply_async_mock.assert_called_once()
queued_task_id = apply_async_mock.call_args.kwargs["task_id"]
job = BackgroundJob.objects.get(task_id=queued_task_id)
self.assertEqual(job.meta["source_key"], "mpt_products")