init commit
This commit is contained in:
185
app/main.py
Normal file
185
app/main.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user