gendesign/backend/app/services/scrapers/obj_checks.py
Light1YT e0fff9cfb0
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m45s
CI / openapi-codegen-check (pull_request) Successful in 1m43s
CI / backend-tests (push) Failing after 8m36s
CI / backend-tests (pull_request) Failing after 8m36s
fix(week-review): backend-аудит v2 — 82 issue (#1560-1656)
Имплементация фиксов 2-го аудита backend/app/** (после merge #1543).
Воркер на файл, точечные правки. Верификация: py_compile 58/58 .py.

Полностью исправлено (82).
Оставлены открытыми (13): partial/needs-cross-file/needs-leha — #1569, #1590, #1593, #1606, #1609, #1617, #1633, #1635, #1637, #1638, #1640, #1642, #1650.

Closes #1560
Closes #1561
Closes #1562
Closes #1563
Closes #1564
Closes #1565
Closes #1566
Closes #1567
Closes #1570
Closes #1571
Closes #1572
Closes #1573
Closes #1574
Closes #1576
Closes #1577
Closes #1578
Closes #1579
Closes #1580
Closes #1581
Closes #1582
Closes #1583
Closes #1584
Closes #1585
Closes #1586
Closes #1587
Closes #1588
Closes #1589
Closes #1591
Closes #1592
Closes #1594
Closes #1595
Closes #1596
Closes #1597
Closes #1598
Closes #1599
Closes #1600
Closes #1601
Closes #1602
Closes #1603
Closes #1604
Closes #1605
Closes #1607
Closes #1608
Closes #1610
Closes #1611
Closes #1612
Closes #1613
Closes #1614
Closes #1615
Closes #1616
Closes #1618
Closes #1619
Closes #1620
Closes #1621
Closes #1622
Closes #1623
Closes #1624
Closes #1625
Closes #1626
Closes #1627
Closes #1628
Closes #1629
Closes #1630
Closes #1631
Closes #1632
Closes #1634
Closes #1636
Closes #1639
Closes #1641
Closes #1643
Closes #1644
Closes #1645
Closes #1646
Closes #1647
Closes #1648
Closes #1649
Closes #1651
Closes #1652
Closes #1653
Closes #1654
Closes #1655
Closes #1656
2026-06-17 01:30:52 +05:00

224 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Extractor for 6 «Проверено на наш.дом.рф» checks per object.
Issue #297, sub-task 22f. Table created in PR #303 (data/sql/111_22f_domrf_obj_checks.sql).
Check types (canonical):
no_problems / docs / timing / photos / bankruptcy / declaration
Source endpoint: /сервисы/api/object/{obj_id}/checks
(pattern analogичен /infrastructure и /photos — те же сервисы/api/object/{obj_id}/ prefix)
URL not verified via devtools — структура payload выведена из аудита страницы ЖК.
Если endpoint не существует — scraper получит HTTP-ошибку, которую _classify_and_log
запишет в kn_scrape_failures, данные в domrf_obj_checks не поступят, scrape не упадёт.
Expected payload shape (предположительно):
{
"data": {
"noProblemObjects": true, # no_problems
"hasDocuments": true, # docs
"meetsDeadlines": true, # timing
"hasPhotos": true, # photos
"notBankrupt": true, # bankruptcy
"hasDeclaration": true # declaration
}
}
OR possibly an array:
[{"checkType": "no_problems", "passed": true}, ...]
Поддерживаются оба варианта: dict-payload (поля маппятся через CHECK_FIELD_MAP)
и list-payload (поля check_type + passed/value).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy import text
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
CHECK_TYPES = ["no_problems", "docs", "timing", "photos", "bankruptcy", "declaration"]
# Mapping of possible API field names → canonical check_type.
# Best-guess from DOM.РФ API field naming conventions (кейс camelCase).
_CHECK_FIELD_MAP: dict[str, str] = {
"noProblemObjects": "no_problems",
"noProblemFlg": "no_problems",
"hasDocuments": "docs",
"documentsFlg": "docs",
"meetsDeadlines": "timing",
"deadlinesFlg": "timing",
"hasPhotos": "photos",
"photosFlg": "photos",
"notBankrupt": "bankruptcy",
"bankruptcyFlg": "bankruptcy",
"hasDeclaration": "declaration",
"declarationFlg": "declaration",
}
# Canonical check_type name → possible API field name aliases
_CHECK_TYPE_ALIASES: dict[str, list[str]] = {
"no_problems": ["no_problems", "noProblemObjects", "noProblemFlg"],
"docs": ["docs", "hasDocuments", "documentsFlg"],
"timing": ["timing", "meetsDeadlines", "deadlinesFlg"],
"photos": ["photos", "hasPhotos", "photosFlg"],
"bankruptcy": ["bankruptcy", "notBankrupt", "bankruptcyFlg"],
"declaration": ["declaration", "hasDeclaration", "declarationFlg"],
}
# Строковые флаги, которые сторонний API может прислать вместо bool.
# Схема payload не верифицирована (см. docstring), поэтому приводим явно.
_TRUE_STRINGS = {"true", "1", "yes", "y", "да", "passed", "ok"}
_FALSE_STRINGS = {"false", "0", "no", "n", "нет", "failed", "not_passed"}
def _coerce_flag(value: Any) -> bool | None:
"""Привести значение флага проверки к bool либо None (UNKNOWN).
bool(value) ломается на строках ('false'/'0'/'нет' → True) и не отличает
отсутствие данных от False. Возвращаем None, если значение нераспознаваемо —
вызывающий код НЕ должен фабриковать False для UNKNOWN.
"""
if isinstance(value, bool):
return value
if value is None:
return None
if isinstance(value, (int, float)):
return bool(value)
if isinstance(value, str):
s = value.strip().lower()
if s in _TRUE_STRINGS:
return True
if s in _FALSE_STRINGS:
return False
return None
return None
_UPSERT_CHECKS_SQL = text(
"""
INSERT INTO domrf_obj_checks (obj_id, check_type, passed, checked_at, scraped_at)
VALUES (:obj_id, :check_type, :passed, NOW(), NOW())
ON CONFLICT (obj_id, check_type) DO UPDATE SET
passed = EXCLUDED.passed,
checked_at = NOW(),
scraped_at = NOW()
"""
)
def extract_obj_checks(raw_payload: Any) -> list[dict[str, Any]]:
"""Извлечь 6 чекбоксов из payload endpoint /object/{obj_id}/checks.
Поддерживает два варианта payload:
1. dict с полями (ожидаемый API-формат): {"data": {"noProblemObjects": true, ...}}
2. list объектов: [{"checkType": "no_problems", "passed": true}, ...]
Для неизвестных полей и неподдерживаемых форматов — WARNING + пустой список.
"""
if not raw_payload:
return []
# Вариант 1: dict с data-обёрткой
data: Any = raw_payload
if isinstance(raw_payload, dict):
data = raw_payload.get("data") or raw_payload
results: list[dict[str, Any]] = []
if isinstance(data, dict):
# Map известных полей к canonical check_type
found: dict[str, bool] = {}
for field, value in data.items():
ct = _CHECK_FIELD_MAP.get(field)
if ct and ct not in found:
flag = _coerce_flag(value)
if flag is not None:
found[ct] = flag
# Также проверить canonical names напрямую
for ct in CHECK_TYPES:
if ct not in found and ct in data:
flag = _coerce_flag(data[ct])
if flag is not None:
found[ct] = flag
if found:
# Только фактически найденные флаги: отсутствие в payload = UNKNOWN,
# а не FAILED — не фабрикуем passed=False для непришедших проверок.
for ct in CHECK_TYPES:
if ct in found:
results.append({"check_type": ct, "passed": found[ct]})
return results
# dict не содержит известных полей — попробуем как list-формат ниже
logger.warning(
"domrf obj_checks: dict payload has no known check fields: %s", list(data)[:10]
)
return []
if isinstance(data, list):
found_list: dict[str, bool] = {}
for item in data:
if not isinstance(item, dict):
continue
ct_raw = item.get("checkType") or item.get("check_type") or item.get("type")
if ct_raw and str(ct_raw) in CHECK_TYPES:
# Не or-коалесинг: легитимный False теряется (False or 'n/a' → 'n/a').
# Берём первый ключ, который реально присутствует в item.
if "passed" in item:
passed_raw = item["passed"]
elif "value" in item:
passed_raw = item["value"]
elif "status" in item:
passed_raw = item["status"]
else:
passed_raw = None
flag = _coerce_flag(passed_raw)
if flag is not None:
found_list[str(ct_raw)] = flag
if found_list:
# См. dict-ветку: только найденные флаги, UNKNOWN не равно FAILED.
for ct in CHECK_TYPES:
if ct in found_list:
results.append({"check_type": ct, "passed": found_list[ct]})
return results
logger.warning(
"domrf obj_checks: list payload has no recognisable check items: %s", data[:3]
)
return []
logger.warning("domrf obj_checks: unexpected payload type %s", type(raw_payload))
return []
def upsert_obj_checks(db: Session, obj_id: int, checks: list[dict[str, Any]]) -> int:
"""UPSERT 6 чек-строк в domrf_obj_checks. Returns count of inserted/updated rows.
Использует SAVEPOINT (begin_nested) per-row — одна битая строка не откатывает
всю транзакцию.
"""
if not checks:
return 0
ok = 0
for c in checks:
try:
with db.begin_nested():
db.execute(
_UPSERT_CHECKS_SQL,
{
"obj_id": obj_id,
"check_type": c["check_type"],
"passed": c["passed"],
},
)
ok += 1
except Exception as exc:
logger.warning(
"upsert obj_checks obj=%s check_type=%s failed: %s",
obj_id,
c.get("check_type"),
exc,
)
db.commit()
return ok