weatherhack-api / main.py
Ig0tU
Performance optimization: Added caching (24h loc, 5m weather) and connection pooling
4833096
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)
@app.get("/health", include_in_schema=False)
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
@app.get("/current", response_model=WeatherResponse)
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
@app.get("/historical", response_model=WeatherResponse)
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(),
)
@app.get("/forecast", response_model=WeatherResponse)
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(),
)
@app.get("/autocomplete", response_model=AutocompleteResponse)
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
@app.get("/", include_in_schema=False)
async def root():
return RedirectResponse(url="/dashboard")
@app.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
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)