Andrey Epifancev c1288d08fb fix(normalizer): итоги финального ревью — Money, статус при ошибочном файле, пропущенные строки, длина дат, дубли yaml
- C-1 (№30): сумма с более чем двумя знаками после запятой не принимается (amount=null, string_value, is_recognized=false);
  confidence ячеек и шапки приводится к [0, 100], NaN и бесконечность дают 0
- F-A (№28): ошибочный файл набора при успешном понижает статус до needs_check
- F-B: «ИНН не распознан» только когда ИНН не извлечён, иначе «ИНН распознан неуверенно»
- №29: предупреждения «строк с нечисловым кодом пропущено: N» и «строк без кода с суммами пропущено: N»
- I-3 (№31): тексты дат длиннее 200 символов не разбираются и не проходят fix_lookalikes
- T1.3 (№33): загрузчик yaml отклоняет повторяющиеся ключи; тавтологичный тест дублей заменён настоящим
- гигиена: докстринг amounts.py описывает фактическое поведение (все точки удаляются), мёртвые noqa в ocr/__init__.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 03:35:10 +04:00

100 lines
4.1 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.

"""Шапка документа из `data_ocr` (§11.1 п. 8) на реальных `aep_3.json` (мусор) и `aep_6.json` (чисто)."""
import json
import time
from datetime import date
from pathlib import Path
import pytest
from src.normalizer.ocr.header import FileHeader, parse_file_header
API_DIR = Path(__file__).resolve().parents[3]
REAL_FIXTURES = API_DIR.parent.parent / "fixtures" / "ocr"
LOOKALIKES = {"О": "0", "о": "0", "З": "3", "l": "1", "I": "1", "|": "1", "б": "6", "S": "5", "Ч": "4"}
def _real(name: str) -> dict:
if not REAL_FIXTURES.is_dir():
pytest.skip("fixtures недоступен: положите репозиторий fixtures рядом с workspace")
return dict(json.loads((REAL_FIXTURES / name).read_text(encoding="utf-8"))["data_ocr"])
def _field(text: str, confidence: float) -> dict:
return {"form_name": text, "confidence": confidence}
def test_clean_header() -> None:
header = parse_file_header(
{
"organization": _field("ООО Ромашка", 97.5),
"inn": _field("7700000000", 100),
"units": _field("тыс. руб.", 100),
"date": _field("на 31 декабря 2024 г.", 100),
},
LOOKALIKES,
)
assert header == FileHeader(
"ООО Ромашка",
97.5,
"7700000000",
100.0,
"тыс. руб.",
"на 31 декабря 2024 г.",
date(2024, 12, 31),
True,
)
def test_noisy_inn_caps_confidence() -> None:
header = parse_file_header({"inn": _field("3|1 21 2| о! О' 0 0; 3: 51", 100)}, LOOKALIKES)
assert header.inn == "3121200351"
assert header.inn_confidence == 99.0
assert header.organization_name is None and header.organization_confidence is None
@pytest.mark.parametrize(("raw", "expected"), [(float("nan"), 0.0), (150, 100.0), (-5, 0.0), (float("inf"), 0.0)])
def test_header_confidence_is_clamped(raw: float, expected: float) -> None:
"""Решение №30: уверенность полей шапки — в [0, 100], NaN → 0 (контракт требует 0..100)."""
header = parse_file_header(
{"organization": _field("ООО Ромашка", raw), "inn": _field("7700000000", raw), "date": _field("2024", raw)},
LOOKALIKES,
)
assert header.organization_confidence == expected and header.inn_confidence == expected
assert header.report_date_recognized is (expected == 100.0)
@pytest.mark.parametrize(
"raw", ["1" + " " * 4000 + "b", "1" + "\n" * 4000, "1" + "О" * 5000], ids=["spaces", "nl", "look"]
)
def test_pathological_report_date_is_fast(raw: str) -> None:
"""Решение №31: длинный текст даты не разбирается и не проходит `fix_lookalikes` (иначе — секунды и минуты)."""
start = time.perf_counter()
header = parse_file_header({"date": _field(raw, 100)}, LOOKALIKES)
assert time.perf_counter() - start < 1.0
assert header.report_date is None and header.report_date_recognized is False
assert header.report_date_text == raw.strip() # исходный текст не теряется
def test_missing_fields() -> None:
header = parse_file_header({}, LOOKALIKES)
assert header == FileHeader(None, None, None, None, None, None, None, False)
@pytest.mark.fixtures
def test_aep6_header() -> None:
header = parse_file_header(_real("aep_6.json"), LOOKALIKES)
assert header.inn is None # текст «+»
assert header.units == "тыс. руб."
assert header.report_date is None # в поле date — название организации
assert header.report_date_text is not None and header.report_date_recognized is False
assert header.organization_name is not None and "Дорстроймонтаж" in header.organization_name
@pytest.mark.fixtures
def test_aep3_header() -> None:
header = parse_file_header(_real("aep_3.json"), LOOKALIKES)
assert header.inn == "3121200351" and header.inn_confidence is not None and header.inn_confidence < 100
assert header.report_date == date(2024, 12, 31)
assert header.report_date_recognized is False # confidence 75