""" CryptoSentinel AI — High-performance FastAPI application. Features: - Fully asynchronous architecture using modern FastAPI lifespan and background tasks. - Integrates a robust, async PriceFetcher with multi-API fallback. - Provides real-time sentiment analysis via an efficient, non-polling SSE stream. - Centralized state management for testability and clarity. """ import asyncio import json from contextlib import asynccontextmanager import httpx from fastapi import FastAPI, Request, BackgroundTasks from fastapi.responses import HTMLResponse, StreamingResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel, constr from .price_fetcher import PriceFetcher from .sentiment import SentimentAnalyzer # --- Configuration & Models --- class SentimentRequest(BaseModel): """Pydantic model for validating sentiment analysis requests.""" text: constr(strip_whitespace=True, min_length=1) # --- Application Lifespan Management --- @asynccontextmanager async def lifespan(app: FastAPI): """ Manages application startup and shutdown events. This is the modern replacement for @app.on_event("startup") and "shutdown". """ # -- Startup -- # Create a single, shared httpx client for the application's lifespan. async with httpx.AsyncClient() as client: # Initialize our stateful services price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"]) sentiment_analyzer = SentimentAnalyzer() # Store service instances in the app's state for access in routes app.state.price_fetcher = price_fetcher app.state.sentiment_analyzer = sentiment_analyzer app.state.request_counter = 0 # Create a cancellable background task for periodic price updates price_update_task = asyncio.create_task( run_periodic_updates(price_fetcher, interval_seconds=10) ) print("🚀 CryptoSentinel AI started successfully.") yield # The application is now running # -- Shutdown -- print("⏳ Shutting down background tasks...") price_update_task.cancel() try: await price_update_task except asyncio.CancelledError: print("Price update task cancelled successfully.") print("✅ Shutdown complete.") async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int): """A simple, robust asyncio background task runner.""" while True: await fetcher.update_prices_async() await asyncio.sleep(interval_seconds) # --- FastAPI App Initialization --- templates = Jinja2Templates(directory="app/templates") app = FastAPI(title="CryptoSentinel AI", lifespan=lifespan) # --- Routes --- @app.get("/", response_class=HTMLResponse) async def index(request: Request): """Renders the main single-page application view.""" return templates.TemplateResponse("index.html", {"request": request}) @app.get("/api/prices", response_class=HTMLResponse) async def get_prices_fragment(request: Request): """ Returns an HTML fragment with the latest crypto prices. Designed to be called by HTMX. """ price_fetcher: PriceFetcher = request.app.state.price_fetcher prices = price_fetcher.get_current_prices() html_fragment = "" for coin, price in prices.items(): price_str = f"${price:,.2f}" if isinstance(price, (int, float)) else price html_fragment += f"