File size: 4,315 Bytes
526c84c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e698c6
 
526c84c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e698c6
526c84c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40a414f
526c84c
5e698c6
526c84c
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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"<div><strong>{coin.capitalize()}:</strong> ${data.usd:,.2f}</div>"
        
        return HTMLResponse(content=html_fragment)

    except httpx.RequestError as e:
        # Handle network-related errors
        return HTMLResponse(content=f"<div class='error'>Network error: Could not connect to CoinGecko.</div>", status_code=503)
    except ValidationError as e:
        # Handle cases where CoinGecko's API response changes unexpectedly
        return HTMLResponse(content=f"<div class='error'>Invalid API response from data source.</div>", status_code=502)
    except Exception as e:
        # Generic catch-all for other unexpected errors
        return HTMLResponse(content=f"<div class='error'>An unexpected error occurred.</div>", 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)