from __future__ import annotations
import ast
import hashlib
import json
from typing import Any
def parse_scalar_or_list(raw: Any, cast=str) -> list[Any]:
if raw is None:
return []
if isinstance(raw, list):
return [cast(v) for v in raw]
if isinstance(raw, tuple):
return [cast(v) for v in raw]
if isinstance(raw, bool):
return [raw]
text = str(raw).strip()
if not text:
return []
if text.startswith("[") and text.endswith("]"):
inner = text[1:-1].strip()
if not inner:
return []
try:
parsed = ast.literal_eval(text)
except (SyntaxError, ValueError):
return [cast(v.strip().strip("'\"")) for v in inner.split(",") if v.strip()]
if not isinstance(parsed, list):
raise ValueError(f"Expected list format, got: {raw}")
return [cast(v) for v in parsed]
if "," in text:
return [cast(v.strip()) for v in text.split(",") if v.strip()]
return [cast(text)]
def parse_optional_number(raw: Any, cast=float) -> Any:
if raw in (None, "", "None", "none", "auto"):
return None
return cast(raw)
def stable_hash(data: dict[str, Any]) -> str:
norm = json.dumps(data, sort_keys=True, ensure_ascii=False, default=str)
return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16]
def bool_from_ui(value: Any) -> bool:
if isinstance(value, bool):
return value
text = str(value).strip().lower()
return text in {"1", "true", "yes", "y", "on"}
def normalize_value(value: Any) -> Any:
if isinstance(value, list):
return [normalize_value(v) for v in value]
if isinstance(value, tuple):
return [normalize_value(v) for v in value]
return value