arm-tabular-parser/api/tests/result_contract.py
Andrey Epifancev 71f596cdb7 feat: формы через arm_statements, контракт результата и конверта v2.0, статус и OSV_* у ОСВ (#54)
- копия схем и примеров контракта v2.0 (запрос остаётся v1.0), модели result/common/message/request;
  ResultWarning/FileWarning, статус и предупреждения у документов, строки forms v2.0, service.statements_version
- statements/pipeline.py: кандидаты адаптера -> normalize_candidate -> to_contract / warning_to_contract
- BookParser: статус книги через envelope_status, SHEET_OTHER / FILE_TYPE_MISMATCH / XLS_NO_CODEPAGE как предупреждения
  файла, OSV_* и OSV_XLSB_NO_OUTLINE в документах ОСВ, тексты иерархии по спеке
- воркер, app и CLI получают statements_version из require_statements_version()
- старые модели документа формы вынесены в statements/legacy_models.py до удаления старого конвейера (задача 9)
- OpenSpec: MODIFIED «Парсер — заменяемый интерфейс» в дельте tabular-recognition-service

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 20:52:52 +04:00

69 lines
3.0 KiB
Python
Raw Permalink 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.

"""Исход разбора → файл результата контракта v2.0 (как его собирает воркер) и проверка JSON-схемой `api/contracts/`.
Общий помощник юнит-тестов парсера и property-теста на реальных файлах: `RecognitionResult.model_validate` и
`recognition-result.schema.json` (Draft 2020-12, `$ref` — локальный реестр без сети) — оба вердикта обязательны.
"""
import json
import uuid
from pathlib import Path
from typing import Any
from jsonschema import Draft202012Validator, FormatChecker
from referencing import Registry, Resource
from src.contract import CONTRACT_VERSION, RecognitionResult
from src.parser import ParseOutcome
CONTRACTS_DIR = Path(__file__).resolve().parents[1] / "contracts"
SCHEMA_FILES = (
"recognition-request.schema.json",
"recognition-result-message.schema.json",
"recognition-result.schema.json",
)
RESULT_SCHEMA = "recognition-result.schema.json"
def _validator() -> Draft202012Validator:
schemas = {name: json.loads((CONTRACTS_DIR / name).read_text(encoding="utf-8")) for name in SCHEMA_FILES}
registry: Registry = Registry()
for schema in schemas.values():
registry = registry.with_resource(schema["$id"], Resource.from_contents(schema))
return Draft202012Validator(schemas[RESULT_SCHEMA], registry=registry, format_checker=FormatChecker())
RESULT_VALIDATOR = _validator()
def result_payload(outcome: ParseOutcome) -> dict[str, Any]:
"""Файл результата из успешного исхода (`done | needs_check`): модель контракта → JSON-словарь."""
result = RecognitionResult.model_validate(
{
"schema_version": CONTRACT_VERSION,
"job_id": uuid.uuid4(),
"file_id": uuid.uuid4(),
"service": {"name": "tabular-parser", "version": "test", "statements_version": "test"},
"status": outcome.status,
"metrics": outcome.metrics,
"documents": list(outcome.documents),
}
)
payload = result.to_wire()
# Круг через текст JSON: в схему идёт то, что реально ляжет в S3
loaded: dict[str, Any] = json.loads(json.dumps(payload, ensure_ascii=False))
return loaded
def schema_errors(payload: dict[str, Any]) -> list[str]:
"""Нарушения JSON-схемы v2.0 (пустой список — файл валиден)."""
return [f"{list(error.absolute_path)}: {error.message}" for error in RESULT_VALIDATOR.iter_errors(payload)]
def assert_valid_result(outcome: ParseOutcome) -> dict[str, Any]:
"""Файл результата проходит и pydantic-модель (с round-trip), и JSON-схему; возвращает JSON-словарь."""
payload = result_payload(outcome)
errors = schema_errors(payload)
assert errors == [], "\n".join(errors[:20])
assert RecognitionResult.model_validate(payload).to_wire() == payload
return payload