You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
13 KiB
Python

"""
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")
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}")
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")
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)}"
)