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_session 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: Settings = Depends(get_current_session) ): 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_session) ): # Get settings result = await db.execute(select(Settings)) settings_row = result.scalar_one_or_none() city = settings_row.weather_city if settings_row else None lat = settings_row.weather_lat if settings_row else None lon = settings_row.weather_lon if settings_row else None 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")