|
|
| """
|
| API Server Extended - HuggingFace Spaces Deployment Ready
|
| Complete Admin API with Real Data Only - NO MOCKS
|
| """
|
|
|
| import os
|
| import asyncio
|
| import sqlite3
|
| import httpx
|
| import json
|
| import subprocess
|
| from pathlib import Path
|
| from typing import Optional, Dict, Any, List
|
| from datetime import datetime
|
| from contextlib import asynccontextmanager
|
| from collections import defaultdict
|
|
|
| from fastapi import FastAPI, HTTPException
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
|
| from fastapi.staticfiles import StaticFiles
|
| from pydantic import BaseModel
|
|
|
|
|
| USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "false").lower() == "true"
|
| PORT = int(os.getenv("PORT", "7860"))
|
|
|
|
|
| WORKSPACE_ROOT = Path("/workspace" if Path("/workspace").exists() else ".")
|
| DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
|
| LOG_DIR = WORKSPACE_ROOT / "logs"
|
| PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
|
| APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
|
|
|
|
|
| DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
| _provider_state = {
|
| "providers": {},
|
| "pools": {},
|
| "logs": [],
|
| "last_check": None,
|
| "stats": {"total": 0, "online": 0, "offline": 0, "degraded": 0}
|
| }
|
|
|
|
|
|
|
| def init_database():
|
| """Initialize SQLite database with required tables"""
|
| conn = sqlite3.connect(str(DB_PATH))
|
| cursor = conn.cursor()
|
|
|
| cursor.execute("""
|
| CREATE TABLE IF NOT EXISTS prices (
|
| id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| symbol TEXT NOT NULL,
|
| name TEXT,
|
| price_usd REAL NOT NULL,
|
| volume_24h REAL,
|
| market_cap REAL,
|
| percent_change_24h REAL,
|
| rank INTEGER,
|
| timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| )
|
| """)
|
|
|
| cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
|
| cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
|
|
|
| conn.commit()
|
| conn.close()
|
| print(f"✓ Database initialized at {DB_PATH}")
|
|
|
|
|
| def save_price_to_db(price_data: Dict[str, Any]):
|
| """Save price data to SQLite"""
|
| try:
|
| conn = sqlite3.connect(str(DB_PATH))
|
| cursor = conn.cursor()
|
| cursor.execute("""
|
| INSERT INTO prices (symbol, name, price_usd, volume_24h, market_cap, percent_change_24h, rank)
|
| VALUES (?, ?, ?, ?, ?, ?, ?)
|
| """, (
|
| price_data.get("symbol"),
|
| price_data.get("name"),
|
| price_data.get("price_usd", 0.0),
|
| price_data.get("volume_24h"),
|
| price_data.get("market_cap"),
|
| price_data.get("percent_change_24h"),
|
| price_data.get("rank")
|
| ))
|
| conn.commit()
|
| conn.close()
|
| except Exception as e:
|
| print(f"Error saving price to database: {e}")
|
|
|
|
|
| def get_price_history_from_db(symbol: str, limit: int = 10) -> List[Dict[str, Any]]:
|
| """Get price history from SQLite"""
|
| try:
|
| conn = sqlite3.connect(str(DB_PATH))
|
| conn.row_factory = sqlite3.Row
|
| cursor = conn.cursor()
|
| cursor.execute("""
|
| SELECT * FROM prices
|
| WHERE symbol = ?
|
| ORDER BY timestamp DESC
|
| LIMIT ?
|
| """, (symbol, limit))
|
| rows = cursor.fetchall()
|
| conn.close()
|
| return [dict(row) for row in rows]
|
| except Exception as e:
|
| print(f"Error fetching price history: {e}")
|
| return []
|
|
|
|
|
|
|
| def load_providers_config() -> Dict[str, Any]:
|
| """Load providers from config file"""
|
| try:
|
| if PROVIDERS_CONFIG_PATH.exists():
|
| with open(PROVIDERS_CONFIG_PATH, 'r') as f:
|
| return json.load(f)
|
| return {"providers": {}}
|
| except Exception as e:
|
| print(f"Error loading providers config: {e}")
|
| return {"providers": {}}
|
|
|
|
|
| def load_apl_report() -> Dict[str, Any]:
|
| """Load APL validation report"""
|
| try:
|
| if APL_REPORT_PATH.exists():
|
| with open(APL_REPORT_PATH, 'r') as f:
|
| return json.load(f)
|
| return {}
|
| except Exception as e:
|
| print(f"Error loading APL report: {e}")
|
| return {}
|
|
|
|
|
|
|
| HEADERS = {
|
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
| "Accept": "application/json"
|
| }
|
|
|
|
|
| async def fetch_coingecko_simple_price() -> Dict[str, Any]:
|
| """Fetch real price data from CoinGecko API"""
|
| url = "https://api.coingecko.com/api/v3/simple/price"
|
| params = {
|
| "ids": "bitcoin,ethereum,binancecoin",
|
| "vs_currencies": "usd",
|
| "include_market_cap": "true",
|
| "include_24hr_vol": "true",
|
| "include_24hr_change": "true"
|
| }
|
|
|
| async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
|
| response = await client.get(url, params=params)
|
| if response.status_code != 200:
|
| raise HTTPException(status_code=503, detail=f"CoinGecko API error: HTTP {response.status_code}")
|
| return response.json()
|
|
|
|
|
| async def fetch_fear_greed_index() -> Dict[str, Any]:
|
| """Fetch real Fear & Greed Index from Alternative.me"""
|
| url = "https://api.alternative.me/fng/"
|
| params = {"limit": "1", "format": "json"}
|
|
|
| async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
|
| response = await client.get(url, params=params)
|
| if response.status_code != 200:
|
| raise HTTPException(status_code=503, detail=f"Alternative.me API error: HTTP {response.status_code}")
|
| return response.json()
|
|
|
|
|
| async def fetch_coingecko_trending() -> Dict[str, Any]:
|
| """Fetch real trending coins from CoinGecko"""
|
| url = "https://api.coingecko.com/api/v3/search/trending"
|
|
|
| async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
|
| response = await client.get(url)
|
| if response.status_code != 200:
|
| raise HTTPException(status_code=503, detail=f"CoinGecko trending API error: HTTP {response.status_code}")
|
| return response.json()
|
|
|
|
|
|
|
| @asynccontextmanager
|
| async def lifespan(app: FastAPI):
|
| """Application lifespan manager"""
|
| print("=" * 80)
|
| print("🚀 Starting Crypto Monitor Admin API")
|
| print("=" * 80)
|
| init_database()
|
|
|
|
|
| config = load_providers_config()
|
| _provider_state["providers"] = config.get("providers", {})
|
| print(f"✓ Loaded {len(_provider_state['providers'])} providers from config")
|
|
|
|
|
| apl_report = load_apl_report()
|
| if apl_report:
|
| print(f"✓ Loaded APL report with validation data")
|
|
|
| print(f"✓ Server ready on port {PORT}")
|
| print("=" * 80)
|
| yield
|
| print("Shutting down...")
|
|
|
|
|
|
|
| app = FastAPI(
|
| title="Crypto Monitor Admin API",
|
| description="Real-time cryptocurrency data API with Admin Dashboard",
|
| version="5.0.0",
|
| lifespan=lifespan
|
| )
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
| try:
|
| static_path = WORKSPACE_ROOT / "static"
|
| if static_path.exists():
|
| app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
| print(f"✓ Mounted static files from {static_path}")
|
| except Exception as e:
|
| print(f"⚠ Could not mount static files: {e}")
|
|
|
|
|
|
|
| @app.get("/", response_class=HTMLResponse)
|
| async def serve_admin_dashboard():
|
| """Serve admin dashboard"""
|
| html_path = WORKSPACE_ROOT / "admin.html"
|
| if html_path.exists():
|
| return FileResponse(html_path)
|
| return HTMLResponse("<h1>Admin Dashboard</h1><p>admin.html not found</p>")
|
|
|
|
|
|
|
| @app.get("/health")
|
| async def health():
|
| """Health check endpoint"""
|
| return {
|
| "status": "healthy",
|
| "timestamp": datetime.now().isoformat(),
|
| "database": str(DB_PATH),
|
| "use_mock_data": USE_MOCK_DATA,
|
| "providers_loaded": len(_provider_state["providers"])
|
| }
|
|
|
|
|
| @app.get("/api/status")
|
| async def get_status():
|
| """System status"""
|
| config = load_providers_config()
|
| providers = config.get("providers", {})
|
|
|
|
|
| validated_count = sum(1 for p in providers.values() if p.get("validated"))
|
|
|
| return {
|
| "system_health": "healthy",
|
| "timestamp": datetime.now().isoformat(),
|
| "total_providers": len(providers),
|
| "validated_providers": validated_count,
|
| "database_status": "connected",
|
| "apl_available": APL_REPORT_PATH.exists(),
|
| "use_mock_data": USE_MOCK_DATA
|
| }
|
|
|
|
|
| @app.get("/api/stats")
|
| async def get_stats():
|
| """System statistics"""
|
| config = load_providers_config()
|
| providers = config.get("providers", {})
|
|
|
|
|
| categories = defaultdict(int)
|
| for p in providers.values():
|
| cat = p.get("category", "unknown")
|
| categories[cat] += 1
|
|
|
| return {
|
| "total_providers": len(providers),
|
| "categories": dict(categories),
|
| "total_categories": len(categories),
|
| "timestamp": datetime.now().isoformat()
|
| }
|
|
|
|
|
|
|
| @app.get("/api/market")
|
| async def get_market_data():
|
| """Market data from CoinGecko - REAL DATA ONLY"""
|
| try:
|
| data = await fetch_coingecko_simple_price()
|
|
|
| cryptocurrencies = []
|
| coin_mapping = {
|
| "bitcoin": {"name": "Bitcoin", "symbol": "BTC", "rank": 1, "image": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"},
|
| "ethereum": {"name": "Ethereum", "symbol": "ETH", "rank": 2, "image": "https://assets.coingecko.com/coins/images/279/small/ethereum.png"},
|
| "binancecoin": {"name": "BNB", "symbol": "BNB", "rank": 3, "image": "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png"}
|
| }
|
|
|
| for coin_id, coin_info in coin_mapping.items():
|
| if coin_id in data:
|
| coin_data = data[coin_id]
|
| crypto_entry = {
|
| "rank": coin_info["rank"],
|
| "name": coin_info["name"],
|
| "symbol": coin_info["symbol"],
|
| "price": coin_data.get("usd", 0),
|
| "change_24h": coin_data.get("usd_24h_change", 0),
|
| "market_cap": coin_data.get("usd_market_cap", 0),
|
| "volume_24h": coin_data.get("usd_24h_vol", 0),
|
| "image": coin_info["image"]
|
| }
|
| cryptocurrencies.append(crypto_entry)
|
|
|
|
|
| save_price_to_db({
|
| "symbol": coin_info["symbol"],
|
| "name": coin_info["name"],
|
| "price_usd": crypto_entry["price"],
|
| "volume_24h": crypto_entry["volume_24h"],
|
| "market_cap": crypto_entry["market_cap"],
|
| "percent_change_24h": crypto_entry["change_24h"],
|
| "rank": coin_info["rank"]
|
| })
|
|
|
|
|
| total_market_cap = sum(c["market_cap"] for c in cryptocurrencies)
|
| btc_dominance = 0
|
| if total_market_cap > 0:
|
| btc_entry = next((c for c in cryptocurrencies if c["symbol"] == "BTC"), None)
|
| if btc_entry:
|
| btc_dominance = (btc_entry["market_cap"] / total_market_cap) * 100
|
|
|
| return {
|
| "cryptocurrencies": cryptocurrencies,
|
| "total_market_cap": total_market_cap,
|
| "btc_dominance": btc_dominance,
|
| "timestamp": datetime.now().isoformat(),
|
| "source": "CoinGecko API (Real Data)"
|
| }
|
|
|
| except Exception as e:
|
| raise HTTPException(status_code=503, detail=f"Failed to fetch market data: {str(e)}")
|
|
|
|
|
| @app.get("/api/market/history")
|
| async def get_market_history(symbol: str = "BTC", limit: int = 10):
|
| """Get price history from database - REAL DATA ONLY"""
|
| history = get_price_history_from_db(symbol.upper(), limit)
|
|
|
| if not history:
|
| return {
|
| "symbol": symbol,
|
| "history": [],
|
| "count": 0,
|
| "message": "No history available"
|
| }
|
|
|
| return {
|
| "symbol": symbol,
|
| "history": history,
|
| "count": len(history),
|
| "source": "SQLite Database (Real Data)"
|
| }
|
|
|
|
|
| @app.get("/api/sentiment")
|
| async def get_sentiment():
|
| """Sentiment data from Alternative.me - REAL DATA ONLY"""
|
| try:
|
| data = await fetch_fear_greed_index()
|
|
|
| if "data" in data and len(data["data"]) > 0:
|
| fng_data = data["data"][0]
|
| return {
|
| "fear_greed_index": int(fng_data["value"]),
|
| "fear_greed_label": fng_data["value_classification"],
|
| "timestamp": datetime.now().isoformat(),
|
| "source": "Alternative.me API (Real Data)"
|
| }
|
|
|
| raise HTTPException(status_code=503, detail="Invalid response from Alternative.me")
|
|
|
| except Exception as e:
|
| raise HTTPException(status_code=503, detail=f"Failed to fetch sentiment: {str(e)}")
|
|
|
|
|
| @app.get("/api/trending")
|
| async def get_trending():
|
| """Trending coins from CoinGecko - REAL DATA ONLY"""
|
| try:
|
| data = await fetch_coingecko_trending()
|
|
|
| trending_coins = []
|
| if "coins" in data:
|
| for item in data["coins"][:10]:
|
| coin = item.get("item", {})
|
| trending_coins.append({
|
| "id": coin.get("id"),
|
| "name": coin.get("name"),
|
| "symbol": coin.get("symbol"),
|
| "market_cap_rank": coin.get("market_cap_rank"),
|
| "thumb": coin.get("thumb"),
|
| "score": coin.get("score", 0)
|
| })
|
|
|
| return {
|
| "trending": trending_coins,
|
| "count": len(trending_coins),
|
| "timestamp": datetime.now().isoformat(),
|
| "source": "CoinGecko API (Real Data)"
|
| }
|
|
|
| except Exception as e:
|
| raise HTTPException(status_code=503, detail=f"Failed to fetch trending: {str(e)}")
|
|
|
|
|
|
|
| @app.get("/api/providers")
|
| async def get_providers():
|
| """Get all providers - REAL DATA from config"""
|
| config = load_providers_config()
|
| providers = config.get("providers", {})
|
|
|
| result = []
|
| for provider_id, provider_data in providers.items():
|
| result.append({
|
| "provider_id": provider_id,
|
| "name": provider_data.get("name", provider_id),
|
| "category": provider_data.get("category", "unknown"),
|
| "type": provider_data.get("type", "unknown"),
|
| "status": "validated" if provider_data.get("validated") else "unvalidated",
|
| "validated_at": provider_data.get("validated_at"),
|
| "response_time_ms": provider_data.get("response_time_ms"),
|
| "added_by": provider_data.get("added_by", "manual")
|
| })
|
|
|
| return {
|
| "providers": result,
|
| "total": len(result),
|
| "source": "providers_config_extended.json (Real Data)"
|
| }
|
|
|
|
|
| @app.get("/api/providers/{provider_id}")
|
| async def get_provider_detail(provider_id: str):
|
| """Get specific provider details"""
|
| config = load_providers_config()
|
| providers = config.get("providers", {})
|
|
|
| if provider_id not in providers:
|
| raise HTTPException(status_code=404, detail=f"Provider {provider_id} not found")
|
|
|
| return {
|
| "provider_id": provider_id,
|
| **providers[provider_id]
|
| }
|
|
|
|
|
| @app.get("/api/providers/category/{category}")
|
| async def get_providers_by_category(category: str):
|
| """Get providers by category"""
|
| config = load_providers_config()
|
| providers = config.get("providers", {})
|
|
|
| filtered = {
|
| pid: data for pid, data in providers.items()
|
| if data.get("category") == category
|
| }
|
|
|
| return {
|
| "category": category,
|
| "providers": filtered,
|
| "count": len(filtered)
|
| }
|
|
|
|
|
|
|
| @app.get("/api/pools")
|
| async def get_pools():
|
| """Get provider pools"""
|
| return {
|
| "pools": [],
|
| "message": "Pools feature not yet implemented in this version"
|
| }
|
|
|
|
|
|
|
| @app.get("/api/logs/recent")
|
| async def get_recent_logs():
|
| """Get recent logs"""
|
| return {
|
| "logs": _provider_state.get("logs", [])[-50:],
|
| "count": min(50, len(_provider_state.get("logs", [])))
|
| }
|
|
|
|
|
| @app.get("/api/logs/errors")
|
| async def get_error_logs():
|
| """Get error logs"""
|
| all_logs = _provider_state.get("logs", [])
|
| errors = [log for log in all_logs if log.get("level") == "ERROR"]
|
| return {
|
| "errors": errors[-50:],
|
| "count": len(errors)
|
| }
|
|
|
|
|
|
|
| @app.post("/api/diagnostics/run")
|
| async def run_diagnostics(auto_fix: bool = False):
|
| """Run system diagnostics"""
|
| issues = []
|
| fixes_applied = []
|
|
|
|
|
| if not DB_PATH.exists():
|
| issues.append({"type": "database", "message": "Database file not found"})
|
| if auto_fix:
|
| init_database()
|
| fixes_applied.append("Initialized database")
|
|
|
|
|
| if not PROVIDERS_CONFIG_PATH.exists():
|
| issues.append({"type": "config", "message": "Providers config not found"})
|
|
|
|
|
| if not APL_REPORT_PATH.exists():
|
| issues.append({"type": "apl", "message": "APL report not found"})
|
|
|
| return {
|
| "status": "completed",
|
| "issues_found": len(issues),
|
| "issues": issues,
|
| "fixes_applied": fixes_applied if auto_fix else [],
|
| "timestamp": datetime.now().isoformat()
|
| }
|
|
|
|
|
| @app.get("/api/diagnostics/last")
|
| async def get_last_diagnostics():
|
| """Get last diagnostics results"""
|
|
|
| return {
|
| "status": "no_previous_run",
|
| "message": "No previous diagnostics run found"
|
| }
|
|
|
|
|
|
|
| @app.post("/api/apl/run")
|
| async def run_apl_scan():
|
| """Run APL provider scan"""
|
| try:
|
|
|
| result = subprocess.run(
|
| ["python3", str(WORKSPACE_ROOT / "auto_provider_loader.py")],
|
| capture_output=True,
|
| text=True,
|
| timeout=300,
|
| cwd=str(WORKSPACE_ROOT)
|
| )
|
|
|
|
|
| config = load_providers_config()
|
| _provider_state["providers"] = config.get("providers", {})
|
|
|
| return {
|
| "status": "completed",
|
| "stdout": result.stdout[-1000:],
|
| "returncode": result.returncode,
|
| "providers_count": len(_provider_state["providers"]),
|
| "timestamp": datetime.now().isoformat()
|
| }
|
|
|
| except subprocess.TimeoutExpired:
|
| return {
|
| "status": "timeout",
|
| "message": "APL scan timed out after 5 minutes"
|
| }
|
| except Exception as e:
|
| raise HTTPException(status_code=500, detail=f"APL scan failed: {str(e)}")
|
|
|
|
|
| @app.get("/api/apl/report")
|
| async def get_apl_report():
|
| """Get APL validation report"""
|
| report = load_apl_report()
|
|
|
| if not report:
|
| return {
|
| "status": "not_available",
|
| "message": "APL report not found. Run APL scan first."
|
| }
|
|
|
| return report
|
|
|
|
|
| @app.get("/api/apl/summary")
|
| async def get_apl_summary():
|
| """Get APL summary statistics"""
|
| report = load_apl_report()
|
|
|
| if not report or "stats" not in report:
|
| return {
|
| "status": "not_available",
|
| "message": "APL report not found"
|
| }
|
|
|
| stats = report.get("stats", {})
|
| return {
|
| "http_candidates": stats.get("total_http_candidates", 0),
|
| "http_valid": stats.get("http_valid", 0),
|
| "http_invalid": stats.get("http_invalid", 0),
|
| "http_conditional": stats.get("http_conditional", 0),
|
| "hf_candidates": stats.get("total_hf_candidates", 0),
|
| "hf_valid": stats.get("hf_valid", 0),
|
| "hf_invalid": stats.get("hf_invalid", 0),
|
| "hf_conditional": stats.get("hf_conditional", 0),
|
| "total_active": stats.get("total_active_providers", 0),
|
| "timestamp": stats.get("timestamp", "")
|
| }
|
|
|
|
|
|
|
| @app.get("/api/hf/models")
|
| async def get_hf_models():
|
| """Get HuggingFace models from APL report"""
|
| report = load_apl_report()
|
|
|
| if not report:
|
| return {"models": [], "count": 0}
|
|
|
| hf_models = report.get("hf_models", {}).get("results", [])
|
|
|
| return {
|
| "models": hf_models,
|
| "count": len(hf_models),
|
| "source": "APL Validation Report (Real Data)"
|
| }
|
|
|
|
|
| @app.get("/api/hf/health")
|
| async def get_hf_health():
|
| """Get HF services health"""
|
| try:
|
| from backend.services.hf_registry import REGISTRY
|
| health = REGISTRY.health()
|
| return health
|
| except Exception as e:
|
| return {
|
| "ok": False,
|
| "error": f"HF registry not available: {str(e)}"
|
| }
|
|
|
|
|
|
|
| @app.get("/api/defi")
|
| async def get_defi():
|
| """DeFi endpoint - Not implemented"""
|
| raise HTTPException(status_code=503, detail="DeFi endpoint not implemented. Real data only - no fakes.")
|
|
|
|
|
|
|
| @app.post("/api/hf/run-sentiment")
|
| async def run_sentiment(data: Dict[str, Any]):
|
| """ML sentiment analysis - Not implemented"""
|
| raise HTTPException(status_code=501, detail="ML sentiment not implemented. Real data only - no fakes.")
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| import uvicorn
|
| print(f"Starting Crypto Monitor Admin Server on port {PORT}")
|
| uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
|
|
|