Словари api/rules/{osv_headers,osv_groups,periods,data_types}.yaml и src/parser/rules.py
(normalize_text, Pattern, load_rules, pydantic-валидация с RuntimeError по файлу и ключу,
currency_aliases по R-6). AST-линтер координат tests/unit/parser/test_no_coordinates.py
перенесён из Task 12 (R-7, правила R-4/R-5). Тест формулировок реальных ОСВ.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
64 lines
3.0 KiB
Python
64 lines
3.0 KiB
Python
"""Словари на реальных ОСВ из ../fixtures: формулировки шапки, блоков сумм, группировок и периодов покрыты.
|
||
|
||
Проверяются сами формулировки файлов (текст ячеек шапки), а не позиции: детекторы Task 4–5 строятся поверх этих же
|
||
словарей. Фикстуры — регрессия формулировок, не оракул формата.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
from typing import cast
|
||
|
||
import pytest
|
||
|
||
from src.contract import FileType
|
||
from src.parser.grid import Sheet
|
||
from src.parser.readers import read_book
|
||
from src.parser.rules import Pattern, Rules, first_match, load_rules
|
||
|
||
HEAD_ROWS = 30 # шапка и заголовок таблицы — в начале листа
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def rules() -> Rules:
|
||
return load_rules()
|
||
|
||
|
||
def _head_texts(sheet: Sheet) -> list[str]:
|
||
last = min(sheet.n_rows, HEAD_ROWS)
|
||
return [cell.text for row in range(1, last + 1) for _, cell in sheet.row_cells(row) if cell.kind == "text"]
|
||
|
||
|
||
def _after_colon(text: str) -> str:
|
||
return text.split(":", 1)[1] if ":" in text else text
|
||
|
||
|
||
@pytest.mark.fixtures
|
||
def test_real_osv_phrases_are_covered_by_dictionaries(osv_file: Path, rules: Rules) -> None:
|
||
sheets, _ = read_book(osv_file.read_bytes(), cast(FileType, osv_file.suffix.lstrip(".").upper()))
|
||
texts = [text for sheet in sheets for text in _head_texts(sheet)]
|
||
headers = rules.headers
|
||
|
||
def any_cell(patterns: list[Pattern]) -> bool:
|
||
return any(pattern.search(text) for text in texts for pattern in patterns)
|
||
|
||
assert any_cell(headers.sheet_markers), "нет заголовка ведомости"
|
||
assert not any_cell(headers.statement_markers), "ОСВ не должна выглядеть как форма ББ/ОФР"
|
||
for group in headers.groups:
|
||
assert any_cell(headers.groups[group]), f"нет блока сумм {group}"
|
||
assert any_cell(headers.debit)
|
||
assert any_cell(headers.credit)
|
||
assert any_cell(headers.header_markers["org_params"])
|
||
assert any_cell(headers.header_markers["data_type"])
|
||
|
||
title = next(text for text in texts if any(p.search(text) for p in headers.sheet_markers))
|
||
account = next(m for p in headers.header_markers["account"] if (m := p.search(title)))
|
||
assert account.string[account.end() :].strip()[:1].isdigit()
|
||
period = next(m for p in headers.header_markers["period"] if (m := p.search(title)))
|
||
period_text = period.string[period.end() :]
|
||
assert rules.periods.match(period_text) is not None, title
|
||
assert rules.periods.year.search(period_text) is not None, title
|
||
|
||
data_type_line = next(t for t in texts if any(p.search(t) for p in headers.header_markers["data_type"]))
|
||
assert first_match(rules.data_types.header, _after_colon(data_type_line)) == "BU"
|
||
currency_line = next(t for t in texts if any(p.search(t) for p in headers.header_markers["currency"]))
|
||
assert rules.data_types.currency_code(_after_colon(currency_line)) == "RUB"
|