SurfSense / app /forecasting /base_client.py
Joshua Wehr
Initial SurfSense deployment
0fb11f4
Raw History Blame Contribute Delete
5.21 kB
"""
Base interface for forecast API clients.
All forecast providers (Stormglass, NOAA, etc.) implement this interface
to ensure consistent behavior across the system.
"""
from abc import ABC, abstractmethod
from typing import Optional
from datetime import datetime
from .models import ForecastResponse, Coordinates
class ForecastAPIClient(ABC):
"""
Abstract base class for forecast API clients.
All forecast data providers must implement this interface to be
compatible with the ForecastIntegrationAgent.
"""
@property
@abstractmethod
def source_name(self) -> str:
"""Return the name of this data source."""
pass
@property
@abstractmethod
def is_configured(self) -> bool:
"""Return True if this client is properly configured (e.g., has API key)."""
pass
@property
@abstractmethod
def requires_api_key(self) -> bool:
"""Return True if this data source requires an API key."""
pass
@abstractmethod
async def get_forecast(
self,
lat: float,
lon: float,
days: int = 5
) -> Optional[ForecastResponse]:
"""
Fetch forecast data for a location.
Args:
lat: Latitude of the location
lon: Longitude of the location
days: Number of days to forecast (default: 5)
Returns:
ForecastResponse if successful, None if failed
"""
pass
@abstractmethod
async def get_tide(
self,
lat: float,
lon: float,
date: Optional[datetime] = None
) -> Optional[dict]:
"""
Fetch tide data for a location.
Args:
lat: Latitude of the location
lon: Longitude of the location
date: Date for tide data (default: today)
Returns:
Tide data dictionary if successful, None if failed
"""
pass
def supports_location(self, lat: float, lon: float) -> bool:
"""
Check if this data source supports the given location.
Some providers (like NOAA) only cover certain regions.
Default implementation returns True (global coverage).
Args:
lat: Latitude
lon: Longitude
Returns:
True if location is supported
"""
return True
async def health_check(self) -> bool:
"""
Check if the API is accessible and working.
Returns:
True if API is healthy
"""
return self.is_configured
class ForecastClientRegistry:
"""
Registry for managing multiple forecast API clients.
Provides source selection and fallback logic.
"""
def __init__(self):
self._clients: dict[str, ForecastAPIClient] = {}
self._priority_order: list[str] = []
def register(self, client: ForecastAPIClient, priority: int = 100) -> None:
"""
Register a forecast client.
Args:
client: The forecast client to register
priority: Lower number = higher priority (default: 100)
"""
name = client.source_name
self._clients[name] = client
# Insert maintaining priority order
inserted = False
for i, existing_name in enumerate(self._priority_order):
existing_client = self._clients[existing_name]
# This is simplified - in practice you'd store priority separately
if not inserted:
self._priority_order.insert(i, name)
inserted = True
break
if not inserted:
self._priority_order.append(name)
def get_client(self, name: str) -> Optional[ForecastAPIClient]:
"""Get a specific client by name."""
return self._clients.get(name)
def get_available_clients(self) -> list[ForecastAPIClient]:
"""Get all configured and available clients."""
return [
client for client in self._clients.values()
if client.is_configured
]
def get_best_client_for_location(
self,
lat: float,
lon: float
) -> Optional[ForecastAPIClient]:
"""
Get the best available client for a specific location.
Considers:
1. Whether client supports the location
2. Whether client is configured
3. Priority order
Args:
lat: Latitude
lon: Longitude
Returns:
Best available client, or None if no suitable client
"""
for name in self._priority_order:
client = self._clients[name]
if client.is_configured and client.supports_location(lat, lon):
return client
return None
@property
def registered_sources(self) -> list[str]:
"""List all registered source names."""
return list(self._clients.keys())
# Global registry instance
forecast_registry = ForecastClientRegistry()