ETF_Suite_Portal/ETF_Portal/api/factory.py
Pascal c462342d44 feat: Add API management system with caching support
- Add base API client and cache manager classes

- Implement FMP and YFinance specific clients and cache managers

- Add API factory for managing multiple data providers

- Add test suite for API configuration and caching

- Add logging configuration for API operations
2025-05-27 14:07:32 +02:00

148 lines
5.0 KiB
Python

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