ICRA/app/logic/constants.py

117 lines
3.4 KiB
Python

"""Shared configuration constants for the application."""
from __future__ import annotations
import contextlib
from pathlib import Path
from typing import Any, Callable
try: # Python 3.11 and above
import tomllib # type: ignore[attr-defined]
except ModuleNotFoundError: # pragma: no cover - fallback for <3.11
with contextlib.suppress(ModuleNotFoundError):
import tomli as tomllib # type: ignore[assignment]
if "tomllib" not in globals():
tomllib = None # type: ignore[assignment]
PREVIEW_MAX_SIZE = (900, 660)
BASE_DIR = Path(__file__).resolve().parents[2]
IMAGES_DIR = BASE_DIR / "images"
CONFIG_FILE = BASE_DIR / "config.toml"
LANG_DIR = BASE_DIR / "app" / "lang"
_DEFAULTS_BASE = {
"hue_min": 250.0,
"hue_max": 310.0,
"sat_min": 15.0,
"val_min": 15.0,
"val_max": 100.0,
"alpha": 120,
}
SUPPORTED_IMAGE_EXTENSIONS = (".webp", ".png", ".jpg", ".jpeg", ".bmp")
LANGUAGE_DEFAULT = "en"
_DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = {
"hue_min": float,
"hue_max": float,
"sat_min": float,
"val_min": float,
"val_max": float,
"alpha": int,
}
def _load_config_data() -> dict[str, Any]:
"""Read the optional config file once and return its parsed data."""
if tomllib is None or not CONFIG_FILE.exists():
return {}
decode_error = getattr(tomllib, "TOMLDecodeError", ValueError) # type: ignore[attr-defined]
try:
with CONFIG_FILE.open("rb") as handle:
data = tomllib.load(handle)
except (OSError, AttributeError, decode_error, TypeError): # type: ignore[arg-type]
return {}
if not isinstance(data, dict):
return {}
return data
def _extract_default_overrides(data: dict[str, Any]) -> dict[str, Any]:
settings = data.get("defaults")
if not isinstance(settings, dict):
return {}
overrides: dict[str, Any] = {}
for key, caster in _DEFAULT_TYPES.items():
if key not in settings:
continue
try:
overrides[key] = caster(settings[key])
except (TypeError, ValueError):
continue
return overrides
def _available_languages() -> set[str]:
languages = {path.stem.lower() for path in LANG_DIR.glob("*.toml")}
if not languages:
languages.add(LANGUAGE_DEFAULT)
return languages
def _extract_language(data: dict[str, Any]) -> str:
value = data.get("language")
supported = _available_languages()
if isinstance(value, str):
normalised = value.strip().lower()
if normalised in supported:
return normalised
if LANGUAGE_DEFAULT in supported:
return LANGUAGE_DEFAULT
return sorted(supported)[0]
_CONFIG_DATA = _load_config_data()
_OPTION_DEFAULTS = {"reset_exclusions_on_image_change": False}
def _extract_options(data: dict[str, Any]) -> dict[str, Any]:
section = data.get("options")
if not isinstance(section, dict):
return {}
result: dict[str, Any] = {}
value = section.get("reset_exclusions_on_image_change")
if isinstance(value, bool):
result["reset_exclusions_on_image_change"] = value
return result
_CONFIG_DATA = _load_config_data()
DEFAULTS = {**_DEFAULTS_BASE, **_extract_default_overrides(_CONFIG_DATA)}
LANGUAGE = _extract_language(_CONFIG_DATA)
OPTIONS = {**_OPTION_DEFAULTS, **_extract_options(_CONFIG_DATA)}
RESET_EXCLUSIONS_ON_IMAGE_CHANGE = OPTIONS["reset_exclusions_on_image_change"]