44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
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]
|
|
|