|
|
|
|
|
""" |
|
|
Comprehensive Resources Database API - Complete Crypto Data Sources |
|
|
Exposes all 400+ resources from api-resources folder: |
|
|
- 274+ resources from crypto_resources_unified_2025-11-11.json |
|
|
- 162+ resources from ultimate_crypto_pipeline_2025_NZasinich.json |
|
|
- RPC nodes, block explorers, market data, news, sentiment, on-chain analytics, whale tracking, etc. |
|
|
|
|
|
Endpoints: |
|
|
- GET /api/resources/database - Get all resources |
|
|
- GET /api/resources/database/categories - Get all categories |
|
|
- GET /api/resources/database/category/{category} - Get resources by category |
|
|
- GET /api/resources/database/search - Search resources |
|
|
- GET /api/resources/database/stats - Database statistics |
|
|
- GET /api/resources/database/random - Get random resources |
|
|
""" |
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query |
|
|
from typing import Optional, Dict, Any, List |
|
|
from datetime import datetime |
|
|
import logging |
|
|
import json |
|
|
from pathlib import Path |
|
|
import random |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(tags=["Resources Database API"]) |
|
|
|
|
|
|
|
|
RESOURCES_DIR = Path(__file__).resolve().parent.parent.parent / "api-resources" |
|
|
|
|
|
_RESOURCES_CACHE = None |
|
|
_ULTIMATE_PIPELINE_CACHE = None |
|
|
|
|
|
|
|
|
def load_unified_resources() -> Dict: |
|
|
"""Load resources from crypto_resources_unified_2025-11-11.json""" |
|
|
global _RESOURCES_CACHE |
|
|
|
|
|
if _RESOURCES_CACHE is not None: |
|
|
return _RESOURCES_CACHE |
|
|
|
|
|
try: |
|
|
file_path = RESOURCES_DIR / "crypto_resources_unified_2025-11-11.json" |
|
|
with open(file_path, 'r', encoding='utf-8') as f: |
|
|
data = json.load(f) |
|
|
_RESOURCES_CACHE = data.get('registry', {}) |
|
|
return _RESOURCES_CACHE |
|
|
except Exception as e: |
|
|
logger.error(f"Error loading unified resources: {e}") |
|
|
return {} |
|
|
|
|
|
|
|
|
def load_ultimate_pipeline() -> Dict: |
|
|
"""Load resources from ultimate_crypto_pipeline_2025_NZasinich.json""" |
|
|
global _ULTIMATE_PIPELINE_CACHE |
|
|
|
|
|
if _ULTIMATE_PIPELINE_CACHE is not None: |
|
|
return _ULTIMATE_PIPELINE_CACHE |
|
|
|
|
|
try: |
|
|
file_path = RESOURCES_DIR / "ultimate_crypto_pipeline_2025_NZasinich.json" |
|
|
with open(file_path, 'r', encoding='utf-8') as f: |
|
|
data = json.load(f) |
|
|
_ULTIMATE_PIPELINE_CACHE = data |
|
|
return _ULTIMATE_PIPELINE_CACHE |
|
|
except Exception as e: |
|
|
logger.error(f"Error loading ultimate pipeline: {e}") |
|
|
return {} |
|
|
|
|
|
|
|
|
def get_all_resources() -> Dict[str, Any]: |
|
|
"""Get all resources from both files, consolidated""" |
|
|
unified = load_unified_resources() |
|
|
pipeline = load_ultimate_pipeline() |
|
|
|
|
|
|
|
|
consolidated = { |
|
|
"unified_resources": unified, |
|
|
"pipeline_resources": pipeline.get("files", [{}])[0].get("content", {}).get("resources", []) if pipeline.get("files") else [] |
|
|
} |
|
|
|
|
|
return consolidated |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database") |
|
|
async def get_resources_database( |
|
|
category: Optional[str] = Query(None, description="Filter by category"), |
|
|
source: Optional[str] = Query("all", description="Source: unified, pipeline, all"), |
|
|
limit: Optional[int] = Query(None, ge=1, le=1000, description="Limit results") |
|
|
): |
|
|
""" |
|
|
Get comprehensive resources database |
|
|
|
|
|
Returns all 400+ cryptocurrency data sources including: |
|
|
- RPC nodes (24) |
|
|
- Block explorers (33) |
|
|
- Market data APIs (33) |
|
|
- News APIs (17) |
|
|
- Sentiment APIs (14) |
|
|
- On-chain analytics (14) |
|
|
- Whale tracking (10) |
|
|
- HuggingFace resources (9) |
|
|
- Free HTTP endpoints (13) |
|
|
- Local backend routes (106) |
|
|
- Ultimate pipeline (162) |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
|
|
|
result = { |
|
|
"success": True, |
|
|
"source_files": { |
|
|
"unified": "crypto_resources_unified_2025-11-11.json", |
|
|
"pipeline": "ultimate_crypto_pipeline_2025_NZasinich.json" |
|
|
} |
|
|
} |
|
|
|
|
|
if source == "unified" or source == "all": |
|
|
unified = all_resources["unified_resources"] |
|
|
|
|
|
|
|
|
if category: |
|
|
unified_filtered = { |
|
|
category: unified.get(category, []) |
|
|
} |
|
|
else: |
|
|
unified_filtered = {k: v for k, v in unified.items() if k != "metadata" and isinstance(v, list)} |
|
|
|
|
|
result["unified_resources"] = { |
|
|
"categories": list(unified_filtered.keys()), |
|
|
"total_categories": len(unified_filtered), |
|
|
"resources": unified_filtered, |
|
|
"metadata": unified.get("metadata", {}) |
|
|
} |
|
|
|
|
|
if source == "pipeline" or source == "all": |
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
|
|
|
|
|
|
if category: |
|
|
pipeline_filtered = [ |
|
|
r for r in pipeline_resources |
|
|
if r.get("category", "").lower() == category.lower() |
|
|
] |
|
|
else: |
|
|
pipeline_filtered = pipeline_resources |
|
|
|
|
|
|
|
|
if limit: |
|
|
pipeline_filtered = pipeline_filtered[:limit] |
|
|
|
|
|
|
|
|
pipeline_by_category = {} |
|
|
for resource in pipeline_filtered: |
|
|
cat = resource.get("category", "uncategorized") |
|
|
if cat not in pipeline_by_category: |
|
|
pipeline_by_category[cat] = [] |
|
|
pipeline_by_category[cat].append(resource) |
|
|
|
|
|
result["pipeline_resources"] = { |
|
|
"total_resources": len(pipeline_filtered), |
|
|
"categories": list(pipeline_by_category.keys()), |
|
|
"total_categories": len(pipeline_by_category), |
|
|
"resources_by_category": pipeline_by_category, |
|
|
"resources_flat": pipeline_filtered if not category else None |
|
|
} |
|
|
|
|
|
result["timestamp"] = datetime.utcnow().isoformat() + "Z" |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error getting resources database: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database/categories") |
|
|
async def get_database_categories(): |
|
|
""" |
|
|
Get all available resource categories |
|
|
|
|
|
Returns complete list of categories from both data sources |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
|
|
|
|
|
|
unified = all_resources["unified_resources"] |
|
|
unified_categories = [k for k in unified.keys() if k != "metadata" and isinstance(unified[k], list)] |
|
|
|
|
|
|
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
pipeline_categories = list(set(r.get("category", "uncategorized") for r in pipeline_resources)) |
|
|
|
|
|
|
|
|
unified_counts = { |
|
|
cat: len(unified[cat]) for cat in unified_categories |
|
|
} |
|
|
|
|
|
pipeline_counts = {} |
|
|
for resource in pipeline_resources: |
|
|
cat = resource.get("category", "uncategorized") |
|
|
pipeline_counts[cat] = pipeline_counts.get(cat, 0) + 1 |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"unified_resources": { |
|
|
"categories": unified_categories, |
|
|
"total_categories": len(unified_categories), |
|
|
"counts": unified_counts, |
|
|
"total_resources": sum(unified_counts.values()) |
|
|
}, |
|
|
"pipeline_resources": { |
|
|
"categories": sorted(pipeline_categories), |
|
|
"total_categories": len(pipeline_categories), |
|
|
"counts": pipeline_counts, |
|
|
"total_resources": len(pipeline_resources) |
|
|
}, |
|
|
"combined": { |
|
|
"unique_categories": len(set(unified_categories + pipeline_categories)), |
|
|
"total_resources": sum(unified_counts.values()) + len(pipeline_resources) |
|
|
}, |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error getting categories: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database/category/{category}") |
|
|
async def get_resources_by_category( |
|
|
category: str, |
|
|
source: str = Query("all", description="Source: unified, pipeline, all"), |
|
|
limit: Optional[int] = Query(None, ge=1, le=1000) |
|
|
): |
|
|
""" |
|
|
Get resources from a specific category |
|
|
|
|
|
Available categories: |
|
|
- rpc_nodes |
|
|
- block_explorers |
|
|
- market_data_apis |
|
|
- news_apis |
|
|
- sentiment_apis |
|
|
- onchain_analytics_apis |
|
|
- whale_tracking_apis |
|
|
- community_sentiment_apis |
|
|
- hf_resources |
|
|
- free_http_endpoints |
|
|
- local_backend_routes |
|
|
- cors_proxies |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
result = {"success": True, "category": category} |
|
|
|
|
|
if source in ["unified", "all"]: |
|
|
unified = all_resources["unified_resources"] |
|
|
category_resources = unified.get(category, []) |
|
|
|
|
|
if limit: |
|
|
category_resources = category_resources[:limit] |
|
|
|
|
|
result["unified_resources"] = { |
|
|
"count": len(category_resources), |
|
|
"resources": category_resources |
|
|
} |
|
|
|
|
|
if source in ["pipeline", "all"]: |
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
filtered = [ |
|
|
r for r in pipeline_resources |
|
|
if r.get("category", "").lower() == category.lower() |
|
|
] |
|
|
|
|
|
if limit: |
|
|
filtered = filtered[:limit] |
|
|
|
|
|
result["pipeline_resources"] = { |
|
|
"count": len(filtered), |
|
|
"resources": filtered |
|
|
} |
|
|
|
|
|
result["timestamp"] = datetime.utcnow().isoformat() + "Z" |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error getting category resources: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database/search") |
|
|
async def search_resources( |
|
|
q: str = Query(..., min_length=2, description="Search query"), |
|
|
fields: Optional[str] = Query("name,url,desc", description="Fields to search: name,url,desc,category"), |
|
|
source: str = Query("all", description="Source: unified, pipeline, all"), |
|
|
limit: int = Query(50, ge=1, le=500) |
|
|
): |
|
|
""" |
|
|
Search resources by keyword |
|
|
|
|
|
Searches across name, URL, description, and category fields |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
query_lower = q.lower() |
|
|
search_fields = fields.split(",") |
|
|
|
|
|
results = [] |
|
|
|
|
|
|
|
|
if source in ["unified", "all"]: |
|
|
unified = all_resources["unified_resources"] |
|
|
|
|
|
for category, resources in unified.items(): |
|
|
if category == "metadata" or not isinstance(resources, list): |
|
|
continue |
|
|
|
|
|
for resource in resources: |
|
|
|
|
|
matches = False |
|
|
|
|
|
if "name" in search_fields and query_lower in str(resource.get("name", "")).lower(): |
|
|
matches = True |
|
|
if "url" in search_fields and query_lower in str(resource.get("base_url", "")).lower(): |
|
|
matches = True |
|
|
if "desc" in search_fields and query_lower in str(resource.get("notes", "")).lower(): |
|
|
matches = True |
|
|
if "category" in search_fields and query_lower in category.lower(): |
|
|
matches = True |
|
|
|
|
|
if matches: |
|
|
results.append({ |
|
|
"source": "unified", |
|
|
"category": category, |
|
|
"resource": resource |
|
|
}) |
|
|
|
|
|
|
|
|
if source in ["pipeline", "all"]: |
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
|
|
|
for resource in pipeline_resources: |
|
|
matches = False |
|
|
|
|
|
if "name" in search_fields and query_lower in str(resource.get("name", "")).lower(): |
|
|
matches = True |
|
|
if "url" in search_fields and query_lower in str(resource.get("url", "")).lower(): |
|
|
matches = True |
|
|
if "desc" in search_fields and query_lower in str(resource.get("desc", "")).lower(): |
|
|
matches = True |
|
|
if "category" in search_fields and query_lower in str(resource.get("category", "")).lower(): |
|
|
matches = True |
|
|
|
|
|
if matches: |
|
|
results.append({ |
|
|
"source": "pipeline", |
|
|
"category": resource.get("category", "uncategorized"), |
|
|
"resource": resource |
|
|
}) |
|
|
|
|
|
|
|
|
results = results[:limit] |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"query": q, |
|
|
"search_fields": search_fields, |
|
|
"total_results": len(results), |
|
|
"results": results, |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error searching resources: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database/stats") |
|
|
async def get_database_stats(): |
|
|
""" |
|
|
Get comprehensive statistics about the resources database |
|
|
|
|
|
Returns counts, categories, free vs paid, and more |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
|
|
|
|
|
|
unified = all_resources["unified_resources"] |
|
|
unified_categories = {k: len(v) for k, v in unified.items() if k != "metadata" and isinstance(v, list)} |
|
|
|
|
|
|
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
|
|
|
pipeline_by_category = {} |
|
|
free_count = 0 |
|
|
paid_count = 0 |
|
|
|
|
|
for resource in pipeline_resources: |
|
|
cat = resource.get("category", "uncategorized") |
|
|
pipeline_by_category[cat] = pipeline_by_category.get(cat, 0) + 1 |
|
|
|
|
|
if resource.get("free", True): |
|
|
free_count += 1 |
|
|
else: |
|
|
paid_count += 1 |
|
|
|
|
|
|
|
|
total_unified = sum(unified_categories.values()) |
|
|
total_pipeline = len(pipeline_resources) |
|
|
total_resources = total_unified + total_pipeline |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"overview": { |
|
|
"total_resources": total_resources, |
|
|
"unified_resources": total_unified, |
|
|
"pipeline_resources": total_pipeline, |
|
|
"total_categories": len(set(list(unified_categories.keys()) + list(pipeline_by_category.keys()))), |
|
|
"unique_data_sources": 2 |
|
|
}, |
|
|
"unified_resources": { |
|
|
"total": total_unified, |
|
|
"categories": unified_categories, |
|
|
"top_categories": sorted(unified_categories.items(), key=lambda x: x[1], reverse=True)[:5], |
|
|
"metadata": unified.get("metadata", {}) |
|
|
}, |
|
|
"pipeline_resources": { |
|
|
"total": total_pipeline, |
|
|
"categories": pipeline_by_category, |
|
|
"free_resources": free_count, |
|
|
"paid_resources": paid_count, |
|
|
"top_categories": sorted(pipeline_by_category.items(), key=lambda x: x[1], reverse=True)[:5] |
|
|
}, |
|
|
"coverage": { |
|
|
"rpc_nodes": unified_categories.get("rpc_nodes", 0), |
|
|
"block_explorers": unified_categories.get("block_explorers", 0) + pipeline_by_category.get("Block Explorer", 0), |
|
|
"market_data": unified_categories.get("market_data_apis", 0) + pipeline_by_category.get("Market Data", 0), |
|
|
"news_apis": unified_categories.get("news_apis", 0) + pipeline_by_category.get("News", 0), |
|
|
"sentiment_apis": unified_categories.get("sentiment_apis", 0), |
|
|
"analytics": unified_categories.get("onchain_analytics_apis", 0) + pipeline_by_category.get("On-chain", 0), |
|
|
"whale_tracking": unified_categories.get("whale_tracking_apis", 0), |
|
|
"defi": pipeline_by_category.get("DeFi", 0), |
|
|
"nft": pipeline_by_category.get("NFT", 0) |
|
|
}, |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error getting database stats: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/resources/database/random") |
|
|
async def get_random_resources( |
|
|
count: int = Query(10, ge=1, le=100, description="Number of random resources"), |
|
|
category: Optional[str] = Query(None, description="Filter by category"), |
|
|
source: str = Query("all", description="Source: unified, pipeline, all") |
|
|
): |
|
|
""" |
|
|
Get random resources from the database |
|
|
|
|
|
Useful for discovering new data sources |
|
|
""" |
|
|
try: |
|
|
all_resources = get_all_resources() |
|
|
all_items = [] |
|
|
|
|
|
|
|
|
if source in ["unified", "all"]: |
|
|
unified = all_resources["unified_resources"] |
|
|
|
|
|
for cat, resources in unified.items(): |
|
|
if cat == "metadata" or not isinstance(resources, list): |
|
|
continue |
|
|
|
|
|
if category and cat != category: |
|
|
continue |
|
|
|
|
|
for resource in resources: |
|
|
all_items.append({ |
|
|
"source": "unified", |
|
|
"category": cat, |
|
|
"resource": resource |
|
|
}) |
|
|
|
|
|
if source in ["pipeline", "all"]: |
|
|
pipeline_resources = all_resources["pipeline_resources"] |
|
|
|
|
|
for resource in pipeline_resources: |
|
|
if category and resource.get("category", "").lower() != category.lower(): |
|
|
continue |
|
|
|
|
|
all_items.append({ |
|
|
"source": "pipeline", |
|
|
"category": resource.get("category", "uncategorized"), |
|
|
"resource": resource |
|
|
}) |
|
|
|
|
|
|
|
|
sample_size = min(count, len(all_items)) |
|
|
random_resources = random.sample(all_items, sample_size) if all_items else [] |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"requested_count": count, |
|
|
"returned_count": len(random_resources), |
|
|
"total_available": len(all_items), |
|
|
"resources": random_resources, |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error getting random resources: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
logger.info("✅ Comprehensive Resources Database API Router loaded") |
|
|
|