""" 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 --- @asynccontextmanager 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'{s.get("summary", "N/A")}' impact_class = f"impact-{s.get('impact', 'low').lower()}" sentiment_class = f"sentiment-{s.get('sentiment', 'neutral').lower()}" return f"""
{text_to_show}