#!/usr/bin/env python3 """Verify NOAA/NWS fallback wiring for US source-backed tiles.""" from __future__ import annotations import json import os import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CONTRACT = ROOT / "Data" / "Weather" / "noaa_nws_us_fallback.json" MANIFEST = ROOT / "Data" / "Tiles" / "tile_weather_manifest.json" HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.h" CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp" ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" EXPECTED = { HEADER: [ "bEnableNoaaNwsFallbackForUSTiles", "NoaaNwsPointsEndpoint", "https://api.weather.gov/points", "RequestNoaaNwsFallbackForTile", "IsNoaaNwsEligibleCoordinate", "BuildNoaaNwsPointsUrl", ], CPP: [ "NoaaNwsPointsEndpoint", "forecastGridData", "ParseNoaaNwsGridData", "probabilityOfPrecipitation", "OutSnapshot.Provider = TEXT(\"noaa-nws\");", ], ROADMAP: ["[x] Add NOAA/NWS fallback or enrichment for US tiles where useful."], TDD: ["NOAA/NWS is the US-only fallback and enrichment path", "Data/Weather/noaa_nws_us_fallback.json"], } def assert_static_contract() -> tuple[dict, dict]: missing = [] for path, snippets in EXPECTED.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("NOAA/NWS fallback verification failed: " + "; ".join(missing)) contract = json.loads(CONTRACT.read_text(encoding="utf-8")) manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) if contract.get("provider_id") != "noaa-nws": raise RuntimeError("NOAA/NWS contract has wrong provider_id") if contract.get("api_key_required") is not False: raise RuntimeError("NOAA/NWS fallback should not require an API key") return contract, manifest def is_us_eligible(tile: dict) -> bool: lat = float(tile["center_latitude"]) lon = float(tile["center_longitude"]) return 18.0 <= lat <= 72.0 and -180.0 <= lon <= -64.0 def assert_live_nws_points(contract: dict, manifest: dict) -> None: if os.environ.get("AGRARIAN_SKIP_LIVE_WEATHER_CHECK") == "1": print("Skipping live NOAA/NWS check because AGRARIAN_SKIP_LIVE_WEATHER_CHECK=1") return eligible_tiles = [tile for tile in manifest.get("tiles", []) if is_us_eligible(tile)] if not eligible_tiles: return for tile in eligible_tiles: url = f"{contract['points_endpoint']}/{float(tile['center_latitude']):.4f},{float(tile['center_longitude']):.4f}" request = urllib.request.Request( url, headers={ "Accept": "application/geo+json", "User-Agent": "AgrarianGameMVP/0.1", }, ) with urllib.request.urlopen(request, timeout=20) as response: payload = json.loads(response.read().decode("utf-8")) properties = payload.get("properties", {}) if not properties.get("forecastGridData"): raise RuntimeError(f"NOAA/NWS points response missing forecastGridData for {tile['tile_id']}") def main() -> None: contract, manifest = assert_static_contract() assert_live_nws_points(contract, manifest) print("NOAA/NWS US fallback verification complete.") if __name__ == "__main__": main()