Source code for pain001.templates.registry

# Copyright (C) 2023-2026 Pain001. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Automatic discovery and metadata registry for bundled templates."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import yaml  # type: ignore[import-untyped]

from pain001.constants import TEMPLATES_DIR


@dataclass(frozen=True)
class TemplateMetadata:
    """Describes a discovered ISO 20022 template bundle."""

    message_type: str
    message_category: str
    template_path: Path
    xsd_path: Path
    example_data_path: Path | None
    example_xml_path: Path | None
    supported_input_formats: list[str]
    iso_version: str
    deprecated: bool = False


[docs] class TemplateRegistry: """Discover and query supported template bundles.""" def __init__(self, templates_dir: Path = TEMPLATES_DIR) -> None: self.templates_dir = Path(templates_dir).resolve() self._registry: dict[str, TemplateMetadata] = {} self._discover_templates() def _discover_templates(self) -> None: """Scan the templates directory and index every metadata.yaml.""" for metadata_path in sorted( self.templates_dir.glob("*/metadata.yaml") ): metadata = self._load_metadata(metadata_path) self._registry[metadata.message_type] = metadata def _load_metadata(self, metadata_path: Path) -> TemplateMetadata: """Parse one metadata.yaml into a TemplateMetadata record.""" raw_data = ( yaml.safe_load(metadata_path.read_text(encoding="utf-8")) or {} ) raw = dict(raw_data) parent = metadata_path.parent files = raw.get("files", {}) example_data = files.get("example_data") example_xml = files.get("example_xml") return TemplateMetadata( message_type=str(raw["message_type"]), message_category=str(raw["message_category"]), template_path=(parent / str(files["template"])).resolve(), xsd_path=(parent / str(files["schema"])).resolve(), example_data_path=( (parent / str(example_data)).resolve() if example_data else None ), example_xml_path=( (parent / str(example_xml)).resolve() if example_xml else None ), supported_input_formats=[ str(item) for item in raw.get("supported_input_formats", []) ], iso_version=str(raw["iso_version"]), deprecated=bool(raw.get("deprecated", False)), )
[docs] def get_template(self, message_type: str) -> TemplateMetadata: """Return metadata for a supported message type.""" try: return self._registry[message_type] except KeyError as exc: raise KeyError( f"Unsupported template '{message_type}'. " f"Known values: {', '.join(self.list_supported_versions())}" ) from exc
[docs] def list_templates(self) -> list[TemplateMetadata]: """Return every discovered template.""" return [self._registry[key] for key in self.list_supported_versions()]
[docs] def list_supported_versions(self) -> list[str]: """Return supported message types in sorted order.""" return sorted(self._registry)
[docs] def search_by_category(self, category: str) -> list[TemplateMetadata]: """Return templates whose category matches case-insensitively.""" category_lower = category.lower() return [ metadata for metadata in self.list_templates() if metadata.message_category.lower() == category_lower ]
[docs] def resolve_paths(self, message_type: str) -> tuple[str, str]: """Return bundled template and schema paths for a message type.""" metadata = self.get_template(message_type) return str(metadata.template_path), str(metadata.xsd_path)
DEFAULT_TEMPLATE_REGISTRY = TemplateRegistry()