feat: complete published registry contracts and gated SRO ingestion
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
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 1m45s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-14 17:01:02 +02:00
parent 49cbfd265c
commit 18971d33ec
76 changed files with 33156 additions and 311 deletions

View File

@@ -5,8 +5,31 @@
- BackgroundJob - статус фоновых задач
"""
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
SNAPSHOT_SOURCES = (
"budget_ubpandnubp",
"fns_sme_support_recipients",
"sro_membership_check",
)
JOB_API_STATUSES = (
"queued",
"running",
"retrying",
"success",
"failed",
"cancelled",
"skipped",
"error",
# Deprecated transport aliases retained for existing generated clients.
"pending",
"started",
"retry",
"failure",
"revoked",
)
def _get_frontend_job_status(raw_status: str) -> str:
mapping = {
@@ -20,6 +43,83 @@ def _get_frontend_job_status(raw_status: str) -> str:
return mapping.get(raw_status, raw_status)
def _get_job_source(job) -> str | None:
meta = job.meta if isinstance(job.meta, dict) else {}
source = meta.get("source") or meta.get("source_key")
if source in SNAPSHOT_SOURCES:
return source
for candidate in SNAPSHOT_SOURCES:
if job.task_name == f"parsers.{candidate}.refresh":
return candidate
return None
def _get_job_status(job) -> str:
if _get_job_source(job) is None:
return _get_frontend_job_status(job.status)
if (
job.status == "success"
and isinstance(job.result, dict)
and job.result.get("status") == "skipped"
):
return "skipped"
return {
"pending": "queued",
"started": "running",
"retry": "retrying",
"success": "success",
"failure": "failed",
"revoked": "cancelled",
}.get(job.status, job.status)
class SnapshotRunMetadataSerializer(serializers.Serializer):
"""Source-controlled counters only; raw responses/configuration stay private."""
source = serializers.ChoiceField(choices=SNAPSHOT_SOURCES, read_only=True)
mode = serializers.ChoiceField(choices=("incremental", "full"), read_only=True)
candidate_organizations_count = serializers.IntegerField(read_only=True)
queried_organizations_count = serializers.IntegerField(read_only=True)
not_found_organizations_count = serializers.IntegerField(read_only=True)
raw_memberships_count = serializers.IntegerField(read_only=True)
found_memberships_count = serializers.IntegerField(read_only=True)
published_records_count = serializers.IntegerField(read_only=True)
quarantined_records_count = serializers.IntegerField(read_only=True)
missing_admission_dates_count = serializers.IntegerField(read_only=True)
http_errors_count = serializers.IntegerField(read_only=True)
parse_errors_count = serializers.IntegerField(read_only=True)
snapshot_records_count = serializers.IntegerField(read_only=True)
snapshot_organizations_count = serializers.IntegerField(read_only=True)
batch_published_records_count = serializers.IntegerField(read_only=True)
updated_records_count = serializers.IntegerField(read_only=True)
upstream_registry_date = serializers.DateField(read_only=True, allow_null=True)
class SnapshotJobResultSerializer(SnapshotRunMetadataSerializer):
"""Published registry result; legacy task results retain their existing shape."""
status = serializers.ChoiceField(choices=("success", "skipped"), read_only=True)
source = serializers.ChoiceField(choices=SNAPSHOT_SOURCES, read_only=True)
batch_id = serializers.IntegerField(read_only=True)
load_id = serializers.IntegerField(read_only=True)
artifact_id = serializers.UUIDField(read_only=True)
records_count = serializers.IntegerField(read_only=True)
organizations_count = serializers.IntegerField(read_only=True)
raw_records_count = serializers.IntegerField(read_only=True)
published_records_count = serializers.IntegerField(read_only=True)
quarantine_records_count = serializers.IntegerField(read_only=True)
skipped_records_count = serializers.IntegerField(read_only=True)
snapshot_date = serializers.DateField(read_only=True, allow_null=True)
source_published_at = serializers.DateField(read_only=True, allow_null=True)
version = serializers.CharField(read_only=True, allow_blank=True, allow_null=True)
checksum_sha256 = serializers.CharField(
read_only=True, allow_blank=True, allow_null=True
)
class Meta:
swagger_schema_fields = {"additional_properties": True}
def _get_task_short_name(task_name: str) -> str:
return task_name.rsplit(".", 1)[-1] if task_name else task_name
@@ -35,17 +135,59 @@ class BackgroundJobSerializer(serializers.Serializer):
status = serializers.SerializerMethodField()
progress = serializers.IntegerField(read_only=True)
message = serializers.SerializerMethodField()
result = serializers.JSONField(read_only=True)
result = serializers.SerializerMethodField()
meta = serializers.SerializerMethodField()
source = serializers.SerializerMethodField()
error = serializers.SerializerMethodField()
def to_representation(self, instance):
payload = super().to_representation(instance)
if _get_job_source(instance) is None:
payload.pop("source", None)
payload.pop("meta", None)
return payload
@swagger_serializer_method(serializer_or_field=SnapshotRunMetadataSerializer())
def get_meta(self, obj):
metadata = obj.meta if isinstance(obj.meta, dict) else {}
return SnapshotRunMetadataSerializer(
{**metadata, "source": _get_job_source(obj)}
).data
@swagger_serializer_method(
serializer_or_field=SnapshotJobResultSerializer(allow_null=True)
)
def get_result(self, obj):
return obj.result
@swagger_serializer_method(
serializer_or_field=serializers.ChoiceField(
choices=SNAPSHOT_SOURCES, allow_null=True
)
)
def get_source(self, obj):
return _get_job_source(obj)
@swagger_serializer_method(
serializer_or_field=serializers.CharField(allow_null=True)
)
def get_message(self, obj):
return obj.progress_message or None
@swagger_serializer_method(
serializer_or_field=serializers.CharField(allow_null=True)
)
def get_error(self, obj):
return obj.error or None
@swagger_serializer_method(
serializer_or_field=serializers.ChoiceField(
choices=JOB_API_STATUSES,
help_text="Registry jobs use queued/running/retrying/success/failed/cancelled/skipped. Raw pending/started/retry/failure/revoked are deprecated legacy aliases.",
)
)
def get_status(self, obj):
return _get_frontend_job_status(obj.status)
return _get_job_status(obj)
class BackgroundJobListSerializer(serializers.Serializer):
@@ -57,12 +199,33 @@ class BackgroundJobListSerializer(serializers.Serializer):
name = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
progress = serializers.IntegerField(read_only=True)
source = serializers.SerializerMethodField()
def to_representation(self, instance):
payload = super().to_representation(instance)
if _get_job_source(instance) is None:
payload.pop("source", None)
return payload
@swagger_serializer_method(
serializer_or_field=serializers.ChoiceField(
choices=SNAPSHOT_SOURCES, allow_null=True
)
)
def get_source(self, obj):
return _get_job_source(obj)
def get_name(self, obj):
return _get_task_short_name(obj.task_name)
@swagger_serializer_method(
serializer_or_field=serializers.ChoiceField(
choices=JOB_API_STATUSES,
help_text="Registry jobs use queued/running/retrying/success/failed/cancelled/skipped. Raw pending/started/retry/failure/revoked are deprecated legacy aliases.",
)
)
def get_status(self, obj):
return _get_frontend_job_status(obj.status)
return _get_job_status(obj)
class BackgroundJobListResponseSerializer(serializers.Serializer):