mgbam commited on
Commit
7239f1c
·
verified ·
1 Parent(s): 4c64df5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -83
app.py CHANGED
@@ -1,111 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import httpx
2
- from fastapi import FastAPI, Request, HTTPException
3
- from fastapi.responses import HTMLResponse
4
  from fastapi.templating import Jinja2Templates
5
- from pydantic import BaseModel, ValidationError
6
- from contextlib import asynccontextmanager
7
 
8
- # --- Configuration ---
9
- # Centralized configuration makes the app easier to modify.
10
- COINGECKO_API_URL = "https://api.coingecko.com/api/v3/simple/price"
11
- TARGET_COINS = ["bitcoin", "ethereum", "dogecoin"]
12
- TARGET_CURRENCY = "usd"
13
 
14
- # --- Pydantic Models for Data Validation ---
15
- # This ensures the API response from CoinGecko matches our expectations.
16
- class PriceData(BaseModel):
17
- usd: float
18
 
19
- # The full response is a dictionary mapping coin names (str) to their PriceData.
20
- # Example: {"bitcoin": {"usd": 65000.0}}
21
- ApiResponse = dict[str, PriceData]
22
 
23
- # --- Application State and Lifespan Management ---
24
- # Using a lifespan event is the modern way to manage resources like HTTP clients.
25
- # This client will be created on startup and closed gracefully on shutdown.
26
- app_state = {}
27
 
28
  @asynccontextmanager
29
  async def lifespan(app: FastAPI):
30
- # On startup: create a single, reusable httpx client.
31
- app_state["http_client"] = httpx.AsyncClient()
32
- yield
33
- # On shutdown: close the client.
34
- await app_state["http_client"].aclose()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  # --- FastAPI App Initialization ---
37
- app = FastAPI(lifespan=lifespan)
38
- templates = Jinja2Templates(directory="templates")
39
 
 
 
40
 
41
  # --- API Endpoints ---
42
 
43
  @app.get("/", response_class=HTMLResponse)
44
- async def serve_home_page(request: Request):
45
- """Serves the main HTML page which will then trigger HTMX calls."""
46
  return templates.TemplateResponse("index.html", {"request": request})
47
 
48
  @app.get("/api/prices", response_class=HTMLResponse)
49
- async def get_prices_ui():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  """
51
- This endpoint is called by HTMX.
52
- It fetches crypto data and returns an HTML FRAGMENT to be swapped into the page.
53
  """
54
- try:
55
- client: httpx.AsyncClient = app_state["http_client"]
56
-
57
- # Build the request parameters dynamically
58
- params = {
59
- "ids": ",".join(TARGET_COINS),
60
- "vs_currencies": TARGET_CURRENCY
61
- }
62
-
63
- response = await client.get(COINGECKO_API_URL, params=params)
64
- response.raise_for_status() # Raise an exception for 4xx/5xx errors
65
 
66
- # Validate the received data against our Pydantic model
67
- prices = ApiResponse(**response.json())
68
-
69
- # This is a simple but effective way to render an HTML fragment.
70
- # For larger fragments, you would use another Jinja2 template.
71
- html_fragment = ""
72
- for coin, data in prices.items():
73
- html_fragment += f"<div><strong>{coin.capitalize()}:</strong> ${data.usd:,.2f}</div>"
74
-
75
- return HTMLResponse(content=html_fragment)
76
 
77
- except httpx.RequestError as e:
78
- # Handle network-related errors
79
- return HTMLResponse(content=f"<div class='error'>Network error: Could not connect to CoinGecko.</div>", status_code=503)
80
- except ValidationError as e:
81
- # Handle cases where CoinGecko's API response changes unexpectedly
82
- return HTMLResponse(content=f"<div class='error'>Invalid API response from data source.</div>", status_code=502)
83
- except Exception as e:
84
- # Generic catch-all for other unexpected errors
85
- return HTMLResponse(content=f"<div class='error'>An unexpected error occurred.</div>", status_code=500)
86
-
87
-
88
- @app.get("/api/forecast/{coin_id}")
89
- async def get_forecast(coin_id: str):
90
  """
91
- Placeholder for a real forecasting model.
92
- A real implementation would involve loading a pre-trained model,
93
- fetching historical data, and running a prediction.
94
  """
95
- if coin_id not in TARGET_COINS:
96
- raise HTTPException(status_code=404, detail=f"Forecast not available for '{coin_id}'")
 
 
 
 
97
 
98
- # In a real app, this would be the output of your ML model.
99
- mock_forecast = {
100
- "coin_id": coin_id,
101
- "prediction_in_24h": "up",
102
- "confidence": 0.78,
103
- "detail": "This is a mock forecast. A real implementation requires a data science model."
104
- }
105
- return mock_forecast
106
-
107
- # --- Main execution (for development) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  if __name__ == "__main__":
109
- # To run: uvicorn main:app --reload --port 7860
110
  import uvicorn
111
- uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
 
 
1
+ """
2
+ CryptoSentinel AI — High-performance FastAPI application.
3
+
4
+ This is the main entry point that orchestrates the entire application.
5
+ - Integrates the asynchronous PriceFetcher for live market data.
6
+ - Integrates the asynchronous SentimentAnalyzer for real-time analysis.
7
+ - Serves the interactive frontend and provides all necessary API endpoints.
8
+ """
9
+ import asyncio
10
+ import json
11
+ from contextlib import asynccontextmanager
12
+
13
  import httpx
14
+ from fastapi import FastAPI, Request, BackgroundTasks
15
+ from fastapi.responses import HTMLResponse, StreamingResponse
16
  from fastapi.templating import Jinja2Templates
17
+ from pydantic import BaseModel, constr
 
18
 
19
+ # Import our modular, asynchronous service classes
20
+ from app.price_fetcher import PriceFetcher
21
+ from app.sentiment import SentimentAnalyzer
 
 
22
 
23
+ # --- Pydantic Model for API Input Validation ---
 
 
 
24
 
25
+ class SentimentRequest(BaseModel):
26
+ """Ensures the text for sentiment analysis is a non-empty string."""
27
+ text: constr(strip_whitespace=True, min_length=1)
28
 
29
+ # --- Application Lifespan for Resource Management ---
 
 
 
30
 
31
  @asynccontextmanager
32
  async def lifespan(app: FastAPI):
33
+ """
34
+ Manages application startup and shutdown events using the modern
35
+ lifespan context manager.
36
+ """
37
+ # On startup:
38
+ async with httpx.AsyncClient() as client:
39
+ # Instantiate and store our services in the application state.
40
+ # This makes them accessible in any request handler via `request.app.state`.
41
+ app.state.price_fetcher = PriceFetcher(client=client, coins=["bitcoin", "ethereum", "dogecoin"])
42
+ app.state.sentiment_analyzer = SentimentAnalyzer(client=client)
43
+ app.state.request_counter = 0 # Simple counter for unique SSE event IDs
44
+
45
+ # Create a cancellable background task for continuous price updates.
46
+ price_update_task = asyncio.create_task(
47
+ run_periodic_updates(app.state.price_fetcher, interval_seconds=10)
48
+ )
49
+
50
+ print("🚀 CryptoSentinel AI started successfully.")
51
+ yield # The application is now running and ready to accept requests.
52
+
53
+ # On shutdown:
54
+ print("⏳ Shutting down background tasks...")
55
+ price_update_task.cancel()
56
+ try:
57
+ await price_update_task
58
+ except asyncio.CancelledError:
59
+ print("Price update task cancelled successfully.")
60
+ print("✅ Shutdown complete.")
61
+
62
+ async def run_periodic_updates(fetcher: PriceFetcher, interval_seconds: int):
63
+ """A robust asyncio background task that periodically updates prices."""
64
+ while True:
65
+ await fetcher.update_prices_async()
66
+ await asyncio.sleep(interval_seconds)
67
 
68
  # --- FastAPI App Initialization ---
 
 
69
 
70
+ app = FastAPI(title="CryptoSentinel AI", lifespan=lifespan)
71
+ templates = Jinja2Templates(directory="app/templates")
72
 
73
  # --- API Endpoints ---
74
 
75
  @app.get("/", response_class=HTMLResponse)
76
+ async def serve_dashboard(request: Request):
77
+ """Serves the main interactive dashboard from `index.html`."""
78
  return templates.TemplateResponse("index.html", {"request": request})
79
 
80
  @app.get("/api/prices", response_class=HTMLResponse)
81
+ async def get_prices_fragment(request: Request):
82
+ """Returns an HTML fragment with the latest cached crypto prices for HTMX."""
83
+ price_fetcher: PriceFetcher = request.app.state.price_fetcher
84
+ prices = price_fetcher.get_current_prices()
85
+
86
+ html_fragment = ""
87
+ for coin, price in prices.items():
88
+ # Format the price nicely, handling the initial '--' state
89
+ price_str = f"${price:,.2f}" if isinstance(price, (int, float)) else price
90
+ html_fragment += f"<div><strong>{coin.capitalize()}:</strong> {price_str}</div>"
91
+
92
+ return HTMLResponse(content=html_fragment)
93
+
94
+ @app.post("/api/sentiment")
95
+ async def analyze_sentiment(
96
+ payload: SentimentRequest,
97
+ request: Request,
98
+ background_tasks: BackgroundTasks
99
+ ):
100
  """
101
+ Validates and queues a text for sentiment analysis. The heavy lifting is
102
+ done in the background to ensure the API responds instantly.
103
  """
104
+ analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
105
+ request.app.state.request_counter += 1
106
+ request_id = request.app.state.request_counter
 
 
 
 
 
 
 
 
107
 
108
+ # The actual API call to Hugging Face will run after this response is sent.
109
+ background_tasks.add_task(analyzer.compute_and_publish, payload.text, request_id)
110
+
111
+ return HTMLResponse(content="<small>Queued for analysis...</small>")
 
 
 
 
 
 
112
 
113
+ @app.get("/api/sentiment/stream")
114
+ async def sentiment_stream(request: Request):
 
 
 
 
 
 
 
 
 
 
 
115
  """
116
+ Establishes a Server-Sent Events (SSE) connection. It efficiently pushes
117
+ new sentiment results as HTML fragments to the client as they become available.
 
118
  """
119
+ analyzer: SentimentAnalyzer = request.app.state.sentiment_analyzer
120
+
121
+ async def event_generator():
122
+ # Clear the initial "waiting..." message on the client.
123
+ # hx-swap-oob="innerHTML" swaps this div out-of-band without affecting the target.
124
+ yield f"event: sentiment_update\ndata: <div id='sentiment-results' hx-swap-oob='innerHTML'></div>\n\n"
125
 
126
+ # Listen for new results from the analyzer's internal queue.
127
+ async for result_payload in analyzer.stream_results():
128
+ try:
129
+ result = result_payload['result']
130
+ label = str(result.get('label', 'NEUTRAL')).lower()
131
+ score = result.get('score', 0.0) * 100
132
+ text = result_payload['text']
133
+
134
+ # Dynamically build the HTML fragment to be sent to the client.
135
+ html_fragment = f"""
136
+ <div>
137
+ <blockquote>{text}</blockquote>
138
+ <p>
139
+ <strong>Result:</strong>
140
+ <span class="sentiment-{label}">{label.upper()}</span>
141
+ (Confidence: {score:.1f}%)
142
+ </p>
143
+ </div>
144
+ """
145
+ # Send the fragment using our custom event name.
146
+ yield f"event: sentiment_update\ndata: {html_fragment.replace('\n', '')}\n\n"
147
+ except (KeyError, TypeError):
148
+ continue # Ignore malformed payloads
149
+
150
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
151
+
152
+ # --- Main execution block for local development ---
153
  if __name__ == "__main__":
 
154
  import uvicorn
155
+ # Correct run command for a file named 'app.py'
156
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)