"""Strict profile decoding driven by the typed field declarations.""" from __future__ import annotations from dataclasses import asdict, fields, is_dataclass from typing import Any from python_app.models.gui_profile_schema import GuiProfileModel, GuiStateModel, Setting from python_app.models.json_fields import finite_number, json_object from python_app.models.run_config_schema import RunConfigModel def _decode_state(model: object, payload: dict[str, Any], context: str) -> None: """Read known fields recursively; unknown fields do not survive re-encoding.""" if not is_dataclass(model) or isinstance(model, type): raise TypeError("GUI state must be a dataclass instance") for member in fields(model): default = getattr(model, member.name) value = payload.get(member.name, default) name = f"{context}.{member.name}" if is_dataclass(default): _decode_state(default, json_object(payload.get(member.name), name), name) continue if isinstance(default, float): value = finite_number(value, name) elif type(value) is not type(default): raise ValueError(f"{name} must be a JSON {type(default).__name__}") spec = member.metadata.get("setting") if isinstance(spec, Setting): if spec.choices and value not in spec.choices: raise ValueError(f"{name} must be one of: {', '.join(spec.choices)}") if spec.minimum is not None: if value < spec.minimum or (spec.exclusive_minimum and value == spec.minimum): operator = ">" if spec.exclusive_minimum else ">=" raise ValueError(f"{name} must be {operator} {spec.minimum:g}") if spec.maximum is not None and value > spec.maximum: raise ValueError(f"{name} must be <= {spec.maximum:g}") setattr(model, member.name, value) def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: """Read the pipeline config and optional GUI state; ignore unknown fields.""" profile = GuiProfileModel(run_config=RunConfigModel.from_dict(payload)) if payload.get("gui") is None: return profile gui_object = json_object(payload["gui"], "gui") gui = GuiStateModel() _decode_state(gui, gui_object, "gui") if gui.version != 1: raise ValueError(f"Unsupported gui.version: {gui.version}") gui.preprocess_dialog.median_sweep_count = max(1, gui.preprocess_dialog.median_sweep_count) profile.gui = gui return profile def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]: """Encode the canonical run config and every declared GUI field.""" payload = model.run_config.to_dict() payload["gui"] = asdict(model.gui if model.gui is not None else GuiStateModel()) return payload