124 lines
5.0 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.

"""Обход набора json ИС: документы по таблицам, предупреждения файла, статус и код ошибки набора.
Спека scan-gateway «Ошибки набора json», statement-normalization «Шаг 8 — статус документа и конверта».
Решения Р-32/Р-33: json без таблиц и «все json ошибочны» — `JSON_FAILED` на каждый файл; код и сообщение ошибки
набора — от первого ошибочного файла.
"""
from collections.abc import Mapping
from dataclasses import dataclass
from arm_statements import (
EnvelopeStatus,
Rules,
StatementDocument,
StatementWarning,
WarningCode,
envelope_status,
make_warning,
normalize_candidate,
)
from src.contract import ErrorCode
from src.normalizer.files import NO_FILES_MESSAGE, NOT_MERGED_MESSAGE, FileError, file_error
from src.normalizer.ocr.adapter import header_cells, table_candidate
__all__ = ["NO_TABLES_MESSAGE", "AnswerSetResult", "SetError", "normalize_answer_set"]
NO_TABLES_MESSAGE = "ни одной таблицы не разобрано"
NO_DATA_OCR_MESSAGE = "ответ ИС без data_ocr"
NO_TABLE_STRUCTURE_MESSAGE = "ответ ИС без table_structure"
EMPTY_TABLES_MESSAGE = "в ответе ИС нет таблиц"
EXCEPTION_TEXT_LIMIT = 300 # P5-9
@dataclass(frozen=True)
class SetError:
code: ErrorCode
message: str
@dataclass(frozen=True)
class AnswerSetResult:
"""`error is not None` тогда и только тогда, когда `status is ERROR`."""
status: EnvelopeStatus
documents: tuple[StatementDocument, ...]
file_warnings: tuple[StatementWarning, ...]
error: SetError | None
def _set_error(code: ErrorCode, message: str) -> AnswerSetResult:
return AnswerSetResult(EnvelopeStatus.ERROR, (), (), SetError(code, message))
def normalize_answer_set(raw: object, rules: Rules) -> AnswerSetResult:
if not isinstance(raw, dict) or not isinstance(raw.get("files"), list):
return _set_error(ErrorCode.INVALID_FORMAT, NOT_MERGED_MESSAGE)
files: list[object] = raw["files"]
if not files:
return _set_error(ErrorCode.INVALID_FORMAT, NO_FILES_MESSAGE)
documents: list[StatementDocument] = []
warnings: list[StatementWarning] = []
first_error: FileError | None = None
succeeded = 0
for index, entry in enumerate(files):
error = file_error(index, entry)
if error is not None:
warnings.append(error.to_warning(rules))
first_error = first_error or error
continue
succeeded += 1
assert isinstance(entry, dict) # проверено file_error
_normalize_file(entry, rules, documents, warnings)
status = envelope_status([d.status for d in documents], warnings)
if status is not EnvelopeStatus.ERROR:
return AnswerSetResult(status, tuple(documents), tuple(warnings), None)
if first_error is not None and not succeeded: # Р-33: код первого ошибочного, только когда все json ошибочны
error_info = SetError(first_error.code, first_error.summary)
else:
error_info = SetError(ErrorCode.RECOGNITION_FAILED, NO_TABLES_MESSAGE)
return AnswerSetResult(status, (), tuple(warnings), error_info)
def _json_failed(name: str, message: str, rules: Rules) -> StatementWarning:
return make_warning(WarningCode.JSON_FAILED, message, rules, locator=f"file:{name}")
def _normalize_file(
entry: Mapping[str, object], rules: Rules, documents: list[StatementDocument], warnings: list[StatementWarning]
) -> None:
name = str(entry["name"])
response = entry["response"]
assert isinstance(response, dict) # проверено file_error
data_ocr = response.get("data_ocr")
if not isinstance(data_ocr, dict):
warnings.append(_json_failed(name, NO_DATA_OCR_MESSAGE, rules))
return
tables = data_ocr.get("table_structure")
if not isinstance(tables, list):
warnings.append(_json_failed(name, NO_TABLE_STRUCTURE_MESSAGE, rules))
return
if not tables:
warnings.append(_json_failed(name, EMPTY_TABLES_MESSAGE, rules))
return
header = header_cells(data_ocr)
for i, table in enumerate(tables):
try:
result = normalize_candidate(table_candidate(name, i, table, header, rules), rules)
except Exception as exc: # noqa: BLE001 -- изоляция разбора одной таблицы (P5-9)
text = f"{type(exc).__name__}: {str(exc)[:EXCEPTION_TEXT_LIMIT]}"
warnings.append(
make_warning(
WarningCode.TABLE_PARSE_ERROR,
f"BACKEND_PARSING_ERROR: {text}",
rules,
locator=f"file:{name};table:{i}",
)
)
continue
if result.document is not None:
documents.append(result.document)
warnings.extend(result.file_warnings)