from __future__ import annotations from pathlib import Path from typing import Literal import yaml from pydantic import BaseModel, ConfigDict, Field, field_validator class CaptureConfig(BaseModel): model_config = ConfigDict(extra="forbid") backend: Literal["fixture", "mss", "dxcam"] = "fixture" monitor: int = 1 fps_limit: int = Field(default=10, ge=1, le=240) window_title: str | None = None class VisionConfig(BaseModel): model_config = ConfigDict(extra="forbid") template_threshold: float = Field(default=0.85, ge=0.0, le=1.0) debug_output_dir: Path = Path("botty_next/debug/output") class InputConfig(BaseModel): model_config = ConfigDict(extra="forbid") enabled: bool = False dry_run: bool = True emergency_stop_key: str = "f12" @field_validator("dry_run") @classmethod def dry_run_required_when_disabled(cls, value: bool) -> bool: return value class BottyNextConfig(BaseModel): model_config = ConfigDict(extra="forbid") profile_name: str = "local" capture: CaptureConfig = Field(default_factory=CaptureConfig) vision: VisionConfig = Field(default_factory=VisionConfig) input: InputConfig = Field(default_factory=InputConfig) @field_validator("input") @classmethod def input_must_be_explicit_and_dry_run_by_default(cls, value: InputConfig) -> InputConfig: if value.enabled and value.dry_run is False: raise ValueError("live input cannot be enabled without a future explicit safety gate") return value def load_config(path: str | Path) -> BottyNextConfig: config_path = Path(path) with config_path.open("r", encoding="utf-8") as handle: raw = yaml.safe_load(handle) or {} return BottyNextConfig.model_validate(raw)