feat(briefing): add past_days/current_weather to Open-Meteo; add parse_weather_card_data()
Also adds get_cached_weather_rows() for parallel gather in briefing pipeline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,76 @@ def detect_changes(old_days: list[dict], new_days: list[dict]) -> list[str]:
|
||||
return changes
|
||||
|
||||
|
||||
def parse_weather_card_data(
|
||||
cache_row,
|
||||
temp_unit: str = "C",
|
||||
) -> dict | None:
|
||||
"""
|
||||
Parse a WeatherCache row into the metadata.weather card schema.
|
||||
Returns None if the cache is stale (older than 24 hours) or unavailable.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
if cache_row is None or cache_row.fetched_at is None:
|
||||
return None
|
||||
|
||||
age_seconds = (datetime.now(timezone.utc) - cache_row.fetched_at).total_seconds()
|
||||
if age_seconds > 86400:
|
||||
return None
|
||||
|
||||
raw = cache_row.forecast_json or {}
|
||||
current_weather = raw.get("current_weather", {})
|
||||
days = parse_forecast(raw)
|
||||
|
||||
today_str = date.today().isoformat()
|
||||
yesterday_str = (date.today() - timedelta(days=1)).isoformat()
|
||||
|
||||
today_day = next((d for d in days if d["date"] == today_str), None)
|
||||
yesterday_day = next((d for d in days if d["date"] == yesterday_str), None)
|
||||
future_days = [d for d in days if d["date"] > today_str][:5]
|
||||
|
||||
def to_temp(c: float) -> int:
|
||||
if temp_unit == "F":
|
||||
return round(c * 9 / 5 + 32)
|
||||
return round(c)
|
||||
|
||||
def day_label(date_str: str) -> str:
|
||||
from datetime import date as _date
|
||||
try:
|
||||
return _date.fromisoformat(date_str).strftime("%a")
|
||||
except ValueError:
|
||||
return date_str
|
||||
|
||||
return {
|
||||
"location": getattr(cache_row, "location_label", ""),
|
||||
"fetched_at": cache_row.fetched_at.isoformat(),
|
||||
"current_temp": to_temp(current_weather.get("temperature", 0)),
|
||||
"condition": describe_weathercode(current_weather.get("weathercode", 0)),
|
||||
"today_high": to_temp(today_day["temp_max"]) if today_day else None,
|
||||
"today_low": to_temp(today_day["temp_min"]) if today_day else None,
|
||||
"yesterday_high": to_temp(yesterday_day["temp_max"]) if yesterday_day else None,
|
||||
"yesterday_low": to_temp(yesterday_day["temp_min"]) if yesterday_day else None,
|
||||
"forecast": [
|
||||
{
|
||||
"day": day_label(d["date"]),
|
||||
"condition": d["description"],
|
||||
"high": to_temp(d["temp_max"]),
|
||||
"low": to_temp(d["temp_min"]),
|
||||
}
|
||||
for d in future_days
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def get_cached_weather_rows(user_id: int) -> list:
|
||||
"""Return raw WeatherCache ORM rows for a user (for card parsing)."""
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
select(WeatherCache).where(WeatherCache.user_id == user_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def geocode(query: str) -> tuple[float, float, str]:
|
||||
"""Return (lat, lon, display_name) for a place name using Nominatim."""
|
||||
async with httpx.AsyncClient(timeout=10.0, headers={"User-Agent": "FabledAssistant/1.0"}) as client:
|
||||
@@ -104,12 +174,14 @@ async def geocode(query: str) -> tuple[float, float, str]:
|
||||
|
||||
|
||||
async def _fetch_open_meteo(lat: float, lon: float) -> dict:
|
||||
"""Fetch 7-day forecast from Open-Meteo."""
|
||||
"""Fetch 7-day forecast from Open-Meteo with current conditions and yesterday's data."""
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(OPEN_METEO_URL, params={
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"daily": OPEN_METEO_DAILY,
|
||||
"current_weather": "true",
|
||||
"past_days": 1,
|
||||
"timezone": "auto",
|
||||
"forecast_days": 7,
|
||||
})
|
||||
|
||||
@@ -54,3 +54,52 @@ def test_detect_forecast_changes_rain_added():
|
||||
assert len(changes) == 1
|
||||
assert "2026-03-12" in changes[0]
|
||||
assert "rain" in changes[0].lower() or "precip" in changes[0].lower()
|
||||
|
||||
|
||||
def test_parse_weather_card_data_returns_none_when_stale():
|
||||
"""Should return None when cache is older than 24 hours."""
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
from fabledassistant.services.weather import parse_weather_card_data
|
||||
|
||||
cache = MagicMock()
|
||||
cache.fetched_at = datetime.now(timezone.utc) - timedelta(hours=25)
|
||||
cache.forecast_json = {}
|
||||
assert parse_weather_card_data(cache) is None
|
||||
|
||||
|
||||
def test_parse_weather_card_data_returns_card_schema():
|
||||
"""Should return the correct metadata.weather schema for fresh cache."""
|
||||
from datetime import datetime, timezone, timedelta, date
|
||||
from unittest.mock import MagicMock
|
||||
from fabledassistant.services.weather import parse_weather_card_data
|
||||
|
||||
today = date.today().isoformat()
|
||||
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
||||
tomorrow = (date.today() + timedelta(days=1)).isoformat()
|
||||
|
||||
cache = MagicMock()
|
||||
cache.fetched_at = datetime.now(timezone.utc)
|
||||
cache.location_label = "Berlin, DE"
|
||||
cache.forecast_json = {
|
||||
"current_weather": {"temperature": 12.0, "weathercode": 2},
|
||||
"daily": {
|
||||
"time": [yesterday, today, tomorrow],
|
||||
"temperature_2m_max": [14.0, 16.0, 18.0],
|
||||
"temperature_2m_min": [9.0, 8.0, 10.0],
|
||||
"precipitation_sum": [0.0, 0.0, 1.5],
|
||||
"weathercode": [1, 2, 61],
|
||||
"windspeed_10m_max": [10.0, 12.0, 8.0],
|
||||
}
|
||||
}
|
||||
|
||||
card = parse_weather_card_data(cache)
|
||||
assert card is not None
|
||||
assert card["current_temp"] == 12
|
||||
assert card["condition"] == "Partly cloudy"
|
||||
assert card["today_high"] == 16
|
||||
assert card["today_low"] == 8
|
||||
assert card["yesterday_high"] == 14
|
||||
assert card["yesterday_low"] == 9
|
||||
assert len(card["forecast"]) >= 1
|
||||
assert card["location"] == "Berlin, DE"
|
||||
|
||||
Reference in New Issue
Block a user