feat: Add city weather query service with city resolution, forecast processing, and weather summary generation.
This commit is contained in:
@@ -103,7 +103,8 @@ def _derive_mgm_daily_highs_from_hourly(
|
|||||||
if not isinstance(hourly, list) or not hourly:
|
if not isinstance(hourly, list) or not hourly:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
daily_highs: Dict[str, float] = {}
|
samples: List[Tuple[str, float]] = []
|
||||||
|
parsed_datetimes: List[datetime] = []
|
||||||
local_tz = timezone(timedelta(seconds=int(fallback_utc_offset)))
|
local_tz = timezone(timedelta(seconds=int(fallback_utc_offset)))
|
||||||
for row in hourly:
|
for row in hourly:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
@@ -119,6 +120,9 @@ def _derive_mgm_daily_highs_from_hourly(
|
|||||||
dt = datetime.fromisoformat(raw_time.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(raw_time.replace("Z", "+00:00"))
|
||||||
if dt.tzinfo is not None:
|
if dt.tzinfo is not None:
|
||||||
dt = dt.astimezone(local_tz)
|
dt = dt.astimezone(local_tz)
|
||||||
|
else:
|
||||||
|
dt = dt.replace(tzinfo=local_tz)
|
||||||
|
parsed_datetimes.append(dt)
|
||||||
date_key = dt.strftime("%Y-%m-%d")
|
date_key = dt.strftime("%Y-%m-%d")
|
||||||
except Exception:
|
except Exception:
|
||||||
if len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-":
|
if len(raw_time) >= 10 and raw_time[4] == "-" and raw_time[7] == "-":
|
||||||
@@ -129,6 +133,24 @@ def _derive_mgm_daily_highs_from_hourly(
|
|||||||
if not date_key:
|
if not date_key:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
samples.append((date_key, temp))
|
||||||
|
|
||||||
|
if not samples:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Guardrail: do not derive "daily highs" from short intraday snippets.
|
||||||
|
if parsed_datetimes:
|
||||||
|
parsed_datetimes.sort()
|
||||||
|
horizon_hours = (
|
||||||
|
parsed_datetimes[-1] - parsed_datetimes[0]
|
||||||
|
).total_seconds() / 3600.0
|
||||||
|
if horizon_hours < 30:
|
||||||
|
return {}
|
||||||
|
elif len(samples) < 24:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
daily_highs: Dict[str, float] = {}
|
||||||
|
for date_key, temp in samples:
|
||||||
prev = daily_highs.get(date_key)
|
prev = daily_highs.get(date_key)
|
||||||
daily_highs[date_key] = temp if prev is None else max(prev, temp)
|
daily_highs[date_key] = temp if prev is None else max(prev, temp)
|
||||||
|
|
||||||
@@ -163,7 +185,7 @@ def _append_future_forecast_lines(
|
|||||||
if mgm_value is not None:
|
if mgm_value is not None:
|
||||||
mgm_display = f"{float(mgm_value):.1f}"
|
mgm_display = f"{float(mgm_value):.1f}"
|
||||||
future_forecasts.append(
|
future_forecasts.append(
|
||||||
f"{d[5:]}: {t}{temp_symbol} | 🇺🇸 <b>MGM: {mgm_display}{temp_symbol}</b>"
|
f"{d[5:]}: {t}{temp_symbol} | <b>MGM: {mgm_display}{temp_symbol}</b>"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
|
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
|
||||||
|
|||||||
@@ -768,22 +768,48 @@ class WeatherDataCollector:
|
|||||||
|
|
||||||
# 5. Fallback for daily_forecasts from hourly data
|
# 5. Fallback for daily_forecasts from hourly data
|
||||||
if not results.get("daily_forecasts") and results.get("hourly"):
|
if not results.get("daily_forecasts") and results.get("hourly"):
|
||||||
from collections import defaultdict
|
# Guardrail: avoid treating short intraday snippets as full-day highs.
|
||||||
daily_max = defaultdict(list)
|
hourly_rows = results.get("hourly") or []
|
||||||
for h in results["hourly"]:
|
parsed_times = []
|
||||||
t = h.get("time", "")
|
for h in hourly_rows:
|
||||||
temp = h.get("temp")
|
t = str(h.get("time") or "")
|
||||||
if t and temp is not None:
|
if "T" not in t:
|
||||||
# Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z"
|
continue
|
||||||
date_str = t[:10]
|
try:
|
||||||
daily_max[date_str].append(temp)
|
parsed_times.append(datetime.fromisoformat(t.replace("Z", "+00:00")))
|
||||||
if daily_max:
|
except Exception:
|
||||||
results["daily_forecasts"] = {}
|
continue
|
||||||
for d, temps in sorted(daily_max.items()):
|
|
||||||
results["daily_forecasts"][d] = max(temps)
|
horizon_hours = 0.0
|
||||||
|
if len(parsed_times) >= 2:
|
||||||
|
parsed_times.sort()
|
||||||
|
horizon_hours = (
|
||||||
|
parsed_times[-1] - parsed_times[0]
|
||||||
|
).total_seconds() / 3600.0
|
||||||
|
|
||||||
|
if len(hourly_rows) >= 24 or horizon_hours >= 30:
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
daily_max = defaultdict(list)
|
||||||
|
for h in hourly_rows:
|
||||||
|
t = h.get("time", "")
|
||||||
|
temp = h.get("temp")
|
||||||
|
if t and temp is not None:
|
||||||
|
# Extract date from ISO timestamp like "2026-03-05T12:00:00.000Z"
|
||||||
|
date_str = t[:10]
|
||||||
|
daily_max[date_str].append(temp)
|
||||||
|
if daily_max:
|
||||||
|
results["daily_forecasts"] = {}
|
||||||
|
for d, temps in sorted(daily_max.items()):
|
||||||
|
results["daily_forecasts"][d] = max(temps)
|
||||||
|
logger.info(
|
||||||
|
f"📋 MGM daily_forecasts (from hourly fallback): "
|
||||||
|
f"{dict(results['daily_forecasts'])}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"📋 MGM daily_forecasts (from hourly fallback): "
|
"📋 Skip MGM daily_forecasts hourly fallback: "
|
||||||
f"{dict(results['daily_forecasts'])}"
|
f"hourly points={len(hourly_rows)}, horizon={horizon_hours:.1f}h"
|
||||||
)
|
)
|
||||||
|
|
||||||
return results if "current" in results else None
|
return results if "current" in results else None
|
||||||
|
|||||||
Reference in New Issue
Block a user