feat: ship MVP itinerary optimization, weather, AI key prefs, and MCP tools
This commit is contained in:
154
backend/server/adventures/views/weather_view.py
Normal file
154
backend/server/adventures/views/weather_view.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from datetime import date as date_cls
|
||||
|
||||
import requests
|
||||
from django.core.cache import cache
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WeatherViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
OPEN_METEO_ARCHIVE_URL = "https://archive-api.open-meteo.com/v1/archive"
|
||||
OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
CACHE_TIMEOUT_SECONDS = 60 * 60 * 6
|
||||
MAX_DAYS_PER_REQUEST = 60
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="daily-temperatures")
|
||||
def daily_temperatures(self, request):
|
||||
days = request.data.get("days", [])
|
||||
if not isinstance(days, list):
|
||||
return Response(
|
||||
{"error": "'days' must be a list"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
if len(days) > self.MAX_DAYS_PER_REQUEST:
|
||||
return Response(
|
||||
{
|
||||
"error": f"A maximum of {self.MAX_DAYS_PER_REQUEST} days is allowed per request"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
results = []
|
||||
for entry in days:
|
||||
if not isinstance(entry, dict):
|
||||
results.append(
|
||||
{"date": None, "available": False, "temperature_c": None}
|
||||
)
|
||||
continue
|
||||
|
||||
date = entry.get("date")
|
||||
latitude = entry.get("latitude")
|
||||
longitude = entry.get("longitude")
|
||||
|
||||
if not date or latitude is None or longitude is None:
|
||||
results.append(
|
||||
{"date": date, "available": False, "temperature_c": None}
|
||||
)
|
||||
continue
|
||||
|
||||
parsed_date = self._parse_date(date)
|
||||
if parsed_date is None:
|
||||
results.append(
|
||||
{"date": date, "available": False, "temperature_c": None}
|
||||
)
|
||||
continue
|
||||
|
||||
if parsed_date > date_cls.today():
|
||||
results.append(
|
||||
{"date": date, "available": False, "temperature_c": None}
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
lat = float(latitude)
|
||||
lon = float(longitude)
|
||||
except (TypeError, ValueError):
|
||||
results.append(
|
||||
{"date": date, "available": False, "temperature_c": None}
|
||||
)
|
||||
continue
|
||||
|
||||
cache_key = self._cache_key(date, lat, lon)
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
results.append(cached)
|
||||
continue
|
||||
|
||||
payload = self._fetch_daily_temperature(date, lat, lon)
|
||||
cache.set(cache_key, payload, timeout=self.CACHE_TIMEOUT_SECONDS)
|
||||
results.append(payload)
|
||||
|
||||
return Response({"results": results}, status=status.HTTP_200_OK)
|
||||
|
||||
def _fetch_daily_temperature(self, date: str, latitude: float, longitude: float):
|
||||
base_payload = {
|
||||
"date": date,
|
||||
"available": False,
|
||||
"temperature_c": None,
|
||||
}
|
||||
|
||||
for url in (self.OPEN_METEO_ARCHIVE_URL, self.OPEN_METEO_FORECAST_URL):
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
params={
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"start_date": date,
|
||||
"end_date": date,
|
||||
"daily": "temperature_2m_max,temperature_2m_min",
|
||||
"timezone": "UTC",
|
||||
},
|
||||
timeout=8,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.RequestException:
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
daily = data.get("daily") or {}
|
||||
max_values = daily.get("temperature_2m_max") or []
|
||||
min_values = daily.get("temperature_2m_min") or []
|
||||
if not max_values or not min_values:
|
||||
continue
|
||||
|
||||
try:
|
||||
avg = (float(max_values[0]) + float(min_values[0])) / 2
|
||||
except (TypeError, ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return {
|
||||
"date": date,
|
||||
"available": True,
|
||||
"temperature_c": round(avg, 1),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"No weather data available for date=%s lat=%s lon=%s",
|
||||
date,
|
||||
latitude,
|
||||
longitude,
|
||||
)
|
||||
return base_payload
|
||||
|
||||
def _cache_key(self, date: str, latitude: float, longitude: float) -> str:
|
||||
rounded_lat = round(latitude, 3)
|
||||
rounded_lon = round(longitude, 3)
|
||||
raw = f"{date}:{rounded_lat}:{rounded_lon}"
|
||||
digest = hashlib.sha256(raw.encode()).hexdigest()
|
||||
return f"weather_daily:{digest}"
|
||||
|
||||
def _parse_date(self, value: str):
|
||||
try:
|
||||
return date_cls.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
Reference in New Issue
Block a user