Files
radar_system/python_app/models/gui_profile_codec.py
T
2026-10-08 16:24:01 +03:00

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