from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from datetime import datetime, timedelta import asyncio 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_user, get_current_settings from app.models.user import User router = APIRouter() _cache: dict = {} class GeoSearchResult(BaseModel): name: str state: str country: str lat: float lon: float def _fetch_json(url: str) -> dict: with urllib.request.urlopen(url, timeout=10) as resp: return json.loads(resp.read().decode()) @router.get("/search", response_model=list[GeoSearchResult]) async def search_locations( q: str = Query(..., min_length=1, max_length=100), current_user: User = Depends(get_current_user) ): api_key = app_settings.OPENWEATHERMAP_API_KEY if not api_key: raise HTTPException(status_code=500, detail="Weather API key not configured") try: loop = asyncio.get_running_loop() url = f"https://api.openweathermap.org/geo/1.0/direct?q={urllib.parse.quote(q)}&limit=5&appid={api_key}" results = await loop.run_in_executor(None, _fetch_json, url) return [ { "name": r.get("name", ""), "state": r.get("state", ""), "country": r.get("country", ""), "lat": r.get("lat"), "lon": r.get("lon"), } for r in results ] except urllib.error.URLError: raise HTTPException(status_code=502, detail="Geocoding service unavailable") except (KeyError, json.JSONDecodeError): raise HTTPException(status_code=502, detail="Invalid geocoding data") @router.get("/") async def get_weather( db: AsyncSession = Depends(get_db), current_user: Settings = Depends(get_current_settings) ): city = current_user.weather_city lat = current_user.weather_lat lon = current_user.weather_lon if not city and (lat is None or lon is None): raise HTTPException(status_code=400, detail="No weather location configured") # Build cache key from coordinates or city use_coords = lat is not None and lon is not None cache_key = f"{lat},{lon}" if use_coords else city # Check cache now = datetime.now() if _cache.get("expires_at") and now < _cache["expires_at"] and _cache.get("cache_key") == cache_key: return _cache["data"] api_key = app_settings.OPENWEATHERMAP_API_KEY if not api_key: raise HTTPException(status_code=500, detail="Weather API key not configured") try: loop = asyncio.get_running_loop() # Build query params based on coordinates or city name if use_coords: location_params = f"lat={lat}&lon={lon}" else: location_params = f"q={urllib.parse.quote(city)}" current_url = f"https://api.openweathermap.org/data/2.5/weather?{location_params}&units=metric&appid={api_key}" forecast_url = f"https://api.openweathermap.org/data/2.5/forecast?{location_params}&units=metric&cnt=8&appid={api_key}" current_data, forecast_data = await asyncio.gather( loop.run_in_executor(None, _fetch_json, current_url), loop.run_in_executor(None, _fetch_json, forecast_url), ) 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) _cache["cache_key"] = cache_key return weather_result except urllib.error.URLError: raise HTTPException(status_code=502, detail="Weather service unavailable") except (KeyError, json.JSONDecodeError): raise HTTPException(status_code=502, detail="Invalid weather data")