feat: align source data and nightly exports
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 43s
CI/CD Pipeline / Build and Push Images (push) Successful in 19s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 24s

This commit is contained in:
2026-08-03 19:27:07 +02:00
parent dd8697c733
commit b19951d0c9
24 changed files with 1736 additions and 210 deletions

View File

@@ -1,4 +1,4 @@
"""Quota-safe monthly claims for organization lookups in Checko."""
"""Quota-safe monthly claims for external organization lookups."""
from datetime import date, datetime
@@ -24,7 +24,7 @@ def claim_monthly_collection(
source: str,
at: date | datetime | None = None,
) -> CheckoCollectionAttempt | None:
"""Atomically claim this month's only allowed Checko collection attempt."""
"""Atomically claim this month's only allowed external collection attempt."""
attempt, created = CheckoCollectionAttempt.objects.get_or_create(
organization_id=organization_id,
source=source,

View File

@@ -346,14 +346,18 @@ class CheckoClient:
# Preserve the existing classification for non-quota HTTP
# errors while still recovering Checko quota metadata.
pass
logger.error("Checko HTTP request failed with status=%s", e.status_code)
logger.error(
"External provider HTTP request failed with status=%s", e.status_code
)
raise CheckoConnectionError(
"Checko API request failed",
"External provider API request failed",
url=e.url,
) from e
except Exception as e:
logger.error("Connection error: %s", e)
raise CheckoConnectionError(f"Failed to connect to Checko API: {e}") from e
raise CheckoConnectionError(
f"Failed to connect to external provider API: {e}"
) from e
self._raise_api_error(data)

View File

@@ -0,0 +1,18 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parsers", "0028_registry_procurement_claims"),
]
operations = [
migrations.AlterModelOptions(
name="checkocollectionattempt",
options={
"ordering": ["-period_month", "source", "organization_id"],
"verbose_name": "попытка сбора внешних данных",
"verbose_name_plural": "попытки сбора внешних данных",
},
),
]

View File

@@ -118,7 +118,7 @@ class ParserBatchSequence(TimestampMixin, models.Model):
class CheckoCollectionAttempt(TimestampMixin, models.Model):
"""Monthly Checko collection claim for one organization and source."""
"""Monthly external collection claim for one organization and source."""
class Source(models.TextChoices):
ARBITRATION = "arbitration", _("Арбитражные дела")
@@ -160,8 +160,8 @@ class CheckoCollectionAttempt(TimestampMixin, models.Model):
class Meta:
db_table = "parsers_checko_collection_attempt"
verbose_name = _("попытка сбора Checko")
verbose_name_plural = _("попытки сбора Checko")
verbose_name = _("попытка сбора внешних данных")
verbose_name_plural = _("попытки сбора внешних данных")
ordering = ["-period_month", "source", "organization_id"]
constraints = [
models.UniqueConstraint(

View File

@@ -236,10 +236,10 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
status="implemented",
upstream_url="https://kad.arbitr.ru/",
access_method="official_search_api",
parser_strategy="checko_legal_cases_by_inn_ogrn",
parser_strategy="external_legal_cases_by_inn_ogrn",
source_notes=(
"Поиск дел выполняется по ИНН/ОГРН активных организаций из реестров. "
"Checko отдаёт карточки со ссылками на КАД Арбитр."
"Внешний сервис данных отдаёт карточки со ссылками на КАД Арбитр."
),
api_route="arbitration/cases",
),
@@ -258,7 +258,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
parser_strategy="fedresurs_bankruptcy_search",
source_notes=(
"Официальный ЕФРСБ; может отдавать anti-bot challenge worker'ам. "
"Если официальный портал недоступен, используется Checko API по "
"Если официальный портал недоступен, используется внешний сервис данных по "
"организациям из реестров. "
"Ручная загрузка разрешена только для выгрузок, переданных Сергеем."
),

View File

@@ -470,7 +470,7 @@ def _fetch_fedresurs_bankruptcy_records(
file_path: str | None,
proxies: list[str] | None,
) -> list[GenericParserItem]:
"""Загрузить банкротства: официальный портал, затем fallback через Checko."""
"""Загрузить банкротства: официальный портал, затем внешний fallback."""
official_error: Exception | None = None
try:
official_records = _fetch_structured_records(
@@ -482,14 +482,14 @@ def _fetch_fedresurs_bankruptcy_records(
if official_records or file_url or file_path:
return official_records
logger.warning(
"Fedresurs official source returned no records, falling back to Checko"
"Fedresurs official source returned no records, using external fallback"
)
except Exception as exc:
if file_url or file_path:
raise
official_error = exc
logger.warning(
"Fedresurs official source failed, falling back to Checko: %s",
"Fedresurs official source failed, using external fallback: %s",
exc,
)
records = _fetch_checko_bankruptcy_records(proxies=proxies)
@@ -498,12 +498,12 @@ def _fetch_fedresurs_bankruptcy_records(
if official_error is None:
raise ParserSourceSkipped(
"fedresurs official source returned no bankruptcy records; "
"Checko fallback returned no bankruptcy records"
"external fallback returned no bankruptcy records"
)
if isinstance(official_error, HTTPClientError):
raise ParserSourceSkipped(
"fedresurs upstream is unavailable or blocked; "
"Checko fallback returned no bankruptcy records"
"external fallback returned no bankruptcy records"
) from official_error
raise official_error
@@ -580,7 +580,7 @@ def _enrich_fstec_record_identities(
logger.info(
"FSTEC identity enrichment completed: enriched=%d ambiguous=%d "
"local_candidates=%d checko_candidates=%d",
"local_candidates=%d external_candidates=%d",
enriched_count,
ambiguous_count,
len(local_candidates),
@@ -720,7 +720,7 @@ def _fstec_checko_identity_candidates(
)
except CheckoError as exc:
logger.info(
"Checko FSTEC identity lookup skipped for %s: %s",
"External FSTEC identity lookup skipped for %s: %s",
applicant_name,
exc,
)
@@ -846,10 +846,10 @@ def _fetch_checko_bankruptcy_records(
*,
proxies: list[str] | None,
) -> list[GenericParserItem]:
"""Получить ЕФРСБ-сообщения по организациям из наших реестров через Checko."""
"""Получить ЕФРСБ-сообщения по организациям через внешний сервис данных."""
api_key = getattr(settings, "CHECKO_API_KEY", "")
if not api_key:
logger.warning("CHECKO_API_KEY is empty; Fedresurs fallback skipped")
logger.warning("External provider API key is empty; Fedresurs fallback skipped")
return []
limit = _resolve_lookup_limit(
@@ -861,7 +861,7 @@ def _fetch_checko_bankruptcy_records(
default=FEDRESURS_CHECKO_FALLBACK_LIMIT,
)
if limit <= 0:
logger.info("Fedresurs Checko fallback is disabled by limit=%s", limit)
logger.info("Fedresurs external fallback is disabled by limit=%s", limit)
return []
targets = _active_registry_lookup_targets(
limit=limit,
@@ -893,7 +893,7 @@ def _fetch_checko_bankruptcy_records(
except CheckoRateLimitError as exc:
finish_collection(attempt, records_count=0, error=exc)
logger.warning(
"Checko bankruptcy fallback stopped: quota/rate limit reached "
"External bankruptcy fallback stopped: quota/rate limit reached "
"(status_code=%s)",
exc.status_code,
)
@@ -901,7 +901,7 @@ def _fetch_checko_bankruptcy_records(
except CheckoError as exc:
finish_collection(attempt, records_count=0, error=exc)
logger.info(
"Checko bankruptcy lookup skipped for target=%s: %s",
"External bankruptcy lookup skipped for target=%s: %s",
target.inn or target.ogrn,
exc,
)
@@ -920,7 +920,7 @@ def _fetch_checko_bankruptcy_records(
attempt,
records_count=len(records) - records_before,
)
logger.info("Fetched %d bankruptcy records through Checko fallback", len(records))
logger.info("Fetched %d bankruptcy records through external fallback", len(records))
return records
@@ -931,7 +931,7 @@ def _checko_bankruptcy_items(
fallback_ogrn: str,
fallback_name: str,
) -> list[GenericParserItem]:
"""Преобразовать банкротные сообщения Checko в generic records."""
"""Преобразовать банкротные сообщения внешнего сервиса в generic records."""
inn = str(getattr(company, "inn", "") or fallback_inn)
ogrn = str(getattr(company, "ogrn", "") or fallback_ogrn)
name = getattr(company, "short_name", None) or fallback_name
@@ -1084,14 +1084,16 @@ def _fetch_checko_arbitration_records(
limit: int | None,
proxies: list[str] | None,
) -> list[GenericParserItem]:
"""Получить арбитражные дела по ИНН/ОГРН через Checko legal-cases API."""
"""Получить арбитражные дела по ИНН/ОГРН через внешний API."""
api_key = getattr(settings, "CHECKO_API_KEY", "")
if not api_key:
raise ParserSourceSkipped("CHECKO_API_KEY is empty; arbitration parser skipped")
raise ParserSourceSkipped(
"External provider API key is empty; arbitration parser skipped"
)
resolved_limit = _resolve_arbitration_limit(limit)
if resolved_limit <= 0:
logger.info("Arbitration Checko parser is disabled by limit=%s", limit)
logger.info("Arbitration external parser is disabled by limit=%s", limit)
return []
subjects = _arbitration_subjects(resolved_limit)
@@ -1128,7 +1130,7 @@ def _fetch_checko_arbitration_records(
failed_lookups += 1
finish_collection(attempt, records_count=0, error=exc)
logger.info(
"Checko arbitration lookup skipped for subject=%s: %s",
"External arbitration lookup skipped for subject=%s: %s",
_arbitration_subject_key(subject),
exc,
)
@@ -1139,10 +1141,12 @@ def _fetch_checko_arbitration_records(
)
if attempted_lookups and failed_lookups == attempted_lookups and not records:
raise ParserSourceSkipped("Checko arbitration lookups failed for all subjects")
raise ParserSourceSkipped(
"External arbitration lookups failed for all subjects"
)
logger.info(
"Fetched %d arbitration records through Checko for %d subjects",
"Fetched %d arbitration records through external service for %d subjects",
len(records),
len(subjects),
)
@@ -1150,7 +1154,7 @@ def _fetch_checko_arbitration_records(
def _checko_arbitration_item(case, *, subject: ArbitrationSubject) -> GenericParserItem:
"""Преобразовать дело Checko в generic record."""
"""Преобразовать дело внешнего сервиса в generic record."""
case_number = getattr(case, "case_number", "") or ""
filing_date = getattr(case, "filing_date", "") or ""
role = _case_role_for_subject(case, subject)
@@ -1335,11 +1339,11 @@ def _fetch_checko_registry_inspections(
limit: int | None,
proxies: list[str] | None,
) -> list[ProverkiInspection]:
"""Получить проверки по активным организациям из реестров через Checko."""
"""Получить проверки по активным организациям через внешний сервис данных."""
api_key = getattr(settings, "CHECKO_API_KEY", "")
if not api_key:
raise ParserSourceSkipped(
"CHECKO_API_KEY is empty; registry inspections parser skipped"
"External provider API key is empty; registry inspections parser skipped"
)
resolved_limit = _resolve_lookup_limit(
@@ -1351,7 +1355,9 @@ def _fetch_checko_registry_inspections(
),
)
if resolved_limit <= 0:
logger.info("Registry inspections Checko parser is disabled by limit=%s", limit)
logger.info(
"Registry inspections external parser is disabled by limit=%s", limit
)
return []
targets = _active_registry_lookup_targets(
@@ -1391,7 +1397,7 @@ def _fetch_checko_registry_inspections(
failed_lookups += 1
finish_collection(attempt, records_count=0, error=exc)
logger.info(
"Checko inspections lookup skipped for target=%s: %s",
"External inspections lookup skipped for target=%s: %s",
target.inn or target.ogrn,
exc,
)
@@ -1402,10 +1408,10 @@ def _fetch_checko_registry_inspections(
)
if attempted_lookups and failed_lookups == attempted_lookups and not records:
raise ParserSourceSkipped("Checko inspections lookups failed for all targets")
raise ParserSourceSkipped("External inspections lookups failed for all targets")
logger.info(
"Fetched %d inspections through Checko for %d registry organizations",
"Fetched %d inspections through external service for %d registry organizations",
len(records),
len(targets),
)
@@ -1488,7 +1494,7 @@ def _checko_unfair_supplier_items(
company,
target: RegistryLookupTarget,
) -> list[GenericParserItem]:
"""Convert Checko НедобПостЗап values into supplier-bound source records."""
"""Convert external НедобПостЗап values into supplier-bound source records."""
company_inn = _normalize_identifier(getattr(company, "inn", ""))
company_ogrn = _normalize_identifier(getattr(company, "ogrn", ""))
if target.inn and company_inn != target.inn:
@@ -1563,10 +1569,12 @@ def _fetch_checko_unfair_supplier_records( # noqa: C901
proxies: list[str] | None,
organization_ids: list[str] | None = None,
) -> list[GenericParserItem]:
"""Fetch RNP entries through one Checko /company request per OПК organization."""
"""Fetch RNP entries through one external request per OПК organization."""
api_key = getattr(settings, "CHECKO_API_KEY", "")
if not api_key:
raise ParserSourceSkipped("CHECKO_API_KEY is empty; RNP parser skipped")
raise ParserSourceSkipped(
"External provider API key is empty; RNP parser skipped"
)
resolved_limit = _resolve_registry_enrichment_limit(limit)
if resolved_limit <= 0:
@@ -1604,13 +1612,13 @@ def _fetch_checko_unfair_supplier_records( # noqa: C901
finish_collection(attempt, records_count=0, error=exc)
failed_lookups += 1
rate_limited = True
logger.warning("Checko RNP lookup stopped: quota/rate limit reached")
logger.warning("External RNP lookup stopped: quota/rate limit reached")
break
except CheckoError as exc:
finish_collection(attempt, records_count=0, error=exc)
failed_lookups += 1
logger.info(
"Checko RNP lookup failed for target=%s: %s",
"External RNP lookup failed for target=%s: %s",
target.inn or target.ogrn,
exc,
)
@@ -1628,7 +1636,7 @@ def _fetch_checko_unfair_supplier_records( # noqa: C901
)
if not records and (rate_limited or failed_lookups == len(targets)):
raise ParserSourceSkipped("Checko RNP lookups failed for all targets")
raise ParserSourceSkipped("External RNP lookups failed for all targets")
return records
@@ -1729,11 +1737,11 @@ def _fetch_checko_registry_contract_records(
limit: int | None,
proxies: list[str] | None,
) -> list[GenericParserItem]:
"""Получить контракты по активным организациям из реестров через Checko."""
"""Получить контракты по активным организациям через внешний сервис данных."""
api_key = getattr(settings, "CHECKO_API_KEY", "")
if not api_key:
raise ParserSourceSkipped(
"CHECKO_API_KEY is empty; registry contracts parser skipped"
"External provider API key is empty; registry contracts parser skipped"
)
resolved_limit = _resolve_lookup_limit(
@@ -1745,7 +1753,7 @@ def _fetch_checko_registry_contract_records(
),
)
if resolved_limit <= 0:
logger.info("Registry contracts Checko parser is disabled by limit=%s", limit)
logger.info("Registry contracts external parser is disabled by limit=%s", limit)
return []
targets = _active_registry_lookup_targets(
@@ -1790,7 +1798,7 @@ def _fetch_checko_registry_contract_records(
failed_lookups += 1
target_failures.append(exc)
logger.info(
"Checko contracts lookup skipped for target=%s law=%s: %s",
"External contracts lookup skipped for target=%s law=%s: %s",
target.inn or target.ogrn,
law.value,
exc,
@@ -1803,10 +1811,10 @@ def _fetch_checko_registry_contract_records(
expected_lookups = attempted_lookups * 2
if expected_lookups and failed_lookups == expected_lookups and not records:
raise ParserSourceSkipped("Checko contracts lookups failed for all targets")
raise ParserSourceSkipped("External contracts lookups failed for all targets")
logger.info(
"Fetched %d contracts through Checko for %d registry organizations",
"Fetched %d contracts through external service for %d registry organizations",
len(records),
len(targets),
)
@@ -3374,7 +3382,7 @@ def parse_unfair_suppliers(
organization_ids: list[str] | None = None,
requested_by_id: int | None = None,
) -> dict:
"""Checko RNP lookup by default; explicit files remain a manual tool."""
"""External RNP lookup by default; explicit files remain a manual tool."""
proxies = _resolve_proxies(proxies)
if file_url or file_path:
@@ -3478,7 +3486,7 @@ def parse_registry_inspections(
proxies: list[str] | None = None,
requested_by_id: int | None = None,
) -> dict:
"""Lookup проверок по активным организациям из реестров через Checko."""
"""Lookup проверок по активным организациям через внешний сервис данных."""
proxies = _resolve_proxies(proxies)
return _run_inspection_parser(
self,