"""Словарь имён строк форм ББ/ОФР по редакциям — состав формы для экрана проверки (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