|
|
|
|
@ -10,6 +10,8 @@ Aggregates news and social media data from multiple sources for sentiment analys
|
|
|
|
|
import asyncio
|
|
|
|
|
import aiohttp
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
from app.core.http_client import get_http_session
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
from typing import Dict, List, Optional, Any, Union
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
@ -326,42 +328,42 @@ class NewsSocialService:
|
|
|
|
|
"language": "en"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
|
async with session.get(url, params=params) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
data = await response.json()
|
|
|
|
|
|
|
|
|
|
articles = []
|
|
|
|
|
for item in data.get('articles', []):
|
|
|
|
|
try:
|
|
|
|
|
# Parse NewsAPI format - make timezone naive for consistency
|
|
|
|
|
published_str = item['publishedAt'].replace('Z', '+00:00')
|
|
|
|
|
published_at = datetime.fromisoformat(published_str).replace(tzinfo=None)
|
|
|
|
|
|
|
|
|
|
article = NewsArticle(
|
|
|
|
|
title=item.get('title', ''),
|
|
|
|
|
summary=item.get('description', ''),
|
|
|
|
|
content=item.get('content', ''),
|
|
|
|
|
url=item.get('url', ''),
|
|
|
|
|
source="NewsAPI",
|
|
|
|
|
published_at=published_at,
|
|
|
|
|
author=item.get('author', ''),
|
|
|
|
|
image_url=item.get('urlToImage', '')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
articles.append(article)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Error parsing NewsAPI article: {e}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
logger.info(f"Retrieved {len(articles)} articles from NewsAPI")
|
|
|
|
|
return articles
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error_data = await response.json()
|
|
|
|
|
logger.error(f"NewsAPI error {response.status}: {error_data}")
|
|
|
|
|
raise NewsAPIError(f"NewsAPI returned {response.status}: {error_data}")
|
|
|
|
|
session = await get_http_session()
|
|
|
|
|
async with session.get(url, params=params) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
data = await response.json()
|
|
|
|
|
|
|
|
|
|
articles = []
|
|
|
|
|
for item in data.get('articles', []):
|
|
|
|
|
try:
|
|
|
|
|
# Parse NewsAPI format - make timezone naive for consistency
|
|
|
|
|
published_str = item['publishedAt'].replace('Z', '+00:00')
|
|
|
|
|
published_at = datetime.fromisoformat(published_str).replace(tzinfo=None)
|
|
|
|
|
|
|
|
|
|
article = NewsArticle(
|
|
|
|
|
title=item.get('title', ''),
|
|
|
|
|
summary=item.get('description', ''),
|
|
|
|
|
content=item.get('content', ''),
|
|
|
|
|
url=item.get('url', ''),
|
|
|
|
|
source="NewsAPI",
|
|
|
|
|
published_at=published_at,
|
|
|
|
|
author=item.get('author', ''),
|
|
|
|
|
image_url=item.get('urlToImage', '')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
articles.append(article)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Error parsing NewsAPI article: {e}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
logger.info(f"Retrieved {len(articles)} articles from NewsAPI")
|
|
|
|
|
return articles
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error_data = await response.json()
|
|
|
|
|
logger.error(f"NewsAPI error {response.status}: {error_data}")
|
|
|
|
|
raise NewsAPIError(f"NewsAPI returned {response.status}: {error_data}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error fetching NewsAPI articles for {ticker}: {e}")
|
|
|
|
|
@ -405,53 +407,53 @@ class NewsSocialService:
|
|
|
|
|
"User-Agent": "StockOracle/1.0.0"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
|
async with session.get(url, params=params, headers=headers) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
data = await response.json()
|
|
|
|
|
|
|
|
|
|
for item in data.get('data', {}).get('children', []):
|
|
|
|
|
try:
|
|
|
|
|
post_data = item.get('data', {})
|
|
|
|
|
|
|
|
|
|
# Filter out posts that are too old
|
|
|
|
|
created_utc = post_data.get('created_utc', 0)
|
|
|
|
|
post_date = datetime.fromtimestamp(created_utc)
|
|
|
|
|
|
|
|
|
|
if (datetime.now() - post_date).days > days_back:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Skip removed/deleted posts
|
|
|
|
|
if post_data.get('removed_by_category') or post_data.get('selftext') == '[removed]':
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
post = SocialPost(
|
|
|
|
|
title=post_data.get('title', ''),
|
|
|
|
|
content=post_data.get('selftext', ''),
|
|
|
|
|
url=f"https://reddit.com{post_data.get('permalink', '')}",
|
|
|
|
|
platform="Reddit",
|
|
|
|
|
author=post_data.get('author', ''),
|
|
|
|
|
published_at=post_date,
|
|
|
|
|
score=post_data.get('score', 0),
|
|
|
|
|
comments_count=post_data.get('num_comments', 0),
|
|
|
|
|
upvotes=post_data.get('ups', 0),
|
|
|
|
|
downvotes=post_data.get('downs', 0),
|
|
|
|
|
subreddit=post_data.get('subreddit', '')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
all_posts.append(post)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Error parsing Reddit post: {e}")
|
|
|
|
|
session = await get_http_session()
|
|
|
|
|
async with session.get(url, params=params, headers=headers) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
data = await response.json()
|
|
|
|
|
|
|
|
|
|
for item in data.get('data', {}).get('children', []):
|
|
|
|
|
try:
|
|
|
|
|
post_data = item.get('data', {})
|
|
|
|
|
|
|
|
|
|
# Filter out posts that are too old
|
|
|
|
|
created_utc = post_data.get('created_utc', 0)
|
|
|
|
|
post_date = datetime.fromtimestamp(created_utc)
|
|
|
|
|
|
|
|
|
|
if (datetime.now() - post_date).days > days_back:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Skip removed/deleted posts
|
|
|
|
|
if post_data.get('removed_by_category') or post_data.get('selftext') == '[removed]':
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
elif response.status == 401:
|
|
|
|
|
logger.error("Reddit API authentication failed")
|
|
|
|
|
# Try to refresh token
|
|
|
|
|
self._reddit_token = None
|
|
|
|
|
await self._ensure_reddit_token()
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"Reddit API error for r/{subreddit}: {response.status}")
|
|
|
|
|
|
|
|
|
|
post = SocialPost(
|
|
|
|
|
title=post_data.get('title', ''),
|
|
|
|
|
content=post_data.get('selftext', ''),
|
|
|
|
|
url=f"https://reddit.com{post_data.get('permalink', '')}",
|
|
|
|
|
platform="Reddit",
|
|
|
|
|
author=post_data.get('author', ''),
|
|
|
|
|
published_at=post_date,
|
|
|
|
|
score=post_data.get('score', 0),
|
|
|
|
|
comments_count=post_data.get('num_comments', 0),
|
|
|
|
|
upvotes=post_data.get('ups', 0),
|
|
|
|
|
downvotes=post_data.get('downs', 0),
|
|
|
|
|
subreddit=post_data.get('subreddit', '')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
all_posts.append(post)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Error parsing Reddit post: {e}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
elif response.status == 401:
|
|
|
|
|
logger.error("Reddit API authentication failed")
|
|
|
|
|
# Try to refresh token
|
|
|
|
|
self._reddit_token = None
|
|
|
|
|
await self._ensure_reddit_token()
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"Reddit API error for r/{subreddit}: {response.status}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"Error fetching from r/{subreddit}: {e}")
|
|
|
|
|
@ -497,21 +499,21 @@ class NewsSocialService:
|
|
|
|
|
|
|
|
|
|
auth = aiohttp.BasicAuth(self.reddit_client_id, self.reddit_client_secret)
|
|
|
|
|
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
|
async with session.post(auth_url, data=auth_data, auth=auth, headers=headers) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
token_data = await response.json()
|
|
|
|
|
|
|
|
|
|
self._reddit_token = token_data.get('access_token')
|
|
|
|
|
expires_in = token_data.get('expires_in', 3600)
|
|
|
|
|
self._reddit_token_expiry = datetime.now() + timedelta(seconds=expires_in - 60)
|
|
|
|
|
|
|
|
|
|
logger.info("Successfully obtained Reddit access token")
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error_data = await response.text()
|
|
|
|
|
logger.error(f"Reddit auth error {response.status}: {error_data}")
|
|
|
|
|
raise RedditAPIError(f"Failed to authenticate with Reddit: {response.status}")
|
|
|
|
|
session = await get_http_session()
|
|
|
|
|
async with session.post(auth_url, data=auth_data, auth=auth, headers=headers) as response:
|
|
|
|
|
if response.status == 200:
|
|
|
|
|
token_data = await response.json()
|
|
|
|
|
|
|
|
|
|
self._reddit_token = token_data.get('access_token')
|
|
|
|
|
expires_in = token_data.get('expires_in', 3600)
|
|
|
|
|
self._reddit_token_expiry = datetime.now() + timedelta(seconds=expires_in - 60)
|
|
|
|
|
|
|
|
|
|
logger.info("Successfully obtained Reddit access token")
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
error_data = await response.text()
|
|
|
|
|
logger.error(f"Reddit auth error {response.status}: {error_data}")
|
|
|
|
|
raise RedditAPIError(f"Failed to authenticate with Reddit: {response.status}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error obtaining Reddit token: {e}")
|
|
|
|
|
|