import httpx from fastapi import FastAPI, Request, HTTPException from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel, ValidationError from contextlib import asynccontextmanager # --- Configuration --- # Centralized configuration makes the app easier to modify. COINGECKO_API_URL = "https://api.coingecko.com/api/v3/simple/price" TARGET_COINS = ["bitcoin", "ethereum", "dogecoin"] TARGET_CURRENCY = "usd" # --- Pydantic Models for Data Validation --- # This ensures the API response from CoinGecko matches our expectations. class PriceData(BaseModel): usd: float # The full response is a dictionary mapping coin names (str) to their PriceData. # Example: {"bitcoin": {"usd": 65000.0}} ApiResponse = dict[str, PriceData] # --- Application State and Lifespan Management --- # Using a lifespan event is the modern way to manage resources like HTTP clients. # This client will be created on startup and closed gracefully on shutdown. app_state = {} @asynccontextmanager async def lifespan(app: FastAPI): # On startup: create a single, reusable httpx client. app_state["http_client"] = httpx.AsyncClient() yield # On shutdown: close the client. await app_state["http_client"].aclose() # --- FastAPI App Initialization --- app = FastAPI(lifespan=lifespan) templates = Jinja2Templates(directory="templates") # --- API Endpoints --- @app.get("/", response_class=HTMLResponse) async def serve_home_page(request: Request): """Serves the main HTML page which will then trigger HTMX calls.""" return templates.TemplateResponse("index.html", {"request": request}) @app.get("/api/prices", response_class=HTMLResponse) async def get_prices_ui(): """ This endpoint is called by HTMX. It fetches crypto data and returns an HTML FRAGMENT to be swapped into the page. """ try: client: httpx.AsyncClient = app_state["http_client"] # Build the request parameters dynamically params = { "ids": ",".join(TARGET_COINS), "vs_currencies": TARGET_CURRENCY } response = await client.get(COINGECKO_API_URL, params=params) response.raise_for_status() # Raise an exception for 4xx/5xx errors # Validate the received data against our Pydantic model prices = ApiResponse(**response.json()) # This is a simple but effective way to render an HTML fragment. # For larger fragments, you would use another Jinja2 template. html_fragment = "" for coin, data in prices.items(): html_fragment += f"
{coin.capitalize()}: ${data.usd:,.2f}
" return HTMLResponse(content=html_fragment) except httpx.RequestError as e: # Handle network-related errors return HTMLResponse(content=f"
Network error: Could not connect to CoinGecko.
", status_code=503) except ValidationError as e: # Handle cases where CoinGecko's API response changes unexpectedly return HTMLResponse(content=f"
Invalid API response from data source.
", status_code=502) except Exception as e: # Generic catch-all for other unexpected errors return HTMLResponse(content=f"
An unexpected error occurred.
", status_code=500) @app.get("/api/forecast/{coin_id}") async def get_forecast(coin_id: str): """ Placeholder for a real forecasting model. A real implementation would involve loading a pre-trained model, fetching historical data, and running a prediction. """ if coin_id not in TARGET_COINS: raise HTTPException(status_code=404, detail=f"Forecast not available for '{coin_id}'") # In a real app, this would be the output of your ML model. mock_forecast = { "coin_id": coin_id, "prediction_in_24h": "up", "confidence": 0.78, "detail": "This is a mock forecast. A real implementation requires a data science model." } return mock_forecast # --- Main execution (for development) --- if __name__ == "__main__": # To run: uvicorn main:app --reload --port 7860 import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)