init commit
This commit is contained in:
5
app/config/__init__.py
Normal file
5
app/config/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Configuration loading and management."""
|
||||
|
||||
from app.config.loader import load_config, AppConfig
|
||||
|
||||
__all__ = ["load_config", "AppConfig"]
|
||||
159
app/config/loader.py
Normal file
159
app/config/loader.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""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 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)
|
||||
|
||||
|
||||
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", {})
|
||||
|
||||
# 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),
|
||||
)
|
||||
|
||||
return AppConfig(
|
||||
app=app_settings,
|
||||
weather=weather_settings,
|
||||
alerts=alert_settings,
|
||||
notifications=notification_settings,
|
||||
state=state_settings,
|
||||
)
|
||||
40
app/config/settings.example.yaml
Normal file
40
app/config/settings.example.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
# Weather Alerts Configuration Example
|
||||
# Copy this file to settings.yaml and customize as needed.
|
||||
# Secrets (API keys, tokens) should be in .env file, not here.
|
||||
|
||||
app:
|
||||
name: "weather-alerts"
|
||||
version: "1.0.0"
|
||||
log_level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
|
||||
weather:
|
||||
location: "viola,tn" # City,State or ZIP code
|
||||
hours_ahead: 24 # Number of forecast hours to check
|
||||
unit_group: "us" # "us" for Fahrenheit/mph, "metric" for Celsius/kph
|
||||
|
||||
alerts:
|
||||
rules:
|
||||
temperature:
|
||||
enabled: true
|
||||
below: 32 # Alert when temp falls below this (freezing)
|
||||
above: 100 # Alert when temp exceeds this (extreme heat)
|
||||
precipitation:
|
||||
enabled: true
|
||||
probability_above: 70 # Alert when precipitation chance exceeds this %
|
||||
wind:
|
||||
enabled: true
|
||||
speed_above: 25 # Alert when sustained wind exceeds this (mph)
|
||||
gust_above: 40 # Alert when wind gusts exceed this (mph)
|
||||
severe_weather:
|
||||
enabled: true # Forward severe weather alerts from the API
|
||||
|
||||
notifications:
|
||||
ntfy:
|
||||
server_url: "https://ntfy.sneakygeek.net"
|
||||
topic: "weather-alerts"
|
||||
priority: "high" # min, low, default, high, urgent
|
||||
tags: ["cloud", "warning"] # Emoji tags for notification
|
||||
|
||||
state:
|
||||
file_path: "./data/state.json"
|
||||
dedup_window_hours: 24 # Don't repeat same alert within this window
|
||||
36
app/config/settings.yaml
Normal file
36
app/config/settings.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
app:
|
||||
name: "weather-alerts"
|
||||
version: "1.0.0"
|
||||
log_level: "INFO"
|
||||
|
||||
weather:
|
||||
location: "viola,tn"
|
||||
hours_ahead: 24
|
||||
unit_group: "us"
|
||||
|
||||
alerts:
|
||||
rules:
|
||||
temperature:
|
||||
enabled: true
|
||||
below: 32
|
||||
above: 95
|
||||
precipitation:
|
||||
enabled: true
|
||||
probability_above: 60
|
||||
wind:
|
||||
enabled: true
|
||||
speed_above: 25
|
||||
gust_above: 30
|
||||
severe_weather:
|
||||
enabled: true
|
||||
|
||||
notifications:
|
||||
ntfy:
|
||||
server_url: "https://ntfy.sneakygeek.net"
|
||||
topic: "weather-alerts"
|
||||
priority: "high"
|
||||
tags: ["cloud", "warning"]
|
||||
|
||||
state:
|
||||
file_path: "./data/state.json"
|
||||
dedup_window_hours: 24
|
||||
Reference in New Issue
Block a user