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"