from __future__ import annotations from pathlib import Path from pydantic import BaseModel, Field, model_validator from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource from pydantic_settings.sources import EnvSettingsSource from tracker.constants import DisplayDistanceUnit, DisplayWeightUnit _TRACKER_REPO_ROOT = Path(__file__).resolve().parent.parent _CONFIG_YAML_PATH = _TRACKER_REPO_ROOT / "config.yaml" class DashboardLogin(BaseModel): """Small, explicit dashboard account for HF/demo deployments.""" label: str email: str = Field(min_length=1) password: str = Field(min_length=1) role: str | None = None notes: list[str] = Field(default_factory=list) class TrackerConfig(BaseSettings): """Tracker app configuration: config.yaml first, then TRACKER_* environment overrides (HF Secrets).""" model_config = SettingsConfigDict(env_prefix="TRACKER_", extra="ignore") parser_data_dir: str tracker_db_path: str google_credentials_path: str google_token_path: str default_calendar_id: str chores_calendar_id: str | None = None workout_calendar_id: str | None = None tracker_app_url: str = "http://127.0.0.1:8020" host: str port: int active_program_id: int | None = None agent_profile_path: str = "agent_profile.yaml" agent_api_key: str | None = None dashboard_password: str | None = None dashboard_logins: list[DashboardLogin] = Field(default_factory=list) session_secret: str | None = None display_weight_unit: DisplayWeightUnit = DisplayWeightUnit.KG display_distance_unit: DisplayDistanceUnit = DisplayDistanceUnit.KM calibration_dumbbell_bench_lb_per_hand: float | None = None calibration_dumbbell_squat_lb_per_hand: float | None = None dumbbell_weight_quantum_lb: float = 5.0 @model_validator(mode="after") def validate_dashboard_session(self) -> TrackerConfig: if self.dashboard_password or self.dashboard_logins: if not self.session_secret or len(self.session_secret) < 16: msg = ( "session_secret must be set to at least 16 characters when " "dashboard_password or dashboard_logins is set" ) raise ValueError(msg) return self @property def user_db_path(self) -> Path: return Path(self.parser_data_dir) / "user.db" @property def query_db_path(self) -> Path: return Path(self.parser_data_dir) / "web_query.db" @property def cache_db_path(self) -> Path: return Path(self.parser_data_dir) / "web_cache.db" @property def tracker_db_full_path(self) -> Path: path = Path(self.tracker_db_path) return path if path.is_absolute() else _TRACKER_REPO_ROOT / path @property def agent_profile_resolved_path(self) -> Path: path = Path(self.agent_profile_path) return path if path.is_absolute() else _TRACKER_REPO_ROOT / path @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, EnvSettingsSource(settings_cls), YamlConfigSettingsSource(settings_cls, yaml_file=str(_CONFIG_YAML_PATH)), ) def load_config() -> TrackerConfig: return TrackerConfig()