"""Main orchestration module for weather alerts application.""" import sys from typing import Optional from app.config.loader import AppConfig, load_config from app.services.alert_aggregator import AlertAggregator from app.services.notification_service import NotificationService from app.services.rule_engine import RuleEngine from app.services.state_manager import StateManager from app.services.weather_service import WeatherService, WeatherServiceError from app.utils.http_client import HttpClient from app.utils.logging_config import configure_logging, get_logger class WeatherAlertsApp: """Main application class for weather alerts.""" def __init__(self, config: AppConfig) -> None: """Initialize the application with configuration. Args: config: The application configuration. """ self.config = config self.logger = get_logger(__name__) # Initialize HTTP client (shared across services) self.http_client = HttpClient() # Initialize services self.weather_service = WeatherService( api_key=config.weather.api_key, http_client=self.http_client, ) self.rule_engine = RuleEngine(rules=config.alerts.rules) self.state_manager = StateManager( file_path=config.state.file_path, dedup_window_hours=config.state.dedup_window_hours, ) self.alert_aggregator = AlertAggregator() self.notification_service = NotificationService( server_url=config.notifications.ntfy.server_url, topic=config.notifications.ntfy.topic, access_token=config.notifications.ntfy.access_token, priority=config.notifications.ntfy.priority, default_tags=config.notifications.ntfy.tags, http_client=self.http_client, ) def run(self) -> int: """Execute the main application flow. Returns: Exit code (0 for success, 1 for error). """ self.logger.info( "app_starting", version=self.config.app.version, location=self.config.weather.location, ) try: # Step 1: Fetch weather forecast self.logger.info("step_fetch_forecast") forecast = self.weather_service.get_forecast( location=self.config.weather.location, hours_ahead=self.config.weather.hours_ahead, unit_group=self.config.weather.unit_group, ) # Step 2: Evaluate rules against forecast self.logger.info("step_evaluate_rules") triggered_alerts = self.rule_engine.evaluate(forecast) if not triggered_alerts: self.logger.info("no_alerts_triggered") return 0 self.logger.info( "alerts_triggered", count=len(triggered_alerts), ) # Step 2.5: Aggregate alerts by type self.logger.info("step_aggregate_alerts") aggregated_alerts = self.alert_aggregator.aggregate(triggered_alerts) self.logger.info( "alerts_aggregated", input_count=len(triggered_alerts), output_count=len(aggregated_alerts), ) # Step 3: Filter duplicates self.logger.info("step_filter_duplicates") new_alerts = self.state_manager.filter_duplicates(aggregated_alerts) if not new_alerts: self.logger.info("all_alerts_are_duplicates") return 0 # Step 4: Send notifications self.logger.info( "step_send_notifications", count=len(new_alerts), ) results = self.notification_service.send_batch(new_alerts) # Step 5: Record sent alerts self.logger.info("step_record_sent") for result in results: if result.success: self.state_manager.record_sent(result.alert) # Step 6: Purge old records and save state self.state_manager.purge_old_records() self.state_manager.save() # Report results success_count = sum(1 for r in results if r.success) failed_count = len(results) - success_count self.logger.info( "app_complete", alerts_sent=success_count, alerts_failed=failed_count, ) return 0 if failed_count == 0 else 1 except WeatherServiceError as e: self.logger.error("weather_service_error", error=str(e)) return 1 except Exception as e: self.logger.exception("unexpected_error", error=str(e)) return 1 finally: self.http_client.close() def main(config_path: Optional[str] = None) -> int: """Main entry point for the application. Args: config_path: Optional path to configuration file. Returns: Exit code (0 for success, 1 for error). """ # Load configuration try: config = load_config(config_path) except Exception as e: print(f"Failed to load configuration: {e}", file=sys.stderr) return 1 # Configure logging configure_logging(config.app.log_level) logger = get_logger(__name__) # Validate required secrets if not config.weather.api_key: logger.error("missing_api_key", hint="Set VISUALCROSSING_API_KEY environment variable") return 1 if not config.notifications.ntfy.access_token: logger.warning( "missing_ntfy_token", hint="Set NTFY_ACCESS_TOKEN if your server requires auth", ) # Run the application app = WeatherAlertsApp(config) return app.run() if __name__ == "__main__": sys.exit(main())