"""Typed configuration loader with YAML and environment variable support.""" import os from dataclasses import dataclass, field from pathlib import Path from typing import Any, Optional import yaml from dotenv import load_dotenv from app.models.alerts import AlertRules @dataclass class AppSettings: """Application-level settings.""" name: str = "weather-alerts" version: str = "1.0.0" log_level: str = "INFO" @dataclass class WeatherSettings: """Weather API settings.""" location: str = "viola,tn" hours_ahead: int = 24 unit_group: str = "us" api_key: str = "" @dataclass class NtfySettings: """Ntfy notification settings.""" server_url: str = "https://ntfy.sneakygeek.net" topic: str = "weather-alerts" priority: str = "high" tags: list[str] = field(default_factory=lambda: ["cloud", "warning"]) access_token: str = "" @dataclass class NotificationSettings: """Notification settings container.""" ntfy: NtfySettings = field(default_factory=NtfySettings) @dataclass class StateSettings: """State management settings.""" file_path: str = "./data/state.json" dedup_window_hours: int = 24 @dataclass class AlertSettings: """Alert configuration settings.""" rules: AlertRules = field(default_factory=AlertRules) @dataclass class AISettings: """AI summarization settings.""" enabled: bool = False model: str = "meta/meta-llama-3-8b-instruct" api_timeout: int = 60 max_tokens: int = 500 api_token: str = "" @dataclass class ChangeThresholds: """Thresholds for detecting significant changes between runs.""" temperature: float = 5.0 wind_speed: float = 10.0 wind_gust: float = 10.0 precipitation_prob: float = 20.0 @dataclass class ChangeDetectionSettings: """Change detection settings.""" enabled: bool = True thresholds: ChangeThresholds = field(default_factory=ChangeThresholds) @dataclass class AppConfig: """Complete application configuration.""" app: AppSettings = field(default_factory=AppSettings) weather: WeatherSettings = field(default_factory=WeatherSettings) alerts: AlertSettings = field(default_factory=AlertSettings) notifications: NotificationSettings = field(default_factory=NotificationSettings) state: StateSettings = field(default_factory=StateSettings) ai: AISettings = field(default_factory=AISettings) change_detection: ChangeDetectionSettings = field(default_factory=ChangeDetectionSettings) def load_config( config_path: Optional[str] = None, env_path: Optional[str] = None, ) -> AppConfig: """Load configuration from YAML file and environment variables. Args: config_path: Path to the YAML config file. Defaults to app/config/settings.yaml. env_path: Path to the .env file. Defaults to .env in the project root. Returns: A fully populated AppConfig instance. """ # Load environment variables from .env file if env_path: load_dotenv(env_path) else: load_dotenv() # Determine config file path if config_path is None: config_path = os.environ.get( "WEATHER_ALERTS_CONFIG", str(Path(__file__).parent / "settings.yaml"), ) # Load YAML config config_data: dict[str, Any] = {} config_file = Path(config_path) if config_file.exists(): with open(config_file) as f: config_data = yaml.safe_load(f) or {} # Build configuration with defaults app_data = config_data.get("app", {}) weather_data = config_data.get("weather", {}) alerts_data = config_data.get("alerts", {}) notifications_data = config_data.get("notifications", {}) state_data = config_data.get("state", {}) ai_data = config_data.get("ai", {}) change_detection_data = config_data.get("change_detection", {}) # Build app settings app_settings = AppSettings( name=app_data.get("name", "weather-alerts"), version=app_data.get("version", "1.0.0"), log_level=app_data.get("log_level", "INFO"), ) # Build weather settings with API key from environment weather_settings = WeatherSettings( location=weather_data.get("location", "viola,tn"), hours_ahead=weather_data.get("hours_ahead", 24), unit_group=weather_data.get("unit_group", "us"), api_key=os.environ.get("VISUALCROSSING_API_KEY", ""), ) # Build alert settings rules_data = alerts_data.get("rules", {}) alert_settings = AlertSettings(rules=AlertRules.from_dict(rules_data)) # Build notification settings with token from environment ntfy_data = notifications_data.get("ntfy", {}) ntfy_settings = NtfySettings( server_url=ntfy_data.get("server_url", "https://ntfy.sneakygeek.net"), topic=ntfy_data.get("topic", "weather-alerts"), priority=ntfy_data.get("priority", "high"), tags=ntfy_data.get("tags", ["cloud", "warning"]), access_token=os.environ.get("NTFY_ACCESS_TOKEN", ""), ) notification_settings = NotificationSettings(ntfy=ntfy_settings) # Build state settings state_settings = StateSettings( file_path=state_data.get("file_path", "./data/state.json"), dedup_window_hours=state_data.get("dedup_window_hours", 24), ) # Build AI settings with token from environment ai_settings = AISettings( enabled=ai_data.get("enabled", False), model=ai_data.get("model", "meta/meta-llama-3-8b-instruct"), api_timeout=ai_data.get("api_timeout", 60), max_tokens=ai_data.get("max_tokens", 500), api_token=os.environ.get("REPLICATE_API_TOKEN", ""), ) # Build change detection settings thresholds_data = change_detection_data.get("thresholds", {}) change_thresholds = ChangeThresholds( temperature=thresholds_data.get("temperature", 5.0), wind_speed=thresholds_data.get("wind_speed", 10.0), wind_gust=thresholds_data.get("wind_gust", 10.0), precipitation_prob=thresholds_data.get("precipitation_prob", 20.0), ) change_detection_settings = ChangeDetectionSettings( enabled=change_detection_data.get("enabled", True), thresholds=change_thresholds, ) return AppConfig( app=app_settings, weather=weather_settings, alerts=alert_settings, notifications=notification_settings, state=state_settings, ai=ai_settings, change_detection=change_detection_settings, )