from typing import Optional, Dict, Any import logging import os from .base import BaseAPIClient from .fmp.client import FMPClient from .yfinance.client import YFinanceClient from ..cache.fmp_cache import FMPCacheManager from ..cache.yfinance_cache import YFinanceCacheManager class APIFactory: """Factory for creating and managing API clients.""" def __init__(self, fmp_api_key: Optional[str] = None): """Initialize API factory. Args: fmp_api_key: Optional FMP API key. If not provided, will try to get from environment variable. """ # Try to get API key from environment variable if not provided self.fmp_api_key = fmp_api_key or os.environ.get('FMP_API_KEY') if not self.fmp_api_key: logging.warning("No FMP API key found in environment. Some features may be limited.") self.logger = logging.getLogger(self.__class__.__name__) self._clients: Dict[str, BaseAPIClient] = {} def get_client(self, provider: str = 'fmp') -> BaseAPIClient: """Get an API client instance. Args: provider: API provider ('fmp' or 'yfinance') Returns: API client instance Raises: ValueError: If provider is invalid or FMP API key is missing """ provider = provider.lower() if provider not in ['fmp', 'yfinance']: raise ValueError(f"Invalid provider: {provider}") if provider in self._clients: return self._clients[provider] if provider == 'fmp': if not self.fmp_api_key: raise ValueError("FMP API key is required") client = FMPClient(self.fmp_api_key) else: # yfinance client = YFinanceClient() self._clients[provider] = client return client def get_data(self, symbol: str, data_type: str, provider: str = 'fmp', fallback: bool = True) -> Any: """Get data from API with fallback support. Args: symbol: ETF ticker symbol data_type: Type of data to retrieve provider: Primary API provider fallback: Whether to fall back to yfinance if primary fails Returns: Requested data or error information """ try: # Try primary provider client = self.get_client(provider) data = getattr(client, f"get_{data_type}")(symbol) # Check if data is valid if isinstance(data, dict) and data.get('error'): if fallback and provider == 'fmp': self.logger.info(f"Falling back to yfinance for {symbol}") return self.get_data(symbol, data_type, 'yfinance', False) return data return data except Exception as e: self.logger.error(f"Error getting {data_type} for {symbol}: {str(e)}") if fallback and provider == 'fmp': self.logger.info(f"Falling back to yfinance for {symbol}") return self.get_data(symbol, data_type, 'yfinance', False) return { 'error': True, 'message': str(e), 'provider': provider, 'data_type': data_type, 'symbol': symbol } def clear_cache(self, provider: Optional[str] = None) -> Dict[str, int]: """Clear cache for specified provider or all providers. Args: provider: Optional provider to clear cache for Returns: Dictionary with number of files cleared per provider """ results = {} if provider: providers = [provider] else: providers = ['fmp', 'yfinance'] for prov in providers: try: client = self.get_client(prov) results[prov] = client.clear_cache() except Exception as e: self.logger.error(f"Error clearing cache for {prov}: {str(e)}") results[prov] = 0 return results def get_cache_stats(self, provider: Optional[str] = None) -> Dict[str, Dict]: """Get cache statistics for specified provider or all providers. Args: provider: Optional provider to get stats for Returns: Dictionary with cache statistics per provider """ results = {} if provider: providers = [provider] else: providers = ['fmp', 'yfinance'] for prov in providers: try: client = self.get_client(prov) results[prov] = client.get_cache_stats() except Exception as e: self.logger.error(f"Error getting cache stats for {prov}: {str(e)}") results[prov] = {} return results