Files
weather-alerts/app/config/loader.py

217 lines
6.4 KiB
Python

"""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,
)