217 lines
6.4 KiB
Python
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,
|
|
)
|