Spaces:
Running
Running
| from fastapi import FastAPI, Request, Query, HTTPException | |
| from fastapi.responses import JSONResponse | |
| from typing import Optional | |
| import uvicorn | |
| from models.weather_models import ( | |
| WeatherResponse, | |
| AutocompleteResponse, | |
| ErrorResponse, | |
| ErrorDetail, | |
| RequestModel, | |
| ) | |
| from services.api_client import lifespan | |
| app = FastAPI(title="Weatherstack-Compatible API", lifespan=lifespan) | |
| async def health(): | |
| return {"status": "ok", "message": "Weatherhack API is live"} | |
| import os | |
| from pydantic import Field | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| class Settings(BaseSettings): | |
| weatherstack_access_key: str = Field("test", alias="WEATHERSTACK_ACCESS_KEY") | |
| model_config = SettingsConfigDict( | |
| env_file=".env", | |
| extra="ignore", | |
| ) | |
| settings = Settings() | |
| # Full debug for HF environment | |
| if "WEATHERSTACK_ACCESS_KEY" in os.environ: | |
| env_val = os.environ["WEATHERSTACK_ACCESS_KEY"] | |
| print(f"DEBUG: Found in os.environ. Length: {len(env_val)}") | |
| else: | |
| print("DEBUG: NOT found in os.environ") | |
| print( | |
| f"DEBUG: Settings value: {settings.weatherstack_access_key[:4]}...{settings.weatherstack_access_key[-4:]}" | |
| ) | |
| def validate_key(access_key: str): | |
| if access_key != settings.weatherstack_access_key: | |
| return ErrorResponse( | |
| success=False, | |
| error=ErrorDetail( | |
| code=101, type="unauthorized", info="Invalid API access key." | |
| ), | |
| ) | |
| return None | |
| from services.location_service import get_location_details | |
| from services.weather_service import fetch_current_weather | |
| async def get_current_weather( | |
| access_key: str, | |
| query: str, | |
| units: str = "m", | |
| language: str = "en", | |
| callback: Optional[str] = None, | |
| ): | |
| error = validate_key(access_key) | |
| if error: | |
| return JSONResponse(status_code=401, content=error.model_dump()) | |
| location = await get_location_details(query) | |
| if not location: | |
| return JSONResponse( | |
| status_code=404, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail( | |
| code=602, type="invalid_query", info="Invalid location query." | |
| ), | |
| ).model_dump(), | |
| ) | |
| try: | |
| current_weather = await fetch_current_weather( | |
| float(location.lat), float(location.lon), unit=units | |
| ) | |
| response = WeatherResponse( | |
| request=RequestModel( | |
| type="City", # This should be dynamic based on query type | |
| query=query, | |
| language=language, | |
| unit=units, | |
| ), | |
| location=location, | |
| current=current_weather, | |
| ) | |
| return response | |
| except Exception as e: | |
| return JSONResponse( | |
| status_code=500, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail(code=615, type="request_failed", info=str(e)), | |
| ).model_dump(), | |
| ) | |
| from services.weather_service import fetch_historical_weather, fetch_weather_forecast | |
| from services.location_service import get_autocomplete_suggestions | |
| async def get_historical_weather( | |
| access_key: str, | |
| query: str, | |
| historical_date: Optional[str] = None, | |
| historical_date_start: Optional[str] = None, | |
| historical_date_end: Optional[str] = None, | |
| units: str = "m", | |
| language: str = "en", | |
| hourly: int = 0, | |
| interval: int = 3, | |
| ): | |
| error = validate_key(access_key) | |
| if error: | |
| return JSONResponse(status_code=401, content=error.model_dump()) | |
| location = await get_location_details(query) | |
| if not location: | |
| return JSONResponse( | |
| status_code=404, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail( | |
| code=602, type="invalid_query", info="Invalid location query." | |
| ), | |
| ).model_dump(), | |
| ) | |
| date = historical_date or historical_date_start | |
| if not date: | |
| return JSONResponse( | |
| status_code=400, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail( | |
| code=614, type="missing_historical_date", info="No date provided." | |
| ), | |
| ).model_dump(), | |
| ) | |
| try: | |
| hist_data = await fetch_historical_weather( | |
| float(location.lat), float(location.lon), date, unit=units | |
| ) | |
| return WeatherResponse( | |
| request=RequestModel( | |
| type="City", query=query, language=language, unit=units | |
| ), | |
| location=location, | |
| historical=hist_data, | |
| ) | |
| except Exception as e: | |
| return JSONResponse( | |
| status_code=500, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail(code=615, type="request_failed", info=str(e)), | |
| ).model_dump(), | |
| ) | |
| async def get_weather_forecast( | |
| access_key: str, | |
| query: str, | |
| forecast_days: int, | |
| units: str = "m", | |
| language: str = "en", | |
| hourly: int = 0, | |
| interval: int = 3, | |
| ): | |
| error = validate_key(access_key) | |
| if error: | |
| return JSONResponse(status_code=401, content=error.model_dump()) | |
| location = await get_location_details(query) | |
| if not location: | |
| return JSONResponse( | |
| status_code=404, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail( | |
| code=602, type="invalid_query", info="Invalid location query." | |
| ), | |
| ).model_dump(), | |
| ) | |
| try: | |
| forecast_data = await fetch_weather_forecast( | |
| float(location.lat), float(location.lon), forecast_days, unit=units | |
| ) | |
| return WeatherResponse( | |
| request=RequestModel( | |
| type="City", query=query, language=language, unit=units | |
| ), | |
| location=location, | |
| forecast=forecast_data, | |
| ) | |
| except Exception as e: | |
| return JSONResponse( | |
| status_code=500, | |
| content=ErrorResponse( | |
| success=False, | |
| error=ErrorDetail(code=615, type="request_failed", info=str(e)), | |
| ).model_dump(), | |
| ) | |
| async def autocomplete_location(access_key: str, query: str): | |
| error = validate_key(access_key) | |
| if error: | |
| return JSONResponse(status_code=401, content=error.model_dump()) | |
| suggestions = await get_autocomplete_suggestions(query) | |
| return AutocompleteResponse( | |
| request={"query": query, "type": "City"}, locations=suggestions | |
| ) | |
| from fastapi.responses import HTMLResponse, RedirectResponse | |
| async def root(): | |
| return RedirectResponse(url="/dashboard") | |
| async def dashboard(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Weatherhack API | Dashboard</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --primary-dark: #1e40af; | |
| --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); | |
| --glass: rgba(255, 255, 255, 0.05); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg-gradient); | |
| color: white; | |
| min-height: 100vh; | |
| margin: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 2rem; | |
| } | |
| .container { | |
| max-width: 900px; | |
| width: 100%; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 3rem; | |
| } | |
| .header h1 { | |
| font-size: 3rem; | |
| margin-bottom: 0.5rem; | |
| background: linear-gradient(to right, #60a5fa, #a855f7); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .card { | |
| background: var(--glass); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 1.5rem; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| flex-wrap: wrap; | |
| } | |
| input, select { | |
| background: rgba(0,0,0,0.2); | |
| border: 1px solid var(--glass-border); | |
| color: white; | |
| padding: 0.75rem 1rem; | |
| border-radius: 0.75rem; | |
| font-size: 1rem; | |
| flex: 1; | |
| min-width: 200px; | |
| } | |
| button { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 2rem; | |
| border-radius: 0.75rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| button:hover { | |
| background: var(--primary-dark); | |
| transform: translateY(-2px); | |
| } | |
| pre { | |
| background: #000; | |
| padding: 1rem; | |
| border-radius: 1rem; | |
| overflow-x: auto; | |
| max-height: 400px; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .stat { | |
| background: rgba(255,255,255,0.03); | |
| padding: 1.5rem; | |
| border-radius: 1rem; | |
| text-align: center; | |
| } | |
| .stat h3 { margin: 0; color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } | |
| .stat p { margin: 0.5rem 0 0; font-size: 1.5rem; font-weight: 700; } | |
| .badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 0.5rem; background: var(--primary); font-size: 0.75rem; font-weight: 700; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🌤️ Weatherhack API</h1> | |
| <p>Premium, Zero-Cost Weather Infrastructure</p> | |
| </div> | |
| <div class="card"> | |
| <h3>🔑 API Configuration</h3> | |
| <div class="input-group"> | |
| <input type="password" id="access_key" placeholder="Enter Access Key" value="b55a6ab9aec1ab156c0b46f0a9ef93dd"> | |
| <input type="text" id="query" placeholder="City or Lat,Lon" value="Monticello, Indiana"> | |
| <select id="units"> | |
| <option value="m">Metric</option> | |
| <option value="f">Fahrenheit</option> | |
| <option value="s">Scientific</option> | |
| </select> | |
| </div> | |
| <button onclick="fetchWeather()">Fetch Current Weather</button> | |
| </div> | |
| <div id="results" style="display:none;"> | |
| <div class="card"> | |
| <div class="grid" id="stats-grid"></div> | |
| <h3>📜 API Response</h3> | |
| <pre id="json-output"></pre> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| async function fetchWeather() { | |
| const key = document.getElementById('access_key').value; | |
| const query = document.getElementById('query').value; | |
| const units = document.getElementById('units').value; | |
| const btn = document.querySelector('button'); | |
| btn.innerText = 'Loading...'; | |
| btn.disabled = true; | |
| try { | |
| const url = `/current?access_key=${key}&query=${query}&units=${units}`; | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| document.getElementById('results').style.display = 'block'; | |
| document.getElementById('json-output').innerText = JSON.stringify(data, null, 2); | |
| if (data.current) { | |
| const stats = [ | |
| { label: 'Temperature', val: `${data.current.temperature}°` }, | |
| { label: 'Condition', val: data.current.weather_descriptions[0] }, | |
| { label: 'Wind Speed', val: `${data.current.wind_speed} ${units === 'f' ? 'mph' : 'km/h'}` }, | |
| { label: 'Air Quality (PM2.5)', val: data.current.air_quality ? data.current.air_quality.pm2_5 : 'N/A' } | |
| ]; | |
| document.getElementById('stats-grid').innerHTML = stats.map(s => ` | |
| <div class="stat"> | |
| <h3>${s.label}</h3> | |
| <p>${s.val}</p> | |
| </div> | |
| `).join(''); | |
| } else { | |
| document.getElementById('stats-grid').innerHTML = '<div class="stat"><p style="color:#f87171;">API Error: ' + (data.error ? data.error.info : 'Unknown error') + '</p></div>'; | |
| } | |
| } catch (e) { | |
| alert('Connection failure. Make sure the API is accessible.'); | |
| } finally { | |
| btn.innerText = 'Fetch Current Weather'; | |
| btn.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| if __name__ == "__main__": | |
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) | |