- test_osv_dkb.py: на каждом из 25 файлов пакета ДКБ — статусы листов по манифесту osv_dkb_expectations.yaml (данные, не if по имени файла), период по каталогу (2025-Q3, 2024-10_2025-09), структура строк, контракт, время разбора. - golden: osv-dkb/** до порога GOLDEN_MAX_BYTES = 1 МиБ (22 из 25 файлов; три файла Агрокомплекса 9–14 МБ — без снимков); golden_update, golden_helpers (подкаталог osv-dkb/), conftest (dkb_file, dkb_golden_file), хелперы. - dkb_helpers: expected_period, манифест; юнит-тесты без каталога fixtures. Stacked on #15. Closes #10 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
93 lines
4.3 KiB
Python
93 lines
4.3 KiB
Python
"""Ожидания для пакета ДКБ `fixtures/osv-dkb/**`: период каталога и манифест ожидаемых нарушений контрольных сумм.
|
||
|
||
Каталог пакета назван по периоду шапки документа (не по имени файла): `2025-Q3` — «9 месяцев 2025 г.»,
|
||
`2024-10_2025-09` — скользящие 12 месяцев «Октябрь 2024 г. – Сентябрь 2025 г.». Ожидаемые статусы — данные
|
||
(`osv_dkb_expectations.yaml`), а не условия по имени файла: всё, чего в манифесте нет, должно быть `done` без
|
||
нарушений §1.4.
|
||
"""
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
from functools import cache
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
|
||
from src.contract import OsvDocument
|
||
from src.contract.result import OsvChecks
|
||
|
||
__all__ = [
|
||
"CheckKind",
|
||
"ExpectedPeriod",
|
||
"expected_period",
|
||
"expected_violations",
|
||
"load_expectations",
|
||
"violated_kinds",
|
||
]
|
||
|
||
CheckKind = str
|
||
EXPECTATIONS_FILE = Path(__file__).with_name("osv_dkb_expectations.yaml")
|
||
QUARTER_DIR = re.compile(r"^(?P<year>\d{4})-(?P<quarter>Q[1-4])$")
|
||
RANGE_DIR = re.compile(r"^(?P<start_year>\d{4})-(?P<start_month>\d{2})_(?P<end_year>\d{4})-(?P<end_month>\d{2})$")
|
||
MONTHS_IN_YEAR = 12
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ExpectedPeriod:
|
||
"""Период документа по каталогу: год и квартал (`None` — не определён), скользящий ли это диапазон."""
|
||
|
||
year: int
|
||
quarter: str | None
|
||
rolling: bool = False
|
||
|
||
|
||
def expected_period(directory_name: str) -> ExpectedPeriod:
|
||
"""`2025-Q3` -> (2025, Q3); `2024-10_2025-09` -> год конца 2025, квартал не определён, скользящий период
|
||
(не с начала года или не ровно квартал: парсер не выдаёт его ни за квартальный, ни за нарастающий)."""
|
||
quarter = QUARTER_DIR.fullmatch(directory_name)
|
||
if quarter is not None:
|
||
return ExpectedPeriod(int(quarter.group("year")), quarter.group("quarter"))
|
||
span = RANGE_DIR.fullmatch(directory_name)
|
||
if span is None:
|
||
raise ValueError(f"имя каталога периода не распознано: {directory_name!r}")
|
||
months = (
|
||
(int(span.group("end_year")) - int(span.group("start_year"))) * MONTHS_IN_YEAR
|
||
+ int(span.group("end_month"))
|
||
- int(span.group("start_month"))
|
||
+ 1
|
||
)
|
||
if months <= 0:
|
||
raise ValueError(f"конец периода раньше начала: {directory_name!r}")
|
||
return ExpectedPeriod(int(span.group("end_year")), None, rolling=True)
|
||
|
||
|
||
@cache
|
||
def load_expectations() -> dict[str, dict[str, list[str]]]:
|
||
"""`{путь файла относительно osv-dkb: {locator: [виды нарушений]}}` из манифеста; у каждой записи есть причина."""
|
||
raw = yaml.safe_load(EXPECTATIONS_FILE.read_text(encoding="utf-8")) or {}
|
||
found: dict[str, dict[str, list[str]]] = {}
|
||
for path, entry in (raw.get("needs_check") or {}).items():
|
||
if not entry.get("reason"):
|
||
raise ValueError(f"{EXPECTATIONS_FILE.name}: у {path!r} нет причины (`reason`)")
|
||
found[path] = {locator: sorted(kinds) for locator, kinds in entry["sheets"].items()}
|
||
return found
|
||
|
||
|
||
def expected_violations(relative_path: str, locator: str) -> list[CheckKind]:
|
||
"""Ожидаемые виды нарушений листа; пусто — лист должен сойтись."""
|
||
return load_expectations().get(relative_path, {}).get(locator, [])
|
||
|
||
|
||
def violated_kinds(document: OsvDocument) -> list[CheckKind]:
|
||
"""Виды нарушений листа: `total_row` («Итого» ≠ корень), `balance_identity`, `parent_sum` (отсортированы)."""
|
||
checks: OsvChecks | None = document.checks
|
||
assert checks is not None, document.locator
|
||
kinds: list[CheckKind] = []
|
||
if not checks.total_row_matches_root:
|
||
kinds.append("total_row")
|
||
if checks.balance_identity_violations:
|
||
kinds.append("balance_identity")
|
||
if checks.parent_sum_violations:
|
||
kinds.append("parent_sum")
|
||
return sorted(kinds)
|