"""Исход разбора → файл результата контракта v1.1 (как его собирает воркер) и проверка 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"}, "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-схемы v1.1 (пустой список — файл валиден).""" 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