"""Conceptual topic catalogue for ``aeat app registry citations``.A tax-naive operator hitting the CLI for the first time needsplain-language explanations of concepts (``iva-regime``, ``casilla``,``pago-fraccionado`` …) without having to leave the terminal. TheCLI exposes:- ``aeat app registry citations`` -> list every registered slug + one-line summary.- ``aeat app registry citations <slug>`` -> render the topic body + see_also pointers + legal references.Topics live as TOML files under ``registry/aeat/topics/<slug>.toml``;title and body text live in the i18n catalogue under ``topic.<slug>.*``so translations follow the project's locale pipeline rather thanhardcoded multiline strings.The :class:`Topic` records are core-level resources: they depend onlyon core primitives and the bundled registry path. They are loaded intoa :class:`TopicCatalogue` by :func:`load_topic_catalogue` and consumedthrough the:class:`core.resources._repos.topics.TopicCatalogueRepository`singleton, keeping ``core`` free of any import into the applicationlayer."""from__future__importannotationsimporttomllibfromfunctoolsimportlru_cachefrompathlibimportPathfrompydanticimportBaseModel,Fieldfrom..importSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom..errorsimportAeatErroras_AeatErrorfrom..external_constantsimportUTF_8_ENCODINGas_UTF_8_ENCODINGfrom..pathsimportfile_stat_fingerprintas_file_stat_fingerprintfrom..resourcesimportbundled_pathas_bundled_path_TOPIC_REGISTRY_ROOT=_bundled_path("registry","aeat","topics")
[docs]classTopicNotFoundError(_AeatError):"""Raised when a requested slug is absent from a :class:`TopicCatalogue`."""
[docs]classTopic(BaseModel):"""One conceptual topic. Attributes: slug: Stable kebab-case identifier (``iva-regime``). title_key: i18n key resolving to the topic's human-readable title. Convention: ``topic.<slug>.title``. body_key: i18n key resolving to the topic body. Convention: ``topic.<slug>.body``. see_also: Slugs of related topics for cross-referencing. legal_refs: Stable corpus references (``ley-58-2003:art-27.2``, ``rd-439-2007:art-110``) the topic anchors against. """model_config=_STRICT_FROZENslug:str=Field(min_length=1,max_length=64,pattern=r"^[a-z][a-z0-9\-]*$")title_key:str=Field(min_length=1,max_length=128)body_key:str=Field(min_length=1,max_length=128)see_also:tuple[str,...]=Field(default=())legal_refs:tuple[str,...]=Field(min_length=1)
[docs]classTopicCatalogue(BaseModel):"""Closed catalogue of registered :class:`Topic` records."""model_config=_STRICT_FROZENtopics:tuple[Topic,...]=Field(min_length=1)
[docs]deftopic(self,slug:str)->Topic:"""Return the :class:`Topic` for ``slug`` or raise. Args: slug: Kebab-case topic identifier. Returns: The matching :class:`Topic`. Raises: TopicNotFoundError: When ``slug`` is not in the catalogue. """fortopicinself.topics:iftopic.slug==slug:returntopicraiseTopicNotFoundError(f"topic not found: {slug!r}")
[docs]defslugs(self)->tuple[str,...]:"""Return every registered slug sorted alphabetically."""returntuple(sorted(topic.slugfortopicinself.topics))
[docs]defload_topic_catalogue(root:Path|None=None)->TopicCatalogue:"""Load every ``registry/aeat/topics/<slug>.toml`` into one catalogue. Args: root: Override directory (defaults to the canonical project registry path). Returns: A :class:`TopicCatalogue` carrying one :class:`Topic` per TOML. """target=rootifrootisnotNoneelse_TOPIC_REGISTRY_ROOTresolved=target.resolve()paths=tuple(sorted(resolved.glob("*.toml")))fingerprint=tuple(_file_stat_fingerprint(path)forpathinpaths)return_load_topic_catalogue_cached(str(resolved),fingerprint)
@lru_cache(maxsize=16)def_load_topic_catalogue_cached(root:str,fingerprint:tuple[tuple[str,int,int],...],)->TopicCatalogue:target=Path(root)topics:list[Topic]=[]forfilename,_byte_count,_modified_nsinfingerprint:path=target/filenameraw=tomllib.loads(path.read_text(encoding=_UTF_8_ENCODING))slug=str(raw.get("slug")orpath.stem)topics.append(Topic(slug=slug,title_key=str(raw.get("title_key")orf"topic.{slug}.title"),body_key=str(raw.get("body_key")orf"topic.{slug}.body"),see_also=tuple(str(item)foriteminraw.get("see_also",())),legal_refs=tuple(str(item)foriteminraw.get("legal_refs",())),),)ifnottopics:raiseTopicNotFoundError(f"topic catalogue at {target} is empty")returnTopicCatalogue(topics=tuple(topics))__all__=["Topic","TopicCatalogue","TopicNotFoundError","load_topic_catalogue",]