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