#!/usr/bin/env python3 """Verify Open-Meteo is the global MVP weather source for source-backed tiles.""" from __future__ import annotations import json import os import urllib.parse import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] PROVIDER_CONTRACT = ROOT / "Data" / "Weather" / "open_meteo_mvp_source.json" TILE_MANIFEST = ROOT / "Data" / "Tiles" / "tile_weather_manifest.json" WEATHER_SUBSYSTEM_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp" WEATHER_SUBSYSTEM_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.h" ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" EXPECTED_SNIPPETS = { WEATHER_SUBSYSTEM_H: [ "OpenMeteoForecastEndpoint", "https://api.open-meteo.com/v1/forecast", "RequestWeatherForTile", ], WEATHER_SUBSYSTEM_CPP: [ "current=temperature_2m,relative_humidity_2m,precipitation", "daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum,wind_speed_10m_max", "OutSnapshot.Provider = TEXT(\"open-meteo\");", ], ROADMAP: [ "[x] Use Open-Meteo as the first global MVP weather source.", ], TDD: [ "Open-Meteo is the first global MVP weather source", "Data/Weather/open_meteo_mvp_source.json", ], } def assert_static_contract() -> tuple[dict, dict]: missing = [] for path, snippets in EXPECTED_SNIPPETS.items(): text = path.read_text(encoding="utf-8") for snippet in snippets: if snippet not in text: missing.append(f"{path.relative_to(ROOT)}: {snippet}") if missing: raise RuntimeError("Open-Meteo MVP source verification failed: " + "; ".join(missing)) provider = json.loads(PROVIDER_CONTRACT.read_text(encoding="utf-8")) manifest = json.loads(TILE_MANIFEST.read_text(encoding="utf-8")) if provider.get("provider_id") != "open-meteo": raise RuntimeError("Provider contract does not identify Open-Meteo") if provider.get("api_key_required") is not False: raise RuntimeError("Open-Meteo MVP source should not require an API key") if not manifest.get("tiles"): raise RuntimeError("Tile weather manifest has no source-backed weather tiles") for tile in manifest["tiles"]: if tile.get("provider") != "open-meteo": raise RuntimeError(f"Unexpected tile weather provider for {tile.get('tile_id')}: {tile.get('provider')}") return provider, manifest def build_open_meteo_url(provider: dict, tile: dict) -> str: params = provider["request_parameters"] query = { "latitude": f"{float(tile['center_latitude']):.6f}", "longitude": f"{float(tile['center_longitude']):.6f}", "current": ",".join(params["current"]), "daily": ",".join(params["daily"]), "forecast_days": str(params["forecast_days"]), "timezone": params["timezone"], } return provider["forecast_endpoint"] + "?" + urllib.parse.urlencode(query, safe=",") def assert_live_open_meteo_response(provider: dict, manifest: dict) -> None: if os.environ.get("AGRARIAN_SKIP_LIVE_WEATHER_CHECK") == "1": print("Skipping live Open-Meteo check because AGRARIAN_SKIP_LIVE_WEATHER_CHECK=1") return for tile in manifest["tiles"]: url = build_open_meteo_url(provider, tile) with urllib.request.urlopen(url, timeout=20) as response: payload = json.loads(response.read().decode("utf-8")) current = payload.get("current", {}) daily = payload.get("daily", {}) required_current = {"temperature_2m", "relative_humidity_2m", "precipitation", "weather_code", "wind_speed_10m"} required_daily = {"temperature_2m_max", "temperature_2m_min", "weather_code", "precipitation_sum"} missing_current = sorted(required_current - set(current)) missing_daily = sorted(required_daily - set(daily)) if missing_current or missing_daily: raise RuntimeError( f"Open-Meteo response for {tile['tile_id']} missing current={missing_current} daily={missing_daily}" ) if not isinstance(daily["temperature_2m_max"], list) or not daily["temperature_2m_max"]: raise RuntimeError(f"Open-Meteo daily max temperature is empty for {tile['tile_id']}") def main() -> None: provider, manifest = assert_static_contract() assert_live_open_meteo_response(provider, manifest) print("Open-Meteo MVP weather source verification complete.") if __name__ == "__main__": main()