149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
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
|
|
|
|
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
|