62 lines
2.8 KiB
Python
62 lines
2.8 KiB
Python
"""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
|