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