from __future__ import annotations import re from typing import Any RGB = tuple[int, int, int] HEX_RE = re.compile(r"^#([0-9a-fA-F]{6})$") def parse_color(value: Any) -> RGB: if isinstance(value, str): match = HEX_RE.match(value.strip()) if not match: raise ValueError("color must be #RRGGBB") raw = match.group(1) return int(raw[0:2], 16), int(raw[2:4], 16), int(raw[4:6], 16) if isinstance(value, dict): try: return validate_rgb((value["r"], value["g"], value["b"])) except KeyError as exc: raise ValueError("color object must contain r, g, and b") from exc if isinstance(value, (list, tuple)) and len(value) == 3: return validate_rgb(value) raise ValueError("color must be #RRGGBB or {r,g,b}") def validate_rgb(value: Any) -> RGB: try: channels = tuple(int(channel) for channel in value) except Exception as exc: raise ValueError("RGB channels must be integers") from exc if len(channels) != 3: raise ValueError("RGB color must have three channels") if any(channel < 0 or channel > 255 for channel in channels): raise ValueError("RGB channels must be in 0..255") return channels # type: ignore[return-value]