"""Обход набора 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)