- Add weather router with OpenWeatherMap integration and 1-hour cache - Add is_starred column to calendar events with countdown widget - Add weather_city setting with Settings page input - Replace people/locations stats with open todos count + weather card - Add quick-add dropdown (event/todo/reminder) to dashboard header - Make CalendarWidget events single-line thin rows - Add rain warnings to smart briefing when chance > 40% - Invalidate dashboard/upcoming queries on form mutations - Migration 004: is_starred + weather_city columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from datetime import datetime, timedelta
|
|
import urllib.request
|
|
import urllib.parse
|
|
import urllib.error
|
|
import json
|
|
|
|
from app.database import get_db
|
|
from app.models.settings import Settings
|
|
from app.config import settings as app_settings
|
|
from app.routers.auth import get_current_session
|
|
|
|
router = APIRouter()
|
|
|
|
_cache: dict = {}
|
|
|
|
|
|
@router.get("/weather")
|
|
async def get_weather(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: Settings = Depends(get_current_session)
|
|
):
|
|
# Check cache
|
|
now = datetime.now()
|
|
if _cache.get("expires_at") and now < _cache["expires_at"]:
|
|
return _cache["data"]
|
|
|
|
# Get city from settings
|
|
result = await db.execute(select(Settings))
|
|
settings_row = result.scalar_one_or_none()
|
|
city = settings_row.weather_city if settings_row else None
|
|
if not city:
|
|
raise HTTPException(status_code=400, detail="No weather city configured")
|
|
|
|
api_key = app_settings.OPENWEATHERMAP_API_KEY
|
|
if not api_key:
|
|
raise HTTPException(status_code=500, detail="Weather API key not configured")
|
|
|
|
try:
|
|
# Current weather
|
|
current_url = f"https://api.openweathermap.org/data/2.5/weather?q={urllib.parse.quote(city)}&units=metric&appid={api_key}"
|
|
with urllib.request.urlopen(current_url, timeout=10) as resp:
|
|
current_data = json.loads(resp.read().decode())
|
|
|
|
# Forecast for rain probability
|
|
forecast_url = f"https://api.openweathermap.org/data/2.5/forecast?q={urllib.parse.quote(city)}&units=metric&cnt=8&appid={api_key}"
|
|
with urllib.request.urlopen(forecast_url, timeout=10) as resp:
|
|
forecast_data = json.loads(resp.read().decode())
|
|
|
|
rain_chance = 0
|
|
for item in forecast_data.get("list", []):
|
|
pop = item.get("pop", 0)
|
|
if pop > rain_chance:
|
|
rain_chance = pop
|
|
|
|
weather_result = {
|
|
"temp": round(current_data["main"]["temp"]),
|
|
"temp_min": round(current_data["main"]["temp_min"]),
|
|
"temp_max": round(current_data["main"]["temp_max"]),
|
|
"description": current_data["weather"][0]["description"],
|
|
"rain_chance": round(rain_chance * 100),
|
|
"sunrise": current_data["sys"]["sunrise"],
|
|
"sunset": current_data["sys"]["sunset"],
|
|
"city": current_data["name"],
|
|
}
|
|
|
|
# Cache for 1 hour
|
|
_cache["data"] = weather_result
|
|
_cache["expires_at"] = now + timedelta(hours=1)
|
|
|
|
return weather_result
|
|
|
|
except urllib.error.URLError as e:
|
|
raise HTTPException(status_code=502, detail=f"Weather service unavailable: {str(e)}")
|
|
except (KeyError, json.JSONDecodeError) as e:
|
|
raise HTTPException(status_code=502, detail=f"Invalid weather data: {str(e)}")
|