Spaces:
Running
Running
""" | |
CryptoSentinel Pro β High-performance FastAPI application. | |
This is the main entry point that orchestrates the entire application. | |
- Integrates an asynchronous PriceFetcher for live market data. | |
- Integrates a sophisticated GeminiAnalyzer for deep text analysis. | |
- Implements an automated pipeline to fetch, analyze, and stream top crypto news. | |
- Serves the interactive frontend and provides all necessary API endpoints. | |
""" | |
import asyncio | |
import json | |
import os | |
from contextlib import asynccontextmanager | |
from typing import Optional, Union | |
from newsapi import NewsApiClient | |
import httpx | |
from fastapi import FastAPI, Request, BackgroundTasks | |
from fastapi.responses import HTMLResponse, StreamingResponse | |
from fastapi.templating import Jinja2Templates | |
from pydantic import BaseModel, constr | |
# Correct imports using relative paths | |
from .price_fetcher import PriceFetcher | |
from .gemini_analyzer import GeminiAnalyzer | |
from newsapi import NewsApiClient | |
# --- Pydantic Model for API Input Validation --- | |
class SentimentRequest(BaseModel): | |
"""Ensures the text for sentiment analysis is a non-empty string.""" | |
text: constr(strip_whitespace=True, min_length=1) | |
# --- Application Lifespan for Resource Management --- | |
async def lifespan(app: FastAPI): | |
""" | |
Manages application startup and shutdown events using the modern | |
lifespan context manager. | |
""" | |
async with httpx.AsyncClient() as client: | |
app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"]) | |
app.state.gemini_analyzer = GeminiAnalyzer(client=client) | |
app.state.news_api = NewsApiClient(api_key=os.getenv("NEWS_API_KEY")) | |
app.state.sentiment_queue: asyncio.Queue = asyncio.Queue() | |
app.state.news_queue: asyncio.Queue = asyncio.Queue() | |
price_task = asyncio.create_task( | |
run_periodic_updates(app.state.price_fetcher, interval_seconds=30) | |
) | |
news_task = asyncio.create_task( | |
run_periodic_news_analysis(app, interval_seconds=900) | |
) | |
print("π CryptoSentinel Pro started successfully.") | |
yield | |
print("β³ Shutting down background tasks...") | |
price_task.cancel() | |
news_task.cancel() | |
try: | |
await asyncio.gather(price_task, news_task) | |
except asyncio.CancelledError: | |
print("Background tasks cancelled successfully.") | |
print("β Shutdown complete.") | |
async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int): | |
"""Periodically updates prices.""" | |
while True: | |
await fetcher.update_prices_async() | |
await asyncio.sleep(interval_seconds) | |
async def run_periodic_news_analysis(app: FastAPI, interval_seconds: int): | |
"""Periodically fetches and analyzes crypto news.""" | |
while True: | |
print("π° Fetching latest crypto news for automated analysis...") | |
try: | |
top_headlines = app.state.news_api.get_everything( | |
q='bitcoin OR ethereum OR crypto OR blockchain', | |
language='en', | |
sort_by='publishedAt', | |
page_size=5 | |
) | |
analyzer: GeminiAnalyzer = app.state.gemini_analyzer | |
for article in top_headlines.get('articles', []): | |
title = article.get('title') | |
if title and "[Removed]" not in title: | |
analysis = await analyzer.analyze_text(title) | |
analysis['url'] = article.get('url') | |
await app.state.news_queue.put(analysis) | |
except Exception as e: | |
print(f"β Error during news fetching or analysis: {e}") | |
await asyncio.sleep(interval_seconds) | |
# --- FastAPI App Initialization --- | |
app = FastAPI(title="CryptoSentinel Pro", lifespan=lifespan) | |
templates = Jinja2Templates(directory="templates") | |
# --- HTML Rendering Helper --- | |
def render_analysis_card(payload: dict, is_news: bool = False) -> str: | |
"""Renders a dictionary of analysis into a styled HTML card.""" | |
s = payload | |
text_to_show = s.get('summary', 'Analysis failed or not available.') | |
if is_news: | |
url = s.get('url', '#') | |
text_to_show = f'<a href="{url}" target="_blank" rel="noopener noreferrer">{s.get("summary", "N/A")}</a>' | |
impact_class = f"impact-{s.get('impact', 'low').lower()}" | |
sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}" | |
return f""" | |
<div class="card {impact_class}"> | |
<blockquote>{text_to_show}</blockquote> | |
<div class="grid"> | |
<div><strong>Sentiment:</strong> <span class="{sentiment_class}">{s.get('sentiment')} ({s.get('sentiment_score', 0):.2f})</span></div> | |
<div><strong>Impact:</strong> {s.get('impact')}</div> | |
</div> | |
<div class="grid"> | |
<div><strong>Topic:</strong> {s.get('topic')}</div> | |
<div><strong>Entities:</strong> {', '.join(s.get('entities', []))}</div> | |
</div> | |
</div> | |
""" | |
# --- API Endpoints --- | |
async def serve_dashboard(request: Request): | |
return templates.TemplateResponse("index.html", {"request": request}) | |
async def get_prices_fragment(request: Request): | |
price_fetcher: PriceFetcher = request.app.state.price_fetcher | |
prices = price_fetcher.get_current_prices() | |
html_fragment = "".join( | |
f"<div><strong>{coin.capitalize()}:</strong> ${price:,.2f}</div>" if isinstance(price, (int, float)) | |
else f"<div><strong>{coin.capitalize()}:</strong> {price}</div>" | |
for coin, price in prices.items() | |
) | |
return HTMLResponse(content=html_fragment) | |
async def analyze_sentiment(payload: SentimentRequest, request: Request, background_tasks: BackgroundTasks): | |
analyzer: GeminiAnalyzer = request.app.state.gemini_analyzer | |
async def analysis_task_wrapper(): | |
analysis_result = await analyzer.analyze_text(payload.text) | |
await request.app.state.sentiment_queue.put(analysis_result) | |
background_tasks.add_task(analysis_task_wrapper) | |
return HTMLResponse(content="<small>β Queued for deep analysis...</small>") | |
async def sentiment_stream(request: Request): | |
queue: asyncio.Queue = request.app.state.sentiment_queue | |
async def event_generator(): | |
while True: | |
payload = await queue.get() | |
html = render_analysis_card(payload) | |
data_payload = html.replace('\n', '') | |
sse_message = f"event: sentiment_update\ndata: {data_payload}\n\n" | |
yield sse_message | |
return StreamingResponse(event_generator(), media_type="text/event-stream") | |
async def news_stream(request: Request): | |
queue: asyncio.Queue = request.app.state.news_queue | |
async def event_generator(): | |
while True: | |
payload = await queue.get() | |
html = render_analysis_card(payload, is_news=True) | |
data_payload = html.replace('\n', '') | |
sse_message = f"event: news_update\ndata: {data_payload}\n\n" | |
yield sse_message | |
return StreamingResponse(event_generator(), media_type="text/event-stream") |