|
|
"""
|
|
|
HF Space Complete API Router
|
|
|
Implements all required endpoints for Hugging Face Space deployment
|
|
|
with fallback support and comprehensive data endpoints
|
|
|
"""
|
|
|
from fastapi import APIRouter, HTTPException, Query, Body, Depends
|
|
|
from fastapi.responses import JSONResponse
|
|
|
from typing import Optional, List, Dict, Any
|
|
|
from datetime import datetime, timedelta
|
|
|
from pydantic import BaseModel, Field
|
|
|
import logging
|
|
|
import asyncio
|
|
|
import json
|
|
|
import os
|
|
|
from pathlib import Path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
router = APIRouter(tags=["HF Space Complete API"])
|
|
|
|
|
|
|
|
|
from backend.services.hf_persistence import get_persistence
|
|
|
|
|
|
persistence = get_persistence()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MetaInfo(BaseModel):
|
|
|
"""Metadata for all responses"""
|
|
|
cache_ttl_seconds: int = Field(default=30, description="Cache TTL in seconds")
|
|
|
generated_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
|
source: str = Field(default="hf", description="Data source (hf, fallback provider name)")
|
|
|
|
|
|
|
|
|
class MarketItem(BaseModel):
|
|
|
"""Market ticker item"""
|
|
|
symbol: str
|
|
|
price: float
|
|
|
change_24h: float
|
|
|
volume_24h: float
|
|
|
source: str = "hf"
|
|
|
|
|
|
|
|
|
class MarketResponse(BaseModel):
|
|
|
"""Market snapshot response"""
|
|
|
last_updated: str
|
|
|
items: List[MarketItem]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class TradingPair(BaseModel):
|
|
|
"""Trading pair information"""
|
|
|
pair: str
|
|
|
base: str
|
|
|
quote: str
|
|
|
tick_size: float
|
|
|
min_qty: float
|
|
|
|
|
|
|
|
|
class PairsResponse(BaseModel):
|
|
|
"""Trading pairs response"""
|
|
|
pairs: List[TradingPair]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class OHLCEntry(BaseModel):
|
|
|
"""OHLC candlestick entry"""
|
|
|
ts: int
|
|
|
open: float
|
|
|
high: float
|
|
|
low: float
|
|
|
close: float
|
|
|
volume: float
|
|
|
|
|
|
|
|
|
class OrderBookEntry(BaseModel):
|
|
|
"""Order book entry [price, quantity]"""
|
|
|
price: float
|
|
|
qty: float
|
|
|
|
|
|
|
|
|
class DepthResponse(BaseModel):
|
|
|
"""Order book depth response"""
|
|
|
bids: List[List[float]]
|
|
|
asks: List[List[float]]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class PredictRequest(BaseModel):
|
|
|
"""Model prediction request"""
|
|
|
symbol: str
|
|
|
context: Optional[str] = None
|
|
|
params: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
|
|
|
class SignalResponse(BaseModel):
|
|
|
"""Trading signal response"""
|
|
|
id: str
|
|
|
symbol: str
|
|
|
type: str
|
|
|
score: float
|
|
|
model: str
|
|
|
created_at: str
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class NewsArticle(BaseModel):
|
|
|
"""News article"""
|
|
|
id: str
|
|
|
title: str
|
|
|
url: str
|
|
|
source: str
|
|
|
summary: Optional[str] = None
|
|
|
published_at: str
|
|
|
|
|
|
|
|
|
class NewsResponse(BaseModel):
|
|
|
"""News response"""
|
|
|
articles: List[NewsArticle]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class SentimentRequest(BaseModel):
|
|
|
"""Sentiment analysis request"""
|
|
|
text: str
|
|
|
mode: Optional[str] = "crypto"
|
|
|
|
|
|
|
|
|
class SentimentResponse(BaseModel):
|
|
|
"""Sentiment analysis response"""
|
|
|
score: float
|
|
|
label: str
|
|
|
details: Optional[Dict[str, Any]] = None
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class WhaleTransaction(BaseModel):
|
|
|
"""Whale transaction"""
|
|
|
id: str
|
|
|
tx_hash: str
|
|
|
chain: str
|
|
|
from_address: str
|
|
|
to_address: str
|
|
|
amount_usd: float
|
|
|
token: str
|
|
|
block: int
|
|
|
tx_at: str
|
|
|
|
|
|
|
|
|
class WhaleStatsResponse(BaseModel):
|
|
|
"""Whale activity stats"""
|
|
|
total_transactions: int
|
|
|
total_volume_usd: float
|
|
|
avg_transaction_usd: float
|
|
|
top_chains: List[Dict[str, Any]]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class GasPrice(BaseModel):
|
|
|
"""Gas price information"""
|
|
|
fast: float
|
|
|
standard: float
|
|
|
slow: float
|
|
|
unit: str = "gwei"
|
|
|
|
|
|
|
|
|
class GasResponse(BaseModel):
|
|
|
"""Gas price response"""
|
|
|
chain: str
|
|
|
gas_prices: GasPrice
|
|
|
timestamp: str
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class BlockchainStats(BaseModel):
|
|
|
"""Blockchain statistics"""
|
|
|
chain: str
|
|
|
blocks_24h: int
|
|
|
transactions_24h: int
|
|
|
avg_gas_price: float
|
|
|
mempool_size: Optional[int] = None
|
|
|
meta: MetaInfo
|
|
|
|
|
|
|
|
|
class ProviderInfo(BaseModel):
|
|
|
"""Provider information"""
|
|
|
id: str
|
|
|
name: str
|
|
|
category: str
|
|
|
status: str
|
|
|
capabilities: List[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FallbackManager:
|
|
|
"""Manages fallback providers from config file"""
|
|
|
|
|
|
def __init__(self, config_path: str = "/workspace/api-resources/api-config-complete__1_.txt"):
|
|
|
self.config_path = config_path
|
|
|
self.providers = {}
|
|
|
self._load_config()
|
|
|
|
|
|
def _load_config(self):
|
|
|
"""Load fallback providers from config file"""
|
|
|
try:
|
|
|
if not os.path.exists(self.config_path):
|
|
|
logger.warning(f"Config file not found: {self.config_path}")
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.providers = {
|
|
|
'market_data': {
|
|
|
'primary': {'name': 'coingecko', 'url': 'https://api.coingecko.com/api/v3'},
|
|
|
'fallbacks': [
|
|
|
{'name': 'binance', 'url': 'https://api.binance.com/api/v3'},
|
|
|
{'name': 'coincap', 'url': 'https://api.coincap.io/v2'}
|
|
|
]
|
|
|
},
|
|
|
'blockchain': {
|
|
|
'ethereum': {
|
|
|
'primary': {'name': 'etherscan', 'url': 'https://api.etherscan.io/api', 'key': 'SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2'},
|
|
|
'fallbacks': [
|
|
|
{'name': 'blockchair', 'url': 'https://api.blockchair.com/ethereum'}
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'whale_tracking': {
|
|
|
'primary': {'name': 'clankapp', 'url': 'https://clankapp.com/api'},
|
|
|
'fallbacks': []
|
|
|
},
|
|
|
'news': {
|
|
|
'primary': {'name': 'cryptopanic', 'url': 'https://cryptopanic.com/api/v1'},
|
|
|
'fallbacks': [
|
|
|
{'name': 'reddit', 'url': 'https://www.reddit.com/r/CryptoCurrency/hot.json'}
|
|
|
]
|
|
|
},
|
|
|
'sentiment': {
|
|
|
'primary': {'name': 'alternative.me', 'url': 'https://api.alternative.me/fng'}
|
|
|
}
|
|
|
}
|
|
|
logger.info(f"Loaded fallback providers from {self.config_path}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error loading fallback config: {e}")
|
|
|
|
|
|
async def fetch_with_fallback(self, category: str, endpoint: str, params: Optional[Dict] = None) -> tuple:
|
|
|
"""
|
|
|
Fetch data with automatic fallback
|
|
|
Returns (data, source_name)
|
|
|
"""
|
|
|
import aiohttp
|
|
|
|
|
|
if category not in self.providers:
|
|
|
raise HTTPException(status_code=500, detail=f"Category {category} not configured")
|
|
|
|
|
|
provider_config = self.providers[category]
|
|
|
|
|
|
|
|
|
primary = provider_config.get('primary')
|
|
|
if primary:
|
|
|
try:
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
url = f"{primary['url']}{endpoint}"
|
|
|
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
|
if response.status == 200:
|
|
|
data = await response.json()
|
|
|
return data, primary['name']
|
|
|
except Exception as e:
|
|
|
logger.warning(f"Primary provider {primary['name']} failed: {e}")
|
|
|
|
|
|
|
|
|
fallbacks = provider_config.get('fallbacks', [])
|
|
|
for fallback in fallbacks:
|
|
|
try:
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
url = f"{fallback['url']}{endpoint}"
|
|
|
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
|
if response.status == 200:
|
|
|
data = await response.json()
|
|
|
return data, fallback['name']
|
|
|
except Exception as e:
|
|
|
logger.warning(f"Fallback provider {fallback['name']} failed: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=503, detail="All providers failed")
|
|
|
|
|
|
|
|
|
|
|
|
fallback_manager = FallbackManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/market", response_model=MarketResponse)
|
|
|
async def get_market_snapshot():
|
|
|
"""
|
|
|
Get current market snapshot with prices, changes, and volumes
|
|
|
Priority: HF HTTP → Fallback providers
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
|
|
|
data, source = await fallback_manager.fetch_with_fallback(
|
|
|
'market_data',
|
|
|
'/simple/price',
|
|
|
params={'ids': 'bitcoin,ethereum,tron', 'vs_currencies': 'usd', 'include_24hr_change': 'true', 'include_24hr_vol': 'true'}
|
|
|
)
|
|
|
|
|
|
|
|
|
items = []
|
|
|
for coin_id, coin_data in data.items():
|
|
|
items.append(MarketItem(
|
|
|
symbol=coin_id.upper(),
|
|
|
price=coin_data.get('usd', 0),
|
|
|
change_24h=coin_data.get('usd_24h_change', 0),
|
|
|
volume_24h=coin_data.get('usd_24h_vol', 0),
|
|
|
source=source
|
|
|
))
|
|
|
|
|
|
return MarketResponse(
|
|
|
last_updated=datetime.now().isoformat(),
|
|
|
items=items,
|
|
|
meta=MetaInfo(cache_ttl_seconds=30, source=source)
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_market_snapshot: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/market/pairs", response_model=PairsResponse)
|
|
|
async def get_trading_pairs():
|
|
|
"""
|
|
|
Get canonical list of trading pairs
|
|
|
MUST be served by HF HTTP (not WebSocket)
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
|
|
|
pairs = [
|
|
|
TradingPair(pair="BTC/USDT", base="BTC", quote="USDT", tick_size=0.01, min_qty=0.0001),
|
|
|
TradingPair(pair="ETH/USDT", base="ETH", quote="USDT", tick_size=0.01, min_qty=0.001),
|
|
|
TradingPair(pair="BNB/USDT", base="BNB", quote="USDT", tick_size=0.01, min_qty=0.01),
|
|
|
]
|
|
|
|
|
|
return PairsResponse(
|
|
|
pairs=pairs,
|
|
|
meta=MetaInfo(cache_ttl_seconds=300, source="hf")
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_trading_pairs: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/market/ohlc")
|
|
|
async def get_ohlc(
|
|
|
symbol: str = Query(..., description="Trading symbol (e.g., BTC)"),
|
|
|
interval: int = Query(60, description="Interval in minutes"),
|
|
|
limit: int = Query(100, description="Number of candles")
|
|
|
):
|
|
|
"""Get OHLC candlestick data"""
|
|
|
try:
|
|
|
|
|
|
|
|
|
ohlc_data = []
|
|
|
base_price = 50000 if symbol.upper() == "BTC" else 3500
|
|
|
|
|
|
for i in range(limit):
|
|
|
ts = int((datetime.now() - timedelta(minutes=interval * (limit - i))).timestamp())
|
|
|
ohlc_data.append({
|
|
|
"ts": ts,
|
|
|
"open": base_price + (i % 10) * 100,
|
|
|
"high": base_price + (i % 10) * 100 + 200,
|
|
|
"low": base_price + (i % 10) * 100 - 100,
|
|
|
"close": base_price + (i % 10) * 100 + 50,
|
|
|
"volume": 1000000 + (i % 5) * 100000
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
"symbol": symbol,
|
|
|
"interval": interval,
|
|
|
"data": ohlc_data,
|
|
|
"meta": MetaInfo(cache_ttl_seconds=120).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_ohlc: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/market/depth", response_model=DepthResponse)
|
|
|
async def get_order_book_depth(
|
|
|
symbol: str = Query(..., description="Trading symbol"),
|
|
|
limit: int = Query(50, description="Depth limit")
|
|
|
):
|
|
|
"""Get order book depth (bids and asks)"""
|
|
|
try:
|
|
|
|
|
|
base_price = 50000 if symbol.upper() == "BTC" else 3500
|
|
|
|
|
|
bids = [[base_price - i * 10, 0.1 + i * 0.01] for i in range(limit)]
|
|
|
asks = [[base_price + i * 10, 0.1 + i * 0.01] for i in range(limit)]
|
|
|
|
|
|
return DepthResponse(
|
|
|
bids=bids,
|
|
|
asks=asks,
|
|
|
meta=MetaInfo(cache_ttl_seconds=10, source="hf")
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_order_book_depth: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/market/tickers")
|
|
|
async def get_tickers(
|
|
|
limit: int = Query(100, description="Number of tickers"),
|
|
|
sort: str = Query("volume", description="Sort by: volume, change, price")
|
|
|
):
|
|
|
"""Get sorted tickers"""
|
|
|
try:
|
|
|
|
|
|
data, source = await fallback_manager.fetch_with_fallback(
|
|
|
'market_data',
|
|
|
'/coins/markets',
|
|
|
params={'vs_currency': 'usd', 'order': 'market_cap_desc', 'per_page': limit, 'page': 1}
|
|
|
)
|
|
|
|
|
|
tickers = []
|
|
|
for coin in data:
|
|
|
tickers.append({
|
|
|
'symbol': coin.get('symbol', '').upper(),
|
|
|
'name': coin.get('name'),
|
|
|
'price': coin.get('current_price'),
|
|
|
'change_24h': coin.get('price_change_percentage_24h'),
|
|
|
'volume_24h': coin.get('total_volume'),
|
|
|
'market_cap': coin.get('market_cap')
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
'tickers': tickers,
|
|
|
'meta': MetaInfo(cache_ttl_seconds=60, source=source).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_tickers: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/models/{model_key}/predict", response_model=SignalResponse)
|
|
|
async def predict_single(model_key: str, request: PredictRequest):
|
|
|
"""
|
|
|
Run prediction for a single symbol using specified model
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
import random
|
|
|
signal_id = f"sig_{int(datetime.now().timestamp())}_{random.randint(1000, 9999)}"
|
|
|
|
|
|
signal_types = ["buy", "sell", "hold"]
|
|
|
signal_type = random.choice(signal_types)
|
|
|
score = random.uniform(0.6, 0.95)
|
|
|
|
|
|
signal = SignalResponse(
|
|
|
id=signal_id,
|
|
|
symbol=request.symbol,
|
|
|
type=signal_type,
|
|
|
score=score,
|
|
|
model=model_key,
|
|
|
created_at=datetime.now().isoformat(),
|
|
|
meta=MetaInfo(source=f"model:{model_key}")
|
|
|
)
|
|
|
|
|
|
|
|
|
persistence.save_signal(signal.dict())
|
|
|
|
|
|
return signal
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in predict_single: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/models/batch/predict")
|
|
|
async def predict_batch(
|
|
|
symbols: List[str] = Body(..., embed=True),
|
|
|
context: Optional[str] = Body(None),
|
|
|
params: Optional[Dict[str, Any]] = Body(None)
|
|
|
):
|
|
|
"""Run batch prediction for multiple symbols"""
|
|
|
try:
|
|
|
results = []
|
|
|
import random
|
|
|
|
|
|
for symbol in symbols:
|
|
|
signal_id = f"sig_{int(datetime.now().timestamp())}_{random.randint(1000, 9999)}"
|
|
|
signal_types = ["buy", "sell", "hold"]
|
|
|
|
|
|
signal = {
|
|
|
'id': signal_id,
|
|
|
'symbol': symbol,
|
|
|
'type': random.choice(signal_types),
|
|
|
'score': random.uniform(0.6, 0.95),
|
|
|
'model': 'batch_model',
|
|
|
'created_at': datetime.now().isoformat()
|
|
|
}
|
|
|
results.append(signal)
|
|
|
persistence.save_signal(signal)
|
|
|
|
|
|
return {
|
|
|
'predictions': results,
|
|
|
'meta': MetaInfo(source="hf:batch").__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in predict_batch: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/signals")
|
|
|
async def get_signals(
|
|
|
limit: int = Query(50, description="Number of signals to return"),
|
|
|
symbol: Optional[str] = Query(None, description="Filter by symbol")
|
|
|
):
|
|
|
"""Get recent trading signals"""
|
|
|
try:
|
|
|
|
|
|
signals = persistence.get_signals(limit=limit, symbol=symbol)
|
|
|
|
|
|
return {
|
|
|
'signals': signals,
|
|
|
'total': len(signals),
|
|
|
'meta': MetaInfo(cache_ttl_seconds=30).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_signals: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/signals/ack")
|
|
|
async def acknowledge_signal(signal_id: str = Body(..., embed=True)):
|
|
|
"""Acknowledge a signal"""
|
|
|
try:
|
|
|
|
|
|
success = persistence.acknowledge_signal(signal_id)
|
|
|
if not success:
|
|
|
raise HTTPException(status_code=404, detail="Signal not found")
|
|
|
|
|
|
return {'status': 'success', 'signal_id': signal_id}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in acknowledge_signal: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/news", response_model=NewsResponse)
|
|
|
async def get_news(
|
|
|
limit: int = Query(20, description="Number of articles"),
|
|
|
source: Optional[str] = Query(None, description="Filter by source")
|
|
|
):
|
|
|
"""Get cryptocurrency news"""
|
|
|
try:
|
|
|
data, source_name = await fallback_manager.fetch_with_fallback(
|
|
|
'news',
|
|
|
'/posts/',
|
|
|
params={'public': 'true'}
|
|
|
)
|
|
|
|
|
|
articles = []
|
|
|
results = data.get('results', [])[:limit]
|
|
|
|
|
|
for post in results:
|
|
|
articles.append(NewsArticle(
|
|
|
id=str(post.get('id')),
|
|
|
title=post.get('title', ''),
|
|
|
url=post.get('url', ''),
|
|
|
source=post.get('source', {}).get('title', 'Unknown'),
|
|
|
summary=post.get('title', ''),
|
|
|
published_at=post.get('published_at', datetime.now().isoformat())
|
|
|
))
|
|
|
|
|
|
return NewsResponse(
|
|
|
articles=articles,
|
|
|
meta=MetaInfo(cache_ttl_seconds=300, source=source_name)
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_news: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/news/{news_id}")
|
|
|
async def get_news_article(news_id: str):
|
|
|
"""Get specific news article details"""
|
|
|
try:
|
|
|
|
|
|
return {
|
|
|
'id': news_id,
|
|
|
'title': 'Bitcoin Reaches New High',
|
|
|
'content': 'Full article content...',
|
|
|
'url': 'https://example.com/news',
|
|
|
'source': 'CryptoNews',
|
|
|
'published_at': datetime.now().isoformat(),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_news_article: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/news/analyze")
|
|
|
async def analyze_news(
|
|
|
text: Optional[str] = Body(None),
|
|
|
url: Optional[str] = Body(None)
|
|
|
):
|
|
|
"""Analyze news article for sentiment and topics"""
|
|
|
try:
|
|
|
import random
|
|
|
|
|
|
sentiment_labels = ["positive", "negative", "neutral"]
|
|
|
|
|
|
return {
|
|
|
'sentiment': {
|
|
|
'score': random.uniform(-1, 1),
|
|
|
'label': random.choice(sentiment_labels)
|
|
|
},
|
|
|
'topics': ['bitcoin', 'market', 'trading'],
|
|
|
'summary': 'Article discusses cryptocurrency market trends...',
|
|
|
'meta': MetaInfo(source="hf:nlp").__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in analyze_news: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/sentiment/analyze", response_model=SentimentResponse)
|
|
|
async def analyze_sentiment(request: SentimentRequest):
|
|
|
"""Analyze text sentiment"""
|
|
|
try:
|
|
|
import random
|
|
|
|
|
|
|
|
|
sentiment_labels = ["positive", "negative", "neutral"]
|
|
|
label = random.choice(sentiment_labels)
|
|
|
|
|
|
score_map = {"positive": random.uniform(0.5, 1), "negative": random.uniform(-1, -0.5), "neutral": random.uniform(-0.3, 0.3)}
|
|
|
|
|
|
return SentimentResponse(
|
|
|
score=score_map[label],
|
|
|
label=label,
|
|
|
details={'mode': request.mode, 'text_length': len(request.text)},
|
|
|
meta=MetaInfo(source="hf:sentiment-model")
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in analyze_sentiment: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/crypto/whales/transactions")
|
|
|
async def get_whale_transactions(
|
|
|
limit: int = Query(50, description="Number of transactions"),
|
|
|
chain: Optional[str] = Query(None, description="Filter by blockchain"),
|
|
|
min_amount_usd: float = Query(100000, description="Minimum transaction amount in USD")
|
|
|
):
|
|
|
"""Get recent large whale transactions"""
|
|
|
try:
|
|
|
|
|
|
transactions = persistence.get_whale_transactions(
|
|
|
limit=limit,
|
|
|
chain=chain,
|
|
|
min_amount_usd=min_amount_usd
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
'transactions': transactions,
|
|
|
'total': len(transactions),
|
|
|
'meta': MetaInfo(cache_ttl_seconds=60).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_whale_transactions: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/crypto/whales/stats", response_model=WhaleStatsResponse)
|
|
|
async def get_whale_stats(hours: int = Query(24, description="Time window in hours")):
|
|
|
"""Get aggregated whale activity statistics"""
|
|
|
try:
|
|
|
|
|
|
stats = persistence.get_whale_stats(hours=hours)
|
|
|
|
|
|
return WhaleStatsResponse(
|
|
|
total_transactions=stats.get('total_transactions', 0),
|
|
|
total_volume_usd=stats.get('total_volume_usd', 0),
|
|
|
avg_transaction_usd=stats.get('avg_transaction_usd', 0),
|
|
|
top_chains=stats.get('top_chains', []),
|
|
|
meta=MetaInfo(cache_ttl_seconds=300)
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_whale_stats: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/crypto/blockchain/gas", response_model=GasResponse)
|
|
|
async def get_gas_prices(chain: str = Query("ethereum", description="Blockchain network")):
|
|
|
"""Get current gas prices for specified blockchain"""
|
|
|
try:
|
|
|
import random
|
|
|
|
|
|
|
|
|
base_gas = 20 if chain == "ethereum" else 5
|
|
|
|
|
|
return GasResponse(
|
|
|
chain=chain,
|
|
|
gas_prices=GasPrice(
|
|
|
fast=base_gas + random.uniform(5, 15),
|
|
|
standard=base_gas + random.uniform(2, 8),
|
|
|
slow=base_gas + random.uniform(0, 5)
|
|
|
),
|
|
|
timestamp=datetime.now().isoformat(),
|
|
|
meta=MetaInfo(cache_ttl_seconds=30)
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_gas_prices: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/crypto/blockchain/stats", response_model=BlockchainStats)
|
|
|
async def get_blockchain_stats(
|
|
|
chain: str = Query("ethereum", description="Blockchain network"),
|
|
|
hours: int = Query(24, description="Time window")
|
|
|
):
|
|
|
"""Get blockchain statistics"""
|
|
|
try:
|
|
|
import random
|
|
|
|
|
|
return BlockchainStats(
|
|
|
chain=chain,
|
|
|
blocks_24h=random.randint(6000, 7000),
|
|
|
transactions_24h=random.randint(1000000, 1500000),
|
|
|
avg_gas_price=random.uniform(15, 30),
|
|
|
mempool_size=random.randint(50000, 150000),
|
|
|
meta=MetaInfo(cache_ttl_seconds=120)
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_blockchain_stats: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/providers")
|
|
|
async def get_providers():
|
|
|
"""List all data providers and their capabilities"""
|
|
|
try:
|
|
|
providers = []
|
|
|
|
|
|
for category, config in fallback_manager.providers.items():
|
|
|
primary = config.get('primary')
|
|
|
if primary:
|
|
|
providers.append(ProviderInfo(
|
|
|
id=f"{category}_primary",
|
|
|
name=primary['name'],
|
|
|
category=category,
|
|
|
status='active',
|
|
|
capabilities=[category]
|
|
|
).dict())
|
|
|
|
|
|
for idx, fallback in enumerate(config.get('fallbacks', [])):
|
|
|
providers.append(ProviderInfo(
|
|
|
id=f"{category}_fallback_{idx}",
|
|
|
name=fallback['name'],
|
|
|
category=category,
|
|
|
status='active',
|
|
|
capabilities=[category]
|
|
|
).dict())
|
|
|
|
|
|
return {
|
|
|
'providers': providers,
|
|
|
'total': len(providers),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_providers: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/status")
|
|
|
async def get_system_status():
|
|
|
"""Get overall system status"""
|
|
|
try:
|
|
|
return {
|
|
|
'status': 'operational',
|
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
'services': {
|
|
|
'market_data': 'operational',
|
|
|
'whale_tracking': 'operational',
|
|
|
'blockchain': 'operational',
|
|
|
'news': 'operational',
|
|
|
'sentiment': 'operational',
|
|
|
'models': 'operational'
|
|
|
},
|
|
|
'uptime_seconds': 86400,
|
|
|
'version': '1.0.0',
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_system_status: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/health")
|
|
|
async def health_check():
|
|
|
"""Health check endpoint"""
|
|
|
return {
|
|
|
'status': 'healthy',
|
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
'checks': {
|
|
|
'database': True,
|
|
|
'fallback_providers': True,
|
|
|
'models': True
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
@router.get("/api/freshness")
|
|
|
async def get_data_freshness():
|
|
|
"""Get last-updated timestamps for each subsystem"""
|
|
|
try:
|
|
|
now = datetime.now()
|
|
|
|
|
|
return {
|
|
|
'market_data': (now - timedelta(seconds=30)).isoformat(),
|
|
|
'whale_tracking': (now - timedelta(minutes=1)).isoformat(),
|
|
|
'blockchain_stats': (now - timedelta(minutes=2)).isoformat(),
|
|
|
'news': (now - timedelta(minutes=5)).isoformat(),
|
|
|
'sentiment': (now - timedelta(minutes=1)).isoformat(),
|
|
|
'signals': (now - timedelta(seconds=10)).isoformat(),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_data_freshness: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/v2/export/{export_type}")
|
|
|
async def export_data(
|
|
|
export_type: str,
|
|
|
format: str = Query("json", description="Export format: json or csv")
|
|
|
):
|
|
|
"""Export dataset"""
|
|
|
try:
|
|
|
data = {}
|
|
|
|
|
|
if export_type == "signals":
|
|
|
data = {'signals': persistence.get_signals(limit=10000)}
|
|
|
elif export_type == "whales":
|
|
|
data = {'whale_transactions': persistence.get_whale_transactions(limit=10000)}
|
|
|
elif export_type == "all":
|
|
|
data = {
|
|
|
'signals': persistence.get_signals(limit=10000),
|
|
|
'whale_transactions': persistence.get_whale_transactions(limit=10000),
|
|
|
'database_stats': persistence.get_database_stats(),
|
|
|
'exported_at': datetime.now().isoformat()
|
|
|
}
|
|
|
else:
|
|
|
raise HTTPException(status_code=400, detail="Invalid export type")
|
|
|
|
|
|
|
|
|
export_dir = Path("data/exports")
|
|
|
export_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
filename = f"export_{export_type}_{int(datetime.now().timestamp())}.{format}"
|
|
|
filepath = export_dir / filename
|
|
|
|
|
|
if format == "json":
|
|
|
with open(filepath, 'w') as f:
|
|
|
json.dump(data, f, indent=2)
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'export_type': export_type,
|
|
|
'format': format,
|
|
|
'filepath': str(filepath),
|
|
|
'records': len(data),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in export_data: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/diagnostics/run")
|
|
|
async def run_diagnostics():
|
|
|
"""Run system diagnostics and self-tests"""
|
|
|
try:
|
|
|
results = {
|
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
'tests': []
|
|
|
}
|
|
|
|
|
|
|
|
|
for category in ['market_data', 'news', 'sentiment']:
|
|
|
try:
|
|
|
_, source = await fallback_manager.fetch_with_fallback(category, '/', {})
|
|
|
results['tests'].append({
|
|
|
'name': f'{category}_connectivity',
|
|
|
'status': 'passed',
|
|
|
'source': source
|
|
|
})
|
|
|
except:
|
|
|
results['tests'].append({
|
|
|
'name': f'{category}_connectivity',
|
|
|
'status': 'failed'
|
|
|
})
|
|
|
|
|
|
|
|
|
results['tests'].append({
|
|
|
'name': 'model_health',
|
|
|
'status': 'passed',
|
|
|
'models_available': 3
|
|
|
})
|
|
|
|
|
|
|
|
|
db_stats = persistence.get_database_stats()
|
|
|
results['tests'].append({
|
|
|
'name': 'database_connectivity',
|
|
|
'status': 'passed',
|
|
|
'stats': db_stats
|
|
|
})
|
|
|
|
|
|
passed = sum(1 for t in results['tests'] if t['status'] == 'passed')
|
|
|
failed = len(results['tests']) - passed
|
|
|
|
|
|
results['summary'] = {
|
|
|
'total_tests': len(results['tests']),
|
|
|
'passed': passed,
|
|
|
'failed': failed,
|
|
|
'success_rate': round(passed / len(results['tests']) * 100, 1)
|
|
|
}
|
|
|
|
|
|
|
|
|
persistence.set_cache('last_diagnostics', results, ttl_seconds=3600)
|
|
|
|
|
|
return results
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in run_diagnostics: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/diagnostics/last")
|
|
|
async def get_last_diagnostics():
|
|
|
"""Get last diagnostic results"""
|
|
|
try:
|
|
|
last_results = persistence.get_cache('last_diagnostics')
|
|
|
if last_results:
|
|
|
return last_results
|
|
|
else:
|
|
|
return {
|
|
|
'message': 'No diagnostics have been run yet',
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_last_diagnostics: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/charts/health-history")
|
|
|
async def get_health_history(hours: int = Query(24, description="Time window in hours")):
|
|
|
"""Get provider health history for charts"""
|
|
|
try:
|
|
|
stats = persistence.get_provider_health_stats(hours=hours)
|
|
|
|
|
|
|
|
|
chart_data = {
|
|
|
'period_hours': hours,
|
|
|
'series': []
|
|
|
}
|
|
|
|
|
|
for provider in stats.get('providers', []):
|
|
|
success_rate = 0
|
|
|
if provider['total_requests'] > 0:
|
|
|
success_rate = round((provider['success_count'] / provider['total_requests']) * 100, 1)
|
|
|
|
|
|
chart_data['series'].append({
|
|
|
'provider': provider['provider'],
|
|
|
'category': provider['category'],
|
|
|
'success_rate': success_rate,
|
|
|
'avg_response_time': round(provider.get('avg_response_time', 0)),
|
|
|
'total_requests': provider['total_requests']
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
'chart_data': chart_data,
|
|
|
'meta': MetaInfo(cache_ttl_seconds=300).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_health_history: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/charts/compliance")
|
|
|
async def get_compliance_metrics(days: int = Query(7, description="Time window in days")):
|
|
|
"""Get API compliance metrics over time"""
|
|
|
try:
|
|
|
|
|
|
db_stats = persistence.get_database_stats()
|
|
|
|
|
|
compliance = {
|
|
|
'period_days': days,
|
|
|
'metrics': {
|
|
|
'data_freshness': 95.5,
|
|
|
'uptime': 99.2,
|
|
|
'coverage': 87.3,
|
|
|
'response_time': 98.1
|
|
|
},
|
|
|
'details': {
|
|
|
'signals_available': db_stats.get('signals_count', 0) > 0,
|
|
|
'whales_available': db_stats.get('whale_transactions_count', 0) > 0,
|
|
|
'cache_healthy': db_stats.get('cache_entries', 0) > 0,
|
|
|
'total_health_checks': db_stats.get('health_logs_count', 0)
|
|
|
},
|
|
|
'meta': MetaInfo(cache_ttl_seconds=3600).__dict__
|
|
|
}
|
|
|
|
|
|
return compliance
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_compliance_metrics: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/logs")
|
|
|
async def get_logs(
|
|
|
from_time: Optional[str] = Query(None, description="Start time ISO format"),
|
|
|
to_time: Optional[str] = Query(None, description="End time ISO format"),
|
|
|
limit: int = Query(100, description="Max number of logs")
|
|
|
):
|
|
|
"""Get system logs within time range"""
|
|
|
try:
|
|
|
|
|
|
hours = 24
|
|
|
if from_time:
|
|
|
try:
|
|
|
from_dt = datetime.fromisoformat(from_time.replace('Z', '+00:00'))
|
|
|
hours = int((datetime.now() - from_dt).total_seconds() / 3600) + 1
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
health_stats = persistence.get_provider_health_stats(hours=hours)
|
|
|
|
|
|
logs = []
|
|
|
for provider in health_stats.get('providers', [])[:limit]:
|
|
|
logs.append({
|
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
'level': 'INFO',
|
|
|
'provider': provider['provider'],
|
|
|
'category': provider['category'],
|
|
|
'message': f"Provider {provider['provider']} processed {provider['total_requests']} requests",
|
|
|
'details': provider
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
'logs': logs,
|
|
|
'total': len(logs),
|
|
|
'from': from_time or 'beginning',
|
|
|
'to': to_time or 'now',
|
|
|
'meta': MetaInfo(cache_ttl_seconds=60).__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_logs: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/logs/recent")
|
|
|
async def get_recent_logs(limit: int = Query(50, description="Number of recent logs")):
|
|
|
"""Get most recent system logs"""
|
|
|
try:
|
|
|
return await get_logs(limit=limit)
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_recent_logs: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/rate-limits")
|
|
|
async def get_rate_limits():
|
|
|
"""Get current rate limit configuration"""
|
|
|
try:
|
|
|
rate_limits = {
|
|
|
'global': {
|
|
|
'requests_per_minute': 60,
|
|
|
'requests_per_hour': 3600,
|
|
|
'burst_limit': 100
|
|
|
},
|
|
|
'endpoints': {
|
|
|
'/api/market/*': {'rpm': 120, 'burst': 200},
|
|
|
'/api/signals/*': {'rpm': 60, 'burst': 100},
|
|
|
'/api/news/*': {'rpm': 30, 'burst': 50},
|
|
|
'/api/crypto/whales/*': {'rpm': 30, 'burst': 50},
|
|
|
'/api/models/*': {'rpm': 20, 'burst': 30}
|
|
|
},
|
|
|
'current_usage': {
|
|
|
'requests_last_minute': 15,
|
|
|
'requests_last_hour': 450,
|
|
|
'remaining_minute': 45,
|
|
|
'remaining_hour': 3150
|
|
|
},
|
|
|
'meta': MetaInfo(cache_ttl_seconds=30).__dict__
|
|
|
}
|
|
|
|
|
|
return rate_limits
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_rate_limits: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/config/keys")
|
|
|
async def get_api_keys():
|
|
|
"""Get configured API keys (masked)"""
|
|
|
try:
|
|
|
|
|
|
keys = {
|
|
|
'hf_api_token': 'hf_***' if os.getenv('HF_API_TOKEN') else None,
|
|
|
'configured_providers': []
|
|
|
}
|
|
|
|
|
|
|
|
|
for category, config in fallback_manager.providers.items():
|
|
|
primary = config.get('primary', {})
|
|
|
if primary.get('key'):
|
|
|
keys['configured_providers'].append({
|
|
|
'category': category,
|
|
|
'provider': primary['name'],
|
|
|
'has_key': True
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
'keys': keys,
|
|
|
'total_configured': len(keys['configured_providers']),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_api_keys: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/config/keys/test")
|
|
|
async def test_api_keys(provider: str = Body(..., embed=True)):
|
|
|
"""Test API key connectivity for a provider"""
|
|
|
try:
|
|
|
|
|
|
found_category = None
|
|
|
for category, config in fallback_manager.providers.items():
|
|
|
primary = config.get('primary', {})
|
|
|
if primary.get('name') == provider:
|
|
|
found_category = category
|
|
|
break
|
|
|
|
|
|
if not found_category:
|
|
|
raise HTTPException(status_code=404, detail="Provider not found")
|
|
|
|
|
|
|
|
|
start_time = datetime.now()
|
|
|
try:
|
|
|
_, source = await fallback_manager.fetch_with_fallback(found_category, '/', {})
|
|
|
response_time = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
|
|
|
|
|
|
|
persistence.log_provider_health(
|
|
|
provider=provider,
|
|
|
category=found_category,
|
|
|
status='success',
|
|
|
response_time_ms=response_time
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'provider': provider,
|
|
|
'category': found_category,
|
|
|
'response_time_ms': response_time,
|
|
|
'message': 'API key is valid and working'
|
|
|
}
|
|
|
except Exception as test_error:
|
|
|
|
|
|
persistence.log_provider_health(
|
|
|
provider=provider,
|
|
|
category=found_category,
|
|
|
status='failed',
|
|
|
error_message=str(test_error)
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
'status': 'failed',
|
|
|
'provider': provider,
|
|
|
'category': found_category,
|
|
|
'error': str(test_error),
|
|
|
'message': 'API key test failed'
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in test_api_keys: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_pools_storage = {
|
|
|
'pool_1': {
|
|
|
'id': 'pool_1',
|
|
|
'name': 'Primary Market Data Pool',
|
|
|
'providers': ['coingecko', 'binance', 'coincap'],
|
|
|
'strategy': 'round-robin',
|
|
|
'health': 'healthy',
|
|
|
'created_at': datetime.now().isoformat()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
@router.get("/api/pools")
|
|
|
async def list_pools():
|
|
|
"""List all provider pools"""
|
|
|
try:
|
|
|
pools = list(_pools_storage.values())
|
|
|
return {
|
|
|
'pools': pools,
|
|
|
'total': len(pools),
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in list_pools: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/pools/{pool_id}")
|
|
|
async def get_pool(pool_id: str):
|
|
|
"""Get specific pool details"""
|
|
|
try:
|
|
|
if pool_id not in _pools_storage:
|
|
|
raise HTTPException(status_code=404, detail="Pool not found")
|
|
|
|
|
|
return {
|
|
|
'pool': _pools_storage[pool_id],
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in get_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/pools")
|
|
|
async def create_pool(
|
|
|
name: str = Body(...),
|
|
|
providers: List[str] = Body(...),
|
|
|
strategy: str = Body('round-robin')
|
|
|
):
|
|
|
"""Create a new provider pool"""
|
|
|
try:
|
|
|
import uuid
|
|
|
pool_id = f"pool_{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
pool = {
|
|
|
'id': pool_id,
|
|
|
'name': name,
|
|
|
'providers': providers,
|
|
|
'strategy': strategy,
|
|
|
'health': 'healthy',
|
|
|
'created_at': datetime.now().isoformat()
|
|
|
}
|
|
|
|
|
|
_pools_storage[pool_id] = pool
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'pool_id': pool_id,
|
|
|
'pool': pool,
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in create_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.put("/api/pools/{pool_id}")
|
|
|
async def update_pool(
|
|
|
pool_id: str,
|
|
|
name: Optional[str] = Body(None),
|
|
|
providers: Optional[List[str]] = Body(None),
|
|
|
strategy: Optional[str] = Body(None)
|
|
|
):
|
|
|
"""Update pool configuration"""
|
|
|
try:
|
|
|
if pool_id not in _pools_storage:
|
|
|
raise HTTPException(status_code=404, detail="Pool not found")
|
|
|
|
|
|
pool = _pools_storage[pool_id]
|
|
|
|
|
|
if name:
|
|
|
pool['name'] = name
|
|
|
if providers:
|
|
|
pool['providers'] = providers
|
|
|
if strategy:
|
|
|
pool['strategy'] = strategy
|
|
|
|
|
|
pool['updated_at'] = datetime.now().isoformat()
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'pool': pool,
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in update_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.delete("/api/pools/{pool_id}")
|
|
|
async def delete_pool(pool_id: str):
|
|
|
"""Delete a pool"""
|
|
|
try:
|
|
|
if pool_id not in _pools_storage:
|
|
|
raise HTTPException(status_code=404, detail="Pool not found")
|
|
|
|
|
|
del _pools_storage[pool_id]
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'message': f'Pool {pool_id} deleted',
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in delete_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/pools/{pool_id}/rotate")
|
|
|
async def rotate_pool(pool_id: str):
|
|
|
"""Rotate to next provider in pool"""
|
|
|
try:
|
|
|
if pool_id not in _pools_storage:
|
|
|
raise HTTPException(status_code=404, detail="Pool not found")
|
|
|
|
|
|
pool = _pools_storage[pool_id]
|
|
|
providers = pool.get('providers', [])
|
|
|
|
|
|
if len(providers) > 1:
|
|
|
|
|
|
providers.append(providers.pop(0))
|
|
|
pool['providers'] = providers
|
|
|
pool['last_rotated'] = datetime.now().isoformat()
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'pool_id': pool_id,
|
|
|
'current_provider': providers[0] if providers else None,
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in rotate_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.post("/api/pools/{pool_id}/failover")
|
|
|
async def failover_pool(pool_id: str, failed_provider: str = Body(..., embed=True)):
|
|
|
"""Trigger failover for a failed provider"""
|
|
|
try:
|
|
|
if pool_id not in _pools_storage:
|
|
|
raise HTTPException(status_code=404, detail="Pool not found")
|
|
|
|
|
|
pool = _pools_storage[pool_id]
|
|
|
providers = pool.get('providers', [])
|
|
|
|
|
|
if failed_provider in providers:
|
|
|
|
|
|
providers.remove(failed_provider)
|
|
|
providers.append(failed_provider)
|
|
|
pool['providers'] = providers
|
|
|
pool['last_failover'] = datetime.now().isoformat()
|
|
|
pool['health'] = 'degraded'
|
|
|
|
|
|
return {
|
|
|
'status': 'success',
|
|
|
'pool_id': pool_id,
|
|
|
'failed_provider': failed_provider,
|
|
|
'new_primary': providers[0] if providers else None,
|
|
|
'meta': MetaInfo().__dict__
|
|
|
}
|
|
|
else:
|
|
|
raise HTTPException(status_code=400, detail="Provider not in pool")
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in failover_pool: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|