""" FRED (Federal Reserve Economic Data) endpoints 연방준비제도 경제 데이터 API """ from typing import Optional from fastapi import APIRouter, HTTPException, Query, Depends from sqlalchemy.ext.asyncio import AsyncSession import logging from app.core.database import get_db from app.services.fred_service import fred_service from app.services.fred_proxy_service import fred_proxy_service router = APIRouter() logger = logging.getLogger("app.api.v1.fred") # Deprecated: Individual endpoints replaced by universal proxy # Use /proxy/{endpoint} instead for all FRED API access @router.get("/stats/usage", summary="FRED API usage statistics and cache performance") async def get_fred_usage_stats( days: int = Query(7, ge=1, le=30, description="Number of days to include in stats"), use_proxy_stats: bool = Query(True, description="Use enhanced proxy service statistics"), db: AsyncSession = Depends(get_db) ): """ Get FRED API usage statistics and cache performance Returns detailed statistics about API usage, cache performance, and daily limits. Now includes enhanced proxy service statistics. **Example Response**: ```json { "success": true, "data": { "daily_limit": 1000, "used_today": 45, "remaining_today": 955, "usage_percentage": 4.5, "can_make_requests": true, "daily_stats": [ { "date": "2025-01-14", "total_calls": 45, "successful_calls": 44, "total_records": 1250, "success_rate": 97.8 } ], "endpoint_stats": [ { "endpoint": "series", "call_count": 25 } ], "proxy_info": { "mode": "pass_through_proxy", "supported_endpoints": "all_fred_endpoints" } } } ``` **Parameters**: - `days`: Number of days to include in historical statistics (1-30) - `use_proxy_stats`: Use enhanced proxy service statistics (recommended) **Metrics Included**: - Daily API usage and remaining quota - Historical usage patterns - Endpoint-specific usage statistics (NEW!) - Success rates and error tracking - Proxy service information (NEW!) """ try: logger.info(f"📊 Getting FRED usage stats for {days} days (proxy_stats={use_proxy_stats})") if use_proxy_stats: # 향상된 proxy 서비스 통계 사용 result = await fred_proxy_service.get_api_usage_stats(db, days) else: # 기존 서비스 통계 사용 result = await fred_service.get_api_usage_stats(db, days) if not result.get('success'): logger.error(f"❌ Failed to get FRED usage stats: {result.get('error')}") raise HTTPException( status_code=500, detail=f"Failed to retrieve usage statistics: {result.get('error')}" ) stats_data = result['data'] used_today = stats_data['used_today'] remaining = stats_data['remaining_today'] logger.info(f"✅ FRED usage stats: {used_today}/1000 used, {remaining} remaining") return { "success": True, "message": f"FRED API usage: {used_today}/1000 used today ({remaining} remaining)", "data": stats_data, "metadata": { "source": "fred.stlouisfed.org", "daily_limit": 1000, "service_type": "proxy_service" if use_proxy_stats else "original_service", "enhanced_features": use_proxy_stats } } except HTTPException: raise except Exception as e: logger.error(f"❌ Error getting FRED usage stats: {e}") raise HTTPException( status_code=500, detail=f"Internal server error while fetching usage statistics: {str(e)}" ) # Removed: /search endpoint - use /proxy/series/search instead @router.get("/proxy/{endpoint:path}", summary="Universal FRED API proxy") async def fred_proxy_endpoint( endpoint: str, db: AsyncSession = Depends(get_db), series_id: Optional[str] = Query(None, description="Series ID parameter"), category_id: Optional[int] = Query(None, description="Category ID parameter"), release_id: Optional[int] = Query(None, description="Release ID parameter"), source_id: Optional[int] = Query(None, description="Source ID parameter"), tag_names: Optional[str] = Query(None, description="Tag names parameter"), realtime_start: Optional[str] = Query(None, description="Realtime start date (YYYY-MM-DD)"), realtime_end: Optional[str] = Query(None, description="Realtime end date (YYYY-MM-DD)"), observation_start: Optional[str] = Query(None, description="Observation start date (YYYY-MM-DD)"), observation_end: Optional[str] = Query(None, description="Observation end date (YYYY-MM-DD)"), limit: Optional[int] = Query(None, ge=1, le=100000, description="Limit number of results"), offset: Optional[int] = Query(None, ge=0, description="Offset for pagination"), order_by: Optional[str] = Query(None, description="Order by parameter"), sort_order: Optional[str] = Query(None, description="Sort order (asc/desc)"), search_text: Optional[str] = Query(None, description="Search text"), search_type: Optional[str] = Query(None, description="Search type"), frequency: Optional[str] = Query(None, description="Data frequency"), aggregation_method: Optional[str] = Query(None, description="Aggregation method"), output_type: Optional[int] = Query(None, description="Output type"), vintage_dates: Optional[str] = Query(None, description="Vintage dates"), exclude_tag_names: Optional[str] = Query(None, description="Exclude tag names"), tag_group_id: Optional[str] = Query(None, description="Tag group ID"), bypass_limit_check: bool = Query(False, description="Bypass daily limit check (admin only)"), force_refresh: bool = Query(False, description="Force refresh from API, bypass cache") ): """ FRED API Pass-through Proxy Universal proxy endpoint that forwards requests to any FRED API endpoint while maintaining our caching and rate limiting logic. **Supported Endpoints**: All FRED API endpoints are supported **Examples**: ```bash # Series information GET /api/v1/fred/proxy/series?series_id=GDP # Series observations GET /api/v1/fred/proxy/series/observations?series_id=UNRATE&limit=12 # Category information GET /api/v1/fred/proxy/category?category_id=125 # Category children GET /api/v1/fred/proxy/category/children?category_id=13 # Release information GET /api/v1/fred/proxy/release?release_id=53 # Search series GET /api/v1/fred/proxy/series/search?search_text=unemployment&limit=25 # Sources GET /api/v1/fred/proxy/sources # Tags GET /api/v1/fred/proxy/tags?limit=100 ``` **Key Features**: - **Universal Access**: Support for all FRED API endpoints - **Smart Caching**: 24-hour DB caching for series and observations (NEW!) - **Permanent Storage**: Historical data permanently stored in database (NEW!) - **Rate Limiting**: Respects 1,000/day limit with usage tracking - **Parameter Forwarding**: Automatically forwards all supported parameters - **Error Handling**: Comprehensive error handling and logging - **Usage Statistics**: Tracks endpoint usage and performance **Parameters**: All standard FRED API parameters are supported including: - `series_id`, `category_id`, `release_id`, `source_id` - `realtime_start`, `realtime_end`, `observation_start`, `observation_end` - `limit`, `offset`, `order_by`, `sort_order` - `search_text`, `search_type`, `frequency`, `aggregation_method` - `force_refresh`: Bypass cache and fetch fresh data from FRED API - `bypass_limit_check`: Skip daily limit validation (admin only) - And many more... **Caching Strategy**: - **Cache Hit**: Returns instantly from database (no API call) - **Cache Miss**: Fetches from FRED API and stores for 24 hours - **Permanent Storage**: Historical observations stored permanently - **API Limit Reached**: Returns cached data even if expired **Response Format**: Returns original FRED API response with additional metadata """ try: logger.info(f"🔄 FRED proxy request: {endpoint}") # 파라미터 수집 - None이 아닌 값만 포함 params = {} # 기본 파라미터들 param_mapping = { 'series_id': series_id, 'category_id': category_id, 'release_id': release_id, 'source_id': source_id, 'tag_names': tag_names, 'realtime_start': realtime_start, 'realtime_end': realtime_end, 'observation_start': observation_start, 'observation_end': observation_end, 'limit': limit, 'offset': offset, 'order_by': order_by, 'sort_order': sort_order, 'search_text': search_text, 'search_type': search_type, 'frequency': frequency, 'aggregation_method': aggregation_method, 'output_type': output_type, 'vintage_dates': vintage_dates, 'exclude_tag_names': exclude_tag_names, 'tag_group_id': tag_group_id } # None이 아닌 파라미터만 추가 for key, value in param_mapping.items(): if value is not None: params[key] = value # Proxy 서비스 호출 result = await fred_proxy_service.proxy_fred_request( db, endpoint, params, bypass_limit_check, force_refresh ) if not result.get('success'): error_detail = result.get('error', 'Unknown error') error_details = result.get('details', {}) logger.warning(f"❌ FRED proxy failed: {endpoint} -> {error_detail}") # 사용량 한도 초과인 경우 429 상태 코드 if 'limit' in error_detail.lower(): raise HTTPException( status_code=429, detail=f"FRED API daily limit reached: {error_detail}", headers={"Retry-After": "86400"} # 24 hours ) else: raise HTTPException( status_code=500, detail=f"FRED API error: {error_detail}" ) # 성공 응답 response_data = result['data'] metadata = result['metadata'] # 응답 크기 계산 response_size = metadata.get('response_size', 0) logger.info(f"✅ FRED proxy success: {endpoint} -> {response_size} records") return { "success": True, "message": f"FRED API proxy: {endpoint} -> {response_size} records", "data": response_data, "metadata": { **metadata, "endpoint_accessed": endpoint, "parameters_used": params, "daily_api_limit": 1000 } } except HTTPException: raise except Exception as e: logger.error(f"❌ Error in FRED proxy endpoint: {e}") raise HTTPException( status_code=500, detail=f"Internal server error in FRED proxy: {str(e)}" ) @router.get("/endpoints", summary="List supported FRED API endpoints") async def get_supported_fred_endpoints(): """ Get list of supported FRED API endpoints Returns comprehensive list of all FRED API endpoints that can be accessed through the proxy service. **Usage**: Use this to discover available endpoints and their categories. **Example Response**: ```json { "series_endpoints": [ "series", "series/observations", "series/search", "..." ], "category_endpoints": ["..."], "release_endpoints": ["..."] } ``` """ try: endpoints = fred_proxy_service.get_supported_endpoints() return { "success": True, "message": "FRED API supported endpoints", "data": endpoints, "metadata": { "total_endpoint_categories": len([k for k in endpoints.keys() if k.endswith('_endpoints')]), "proxy_mode": "pass_through", "base_url": "https://api.stlouisfed.org/fred" } } except Exception as e: logger.error(f"❌ Error getting FRED endpoints: {e}") raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" )