| |
| """ |
| External API Clients - Complete Collection |
| Direct HTTP clients for all external cryptocurrency data sources |
| NO WEBSOCKET - Only HTTP REST requests |
| """ |
|
|
| import logging |
| import httpx |
| import feedparser |
| from typing import Dict, Any, List, Optional |
| from datetime import datetime |
| import os |
| import json |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class AlternativeMeClient: |
| """ |
| Alternative.me API Client |
| Fetches Fear & Greed Index for crypto markets |
| """ |
| |
| def __init__(self): |
| self.base_url = "https://api.alternative.me" |
| self.timeout = 10.0 |
| |
| async def get_fear_greed_index(self, limit: int = 1) -> Dict[str, Any]: |
| """ |
| Get Fear & Greed Index |
| |
| Args: |
| limit: Number of historical data points (default: 1 for current) |
| |
| Returns: |
| Fear & Greed Index data |
| """ |
| try: |
| async with httpx.AsyncClient(timeout=self.timeout) as client: |
| response = await client.get( |
| f"{self.base_url}/fng/", |
| params={"limit": limit} |
| ) |
| response.raise_for_status() |
| data = response.json() |
| |
| |
| results = [] |
| for item in data.get("data", []): |
| results.append({ |
| "value": int(item.get("value", 0)), |
| "value_classification": item.get("value_classification", "neutral"), |
| "timestamp": int(item.get("timestamp", 0)), |
| "time_until_update": item.get("time_until_update"), |
| "source": "alternative.me" |
| }) |
| |
| logger.info(f"✅ Alternative.me: Fetched Fear & Greed Index") |
| |
| return { |
| "success": True, |
| "data": results, |
| "metadata": data.get("metadata", {}), |
| "source": "alternative.me", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Alternative.me API failed: {e}") |
| raise Exception(f"Failed to fetch Fear & Greed Index: {str(e)}") |
|
|
|
|
| class RedditClient: |
| """ |
| Reddit API Client |
| Fetches cryptocurrency posts from Reddit |
| """ |
| |
| def __init__(self): |
| self.base_url = "https://www.reddit.com" |
| self.timeout = 15.0 |
| self.user_agent = "CryptoDataHub/1.0" |
| |
| async def get_top_posts( |
| self, |
| subreddit: str = "cryptocurrency", |
| time_filter: str = "day", |
| limit: int = 25 |
| ) -> Dict[str, Any]: |
| """ |
| Get top posts from a subreddit |
| |
| Args: |
| subreddit: Subreddit name (default: cryptocurrency) |
| time_filter: Time filter (hour, day, week, month, year, all) |
| limit: Number of posts |
| |
| Returns: |
| Top Reddit posts |
| """ |
| try: |
| async with httpx.AsyncClient(timeout=self.timeout) as client: |
| response = await client.get( |
| f"{self.base_url}/r/{subreddit}/top.json", |
| params={"t": time_filter, "limit": limit}, |
| headers={"User-Agent": self.user_agent} |
| ) |
| response.raise_for_status() |
| data = response.json() |
| |
| |
| posts = [] |
| for child in data.get("data", {}).get("children", []): |
| post_data = child.get("data", {}) |
| posts.append({ |
| "id": post_data.get("id"), |
| "title": post_data.get("title"), |
| "author": post_data.get("author"), |
| "score": post_data.get("score", 0), |
| "upvote_ratio": post_data.get("upvote_ratio", 0), |
| "num_comments": post_data.get("num_comments", 0), |
| "url": post_data.get("url"), |
| "permalink": f"{self.base_url}{post_data.get('permalink', '')}", |
| "created_utc": int(post_data.get("created_utc", 0)), |
| "selftext": post_data.get("selftext", "")[:500], |
| "subreddit": subreddit, |
| "source": "reddit" |
| }) |
| |
| logger.info(f"✅ Reddit: Fetched {len(posts)} posts from r/{subreddit}") |
| |
| return { |
| "success": True, |
| "data": posts, |
| "subreddit": subreddit, |
| "time_filter": time_filter, |
| "count": len(posts), |
| "source": "reddit", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Reddit API failed: {e}") |
| raise Exception(f"Failed to fetch Reddit posts: {str(e)}") |
| |
| async def get_new_posts( |
| self, |
| subreddit: str = "cryptocurrency", |
| limit: int = 25 |
| ) -> Dict[str, Any]: |
| """ |
| Get new posts from a subreddit |
| |
| Args: |
| subreddit: Subreddit name |
| limit: Number of posts |
| |
| Returns: |
| New Reddit posts |
| """ |
| try: |
| async with httpx.AsyncClient(timeout=self.timeout) as client: |
| response = await client.get( |
| f"{self.base_url}/r/{subreddit}/new.json", |
| params={"limit": limit}, |
| headers={"User-Agent": self.user_agent} |
| ) |
| response.raise_for_status() |
| data = response.json() |
| |
| |
| posts = [] |
| for child in data.get("data", {}).get("children", []): |
| post_data = child.get("data", {}) |
| posts.append({ |
| "id": post_data.get("id"), |
| "title": post_data.get("title"), |
| "author": post_data.get("author"), |
| "score": post_data.get("score", 0), |
| "num_comments": post_data.get("num_comments", 0), |
| "url": post_data.get("url"), |
| "created_utc": int(post_data.get("created_utc", 0)), |
| "source": "reddit" |
| }) |
| |
| logger.info(f"✅ Reddit: Fetched {len(posts)} new posts from r/{subreddit}") |
| |
| return { |
| "success": True, |
| "data": posts, |
| "subreddit": subreddit, |
| "count": len(posts), |
| "source": "reddit", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Reddit API failed: {e}") |
| raise Exception(f"Failed to fetch Reddit posts: {str(e)}") |
|
|
|
|
| class RSSFeedClient: |
| """ |
| RSS Feed Client |
| Fetches news from cryptocurrency RSS feeds |
| """ |
| |
| def __init__(self): |
| self.feeds = { |
| "coindesk": "https://www.coindesk.com/arc/outboundfeeds/rss/", |
| "cointelegraph": "https://cointelegraph.com/rss", |
| "bitcoinmagazine": "https://bitcoinmagazine.com/.rss/full/", |
| "decrypt": "https://decrypt.co/feed", |
| "theblock": "https://www.theblock.co/rss.xml" |
| } |
| |
| async def fetch_feed(self, feed_name: str, limit: int = 20) -> Dict[str, Any]: |
| """ |
| Fetch articles from a specific RSS feed |
| |
| Args: |
| feed_name: Name of the feed (coindesk, cointelegraph, etc.) |
| limit: Maximum number of articles |
| |
| Returns: |
| RSS feed articles |
| """ |
| if feed_name not in self.feeds: |
| raise ValueError(f"Unknown feed: {feed_name}. Available: {list(self.feeds.keys())}") |
| |
| try: |
| feed_url = self.feeds[feed_name] |
| |
| |
| feed = feedparser.parse(feed_url) |
| |
| |
| articles = [] |
| for entry in feed.entries[:limit]: |
| |
| try: |
| if hasattr(entry, "published_parsed"): |
| dt = datetime(*entry.published_parsed[:6]) |
| elif hasattr(entry, "updated_parsed"): |
| dt = datetime(*entry.updated_parsed[:6]) |
| else: |
| dt = datetime.utcnow() |
| |
| timestamp = int(dt.timestamp()) |
| except: |
| timestamp = int(datetime.utcnow().timestamp()) |
| |
| articles.append({ |
| "title": entry.get("title", ""), |
| "link": entry.get("link", ""), |
| "summary": entry.get("summary", "")[:500], |
| "author": entry.get("author", ""), |
| "published": timestamp, |
| "source": feed_name, |
| "feed_url": feed_url |
| }) |
| |
| logger.info(f"✅ RSS: Fetched {len(articles)} articles from {feed_name}") |
| |
| return { |
| "success": True, |
| "data": articles, |
| "feed_name": feed_name, |
| "feed_url": feed_url, |
| "count": len(articles), |
| "source": "rss", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ RSS feed {feed_name} failed: {e}") |
| raise Exception(f"Failed to fetch RSS feed: {str(e)}") |
| |
| async def fetch_all_feeds(self, limit_per_feed: int = 10) -> Dict[str, Any]: |
| """ |
| Fetch articles from all RSS feeds |
| |
| Args: |
| limit_per_feed: Maximum number of articles per feed |
| |
| Returns: |
| All RSS feed articles |
| """ |
| all_articles = [] |
| feed_results = {} |
| |
| for feed_name in self.feeds.keys(): |
| try: |
| result = await self.fetch_feed(feed_name, limit_per_feed) |
| feed_results[feed_name] = { |
| "success": True, |
| "count": result["count"] |
| } |
| all_articles.extend(result["data"]) |
| except Exception as e: |
| logger.error(f"❌ Failed to fetch {feed_name}: {e}") |
| feed_results[feed_name] = { |
| "success": False, |
| "error": str(e) |
| } |
| |
| |
| all_articles.sort(key=lambda x: x.get("published", 0), reverse=True) |
| |
| logger.info(f"✅ RSS: Fetched {len(all_articles)} total articles from {len(self.feeds)} feeds") |
| |
| return { |
| "success": True, |
| "data": all_articles, |
| "total_articles": len(all_articles), |
| "feeds": feed_results, |
| "source": "rss", |
| "timestamp": datetime.utcnow().isoformat() |
| } |
|
|
|
|
| |
| alternative_me_client = AlternativeMeClient() |
| reddit_client = RedditClient() |
| rss_feed_client = RSSFeedClient() |
|
|
|
|
| |
| __all__ = [ |
| "AlternativeMeClient", |
| "RedditClient", |
| "RSSFeedClient", |
| "alternative_me_client", |
| "reddit_client", |
| "rss_feed_client" |
| ] |
|
|