Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
97 lines
4.8 KiB
Python
97 lines
4.8 KiB
Python
"""Словарь имён строк форм ББ/ОФР по редакциям — состав формы для экрана проверки (05-domain-rules §7 «Ядро»;
|
||
change statement-template-lines, design D-2) и справочные имена для совпадения наименований (§11.9, Р-20).
|
||
|
||
Данные — `api/rules/statement_lines.yaml`; порядок строк в списке — порядок формы. Код в редакции может стоять
|
||
несколько раз (синонимы и законные повторы, Р-20): состав формы берёт первое имя, сравнение наименований — любое.
|
||
Редакцию выбирает `statement_mapping.edition_for`; здесь — только состав. Справочник ничего не убирает и не
|
||
переименовывает в форме: строки источника, которых в нём нет, остаются как есть.
|
||
"""
|
||
|
||
import functools
|
||
import re
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
|
||
__all__ = ["RULES_PATH", "LineCatalog", "TemplateLine", "load_line_catalog"]
|
||
|
||
RULES_PATH = Path(__file__).resolve().parents[2] / "rules" / "statement_lines.yaml"
|
||
FORMS = ("BALANCE", "INCOME")
|
||
_CODE = re.compile(r"^\d{4}$")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class TemplateLine:
|
||
code: str
|
||
name: str
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class LineCatalog:
|
||
"""`editions[редакция][форма]` — все записи словаря в порядке формы, повтор кода допустим."""
|
||
|
||
editions: dict[str, dict[str, tuple[TemplateLine, ...]]]
|
||
|
||
def _entries(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
|
||
if edition is None:
|
||
return ()
|
||
return self.editions.get(edition, {}).get(form, ())
|
||
|
||
def template_lines(self, edition: str | None, form: str) -> tuple[TemplateLine, ...]:
|
||
"""Строки формы в порядке формы: по одной на код, первое имя, порядок первого появления.
|
||
Без редакции или для чужой формы — пусто."""
|
||
seen: set[str] = set()
|
||
result: list[TemplateLine] = []
|
||
for line in self._entries(edition, form):
|
||
if line.code not in seen:
|
||
seen.add(line.code)
|
||
result.append(line)
|
||
return tuple(result)
|
||
|
||
def reference_name(self, edition: str | None, form: str, code: str) -> str | None:
|
||
"""Первое имя кода в редакции (P3-11); кода нет — `None`."""
|
||
for line in self._entries(edition, form):
|
||
if line.code == code:
|
||
return line.name
|
||
return None
|
||
|
||
def names(self, form: str, code: str) -> tuple[str, ...]:
|
||
"""Все имена кода во всех редакциях без повторов, в порядке появления."""
|
||
result: dict[str, None] = {}
|
||
for forms in self.editions.values():
|
||
for line in forms.get(form, ()):
|
||
if line.code == code:
|
||
result.setdefault(line.name)
|
||
return tuple(result)
|
||
|
||
def edition_codes(self, edition: str, form: str) -> frozenset[str]:
|
||
"""Коды формы в редакции."""
|
||
return frozenset(line.code for line in self._entries(edition, form))
|
||
|
||
|
||
def _form_lines(edition: str, form: str, items: object) -> tuple[TemplateLine, ...]:
|
||
if not isinstance(items, list):
|
||
raise ValueError(f"редакция {edition}: нет списка строк формы {form}")
|
||
lines = tuple(TemplateLine(str(item["code"]), " ".join(str(item["name"]).split())) for item in items)
|
||
for line in lines:
|
||
if not _CODE.fullmatch(line.code):
|
||
raise ValueError(f"редакция {edition}, форма {form}: код {line.code!r} — не четыре цифры")
|
||
return lines
|
||
|
||
|
||
@functools.lru_cache(maxsize=4)
|
||
def load_line_catalog(path: Path | None = None) -> LineCatalog:
|
||
"""Справочник из yaml; любая ошибка формата — `ValueError` с именем файла."""
|
||
source = path or RULES_PATH
|
||
try:
|
||
data = yaml.safe_load(source.read_text(encoding="utf-8"))
|
||
editions: dict[str, dict[str, tuple[TemplateLine, ...]]] = {}
|
||
for edition in data["editions"]:
|
||
edition_id = str(edition["id"])
|
||
forms = edition["forms"]
|
||
editions[edition_id] = {form: _form_lines(edition_id, form, forms.get(form)) for form in FORMS}
|
||
return LineCatalog(editions=editions)
|
||
except (OSError, KeyError, TypeError, AttributeError, ValueError, yaml.YAMLError) as exc:
|
||
raise ValueError(f"справочник строк {source.name}: {exc}") from exc
|