Source code for hydromodpy.schema.export

"""Full JSON Schema export for the HydroModPy configuration.

Produces three companion JSON files ready to be consumed by an external
frontend that renders a formular without having to know anything about
Python:

* ``config.json``           - the full JSON Schema of ``HydroModPyConfig``.
* ``config_meta.json``      - high-level metadata (ordered root sections,
                               UI groups, per-section titles).
* ``field_validators.json`` - flat mapping ``field_path -> validator_type``
                               so the frontend can pick a widget + a local
                               validator without re-parsing the schema.

The exporter reuses :mod:`hydromodpy.config.schema_export` for the raw
Pydantic schema and adds the two companion documents on top.

Usage::

    from hydromodpy.schema import export_full_schema

    export_full_schema("./schema/")

CLI::

    hmp dev schema export --output ./schema/
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from hydromodpy.config.schema_export import (
    _cached_full_schema,
    _ensure_root_sections,
    export_schema,
)
from hydromodpy.core.config_kit.registry import root_scalar_fields

SCHEMA_FILE = "config.json"
META_FILE = "config_meta.json"
VALIDATORS_FILE = "field_validators.json"


[docs] def build_config_meta() -> dict[str, Any]: """Return metadata describing the TOML layout (sections, order, groups). The returned document is **derived** from the JSON Schema but flattened for easy consumption by a frontend: * ``sections`` lists the root TOML sections in the canonical order produced by :func:`hydromodpy.config.schema_export.export_schema`. * ``groups`` aggregates ``json_schema_extra.group`` across all annotated fields so a UI can render tabs / accordions. """ sections = _ensure_root_sections() root_fields = root_scalar_fields() full_schema = export_schema() root_props = full_schema.get("properties", {}) ordered_root_fields: list[dict[str, Any]] = [] for name in root_fields: if name not in root_props: continue entry = root_props[name] ordered_root_fields.append( { "name": name, "title": entry.get("title", name), "description": entry.get("description"), "validator": _infer_validator_type(entry), } ) ordered_sections: list[dict[str, Any]] = [] for name in sections: if name not in root_props: # The root model may not expose every registered section. continue entry = root_props[name] ordered_sections.append( { "name": name, "title": entry.get("title", name), "description": entry.get("description"), "ref": entry.get("$ref") or entry.get("allOf", [{}])[0].get("$ref"), } ) groups: dict[str, list[str]] = {} for field_name, info in root_fields.items(): extra = info.json_schema_extra or {} if isinstance(extra, dict) and "group" in extra: groups.setdefault(extra["group"], []).append(field_name) for section_name, cls in sections.items(): for field_name, info in cls.model_fields.items(): extra = info.json_schema_extra or {} if isinstance(extra, dict) and "group" in extra: groups.setdefault(extra["group"], []).append(f"{section_name}.{field_name}") return { "$comment": "Generated by hydromodpy.schema.export.build_config_meta", "root_fields": ordered_root_fields, "sections": ordered_sections, "groups": groups, }
def _infer_validator_type(field_schema: dict[str, Any]) -> str: """Map a JSON Schema field entry to a high-level validator type.""" if "enum" in field_schema: return "enum" json_type = field_schema.get("type") if isinstance(json_type, list): json_type = next((t for t in json_type if t != "null"), json_type[0]) if json_type in ("integer", "number"): return "number" if json_type == "boolean": return "boolean" if json_type == "array": return "array" if json_type == "object": return "object" if json_type == "string": fmt = field_schema.get("format") if fmt in ("date", "date-time", "duration"): return fmt if fmt == "path": return "path" return "string" if "$ref" in field_schema: return "nested" return "any" def _walk_fields( section_name: str, model_cls: type, out: dict[str, str], ) -> None: """Populate *out* with ``field_path -> validator_type`` entries.""" try: schema = _cached_full_schema(model_cls) except Exception: return props = schema.get("properties", {}) for field_name, field_schema in props.items(): path = f"{section_name}.{field_name}" out[path] = _infer_validator_type(field_schema)
[docs] def build_field_validators() -> dict[str, str]: """Flatten every root-level field into a ``path -> validator_type`` map.""" sections = _ensure_root_sections() root_fields = root_scalar_fields() root_schema = export_schema().get("properties", {}) flat: dict[str, str] = {} for field_name in root_fields: field_schema = root_schema.get(field_name, {}) flat[field_name] = _infer_validator_type(field_schema) for section_name, cls in sections.items(): _walk_fields(section_name, cls, flat) return flat
[docs] def export_full_schema(output_dir: str | Path, *, indent: int = 2) -> dict[str, Path]: """Write the three companion JSON files into *output_dir*. Parameters ---------- output_dir Destination directory (created if missing). indent JSON indent used for human-readable output. Returns ------- dict[str, Path] Map keyed by ``config``, ``meta`` and ``validators`` pointing at the files that were written. """ out = Path(output_dir).expanduser().resolve() out.mkdir(parents=True, exist_ok=True) schema = export_schema() meta = build_config_meta() validators = build_field_validators() config_path = out / SCHEMA_FILE meta_path = out / META_FILE validators_path = out / VALIDATORS_FILE config_path.write_text( json.dumps(schema, indent=indent, ensure_ascii=False) + "\n", encoding="utf-8", ) meta_path.write_text( json.dumps(meta, indent=indent, ensure_ascii=False) + "\n", encoding="utf-8", ) validators_path.write_text( json.dumps(validators, indent=indent, ensure_ascii=False, sort_keys=True) + "\n", encoding="utf-8", ) return { "config": config_path, "meta": meta_path, "validators": validators_path, }
__all__ = [ "build_config_meta", "build_field_validators", "export_full_schema", "SCHEMA_FILE", "META_FILE", "VALIDATORS_FILE", ]