arm-tabular-parser/api/tests/result_contract.py
Andrey Epifancev e705d75520 feat(parser): контрольные суммы §1.4, статусы, OsvParser как парсер по умолчанию, лимиты
checks.py: тождество сальдо по каждой строке, суммы родителя = Σ детей того же data_type
(CURRENCY/QUANTITY исключены; обороты — по колонкам, сальдо — по колонкам или по нетто при
свёрнутом сальдо родителя), «Итого» = корневой счёт (R-13: нет корня — «Итого» пусто/0;
R-20: нет «Итого» — false и предупреждение).
osv.py: OsvParser — лимит размера, чтение по сигнатуре, листы по содержимому (формы ББ/ОФР и
прочие — пропуск с предупреждением), карта исходов D-6, кооперативный дедлайн, metrics
(rows без TOTAL, warnings с «sheet=<имя> hierarchy=<стратегия>» первой записью по листу).
get_parser(settings, rules_dir=…) → OsvParser (словари грузятся при создании: битый словарь —
падение старта); PARSE_MAX_BYTES / PARSE_TIMEOUT_SECONDS в Settings и .env.*; app.py передаёт
настройки. test_null_parser.py → test_parser_factory.py.
Тесты: юнит-карта статусов, «Испорченная сумма», «Пустая ОСВ», два листа ОСВ, новый синоним
без релиза кода (R-8); property-тест на 66 реальных файлах (все done, < 5 с, схема v1.1).
test_app и интеграционный round-trip ждут ошибку реального парсера вместо NOT_IMPLEMENTED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 14:37:21 +04:00

69 lines
3.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.

"""Исход разбора → файл результата контракта 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