From 6a5d938976f61088334f09fee5df7297359af578 Mon Sep 17 00:00:00 2001 From: ledger Date: Mon, 2 Nov 2026 00:06:28 +0000 Subject: [PATCH] ledger: openmeteo per-city summary extract (2026-11-02) --- .../ledger/2026-11-02-openmeteo-summary.csv | 7 ++ .../verify_2026_11_02_openmeteo_summary.py | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 wizard-work/ledger/2026-11-02-openmeteo-summary.csv create mode 100644 wizard-work/ledger/verify_2026_11_02_openmeteo_summary.py diff --git a/wizard-work/ledger/2026-11-02-openmeteo-summary.csv b/wizard-work/ledger/2026-11-02-openmeteo-summary.csv new file mode 100644 index 0000000..7ced6fb --- /dev/null +++ b/wizard-work/ledger/2026-11-02-openmeteo-summary.csv @@ -0,0 +1,7 @@ +city,country,temperature_2m_C,apparent_temperature_C,apparent_minus_actual_C,relative_humidity_pct,weather_code,condition,wind_speed_10m_kmh +Cape Town,South Africa,16.1,14.1,-2.0,85,1,Mainly clear,21.8 +London,United Kingdom,8.9,6.9,-2.0,84,2,Partly cloudy,7.9 +New York,United States,12.7,9.9,-2.8,58,0,Clear sky,10.5 +Sydney,Australia,20.2,21.0,0.8,74,1,Mainly clear,6.6 +São Paulo,Brazil,21.7,24.1,2.4,87,3,Overcast,7.0 +Tokyo,Japan,11.9,11.7,-0.2,91,3,Overcast,2.3 diff --git a/wizard-work/ledger/verify_2026_11_02_openmeteo_summary.py b/wizard-work/ledger/verify_2026_11_02_openmeteo_summary.py new file mode 100644 index 0000000..12c254f --- /dev/null +++ b/wizard-work/ledger/verify_2026_11_02_openmeteo_summary.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Verify wizard-work/ledger/2026-11-02-openmeteo-summary.csv against the +source JSON it was derived from. Exit code is the verdict. + +EXIT CODES + 0 committed CSV matches the source-derived table (header + all rows) + 1 mismatch (header, row count, or per-cell value) + 2 ungradable (source JSON or CSV unreadable/malformed) +""" +import json +import sys + +HERE = '/root/troupe-standup/wizard-work/ledger' +SRC = HERE + '/openmeteo-source-2026-11-01.json' +CSV = HERE + '/2026-11-02-openmeteo-summary.csv' +EXPECTED_HEADER = ['city', 'country', 'temperature_2m_C', 'apparent_temperature_C', + 'apparent_minus_actual_C', 'relative_humidity_pct', 'weather_code', + 'condition', 'wind_speed_10m_kmh'] + + +def recompute(): + d = json.load(open(SRC)) + rows = [] + for c in sorted(d): + g = d[c]['geocode'] + w = d[c]['weather'] + rows.append({ + 'city': c, + 'country': g['country'], + 'temperature_2m_C': w['temperature_2m'], + 'apparent_temperature_C': w['apparent_temperature'], + 'apparent_minus_actual_C': round(w['apparent_temperature'] - w['temperature_2m'], 1), + 'relative_humidity_pct': w['relative_humidity_2m'], + 'weather_code': w['weather_code'], + 'condition': w['weather_desc'], + 'wind_speed_10m_kmh': w['wind_speed_10m'], + }) + return rows + + +def close(a, b, tol=1e-9): + try: + return abs(float(a) - float(b)) <= tol + except (TypeError, ValueError): + return a == b + + +def main(): + try: + expected = recompute() + except Exception as e: + print('UNGRADABLE: cannot recompute from source: ' + str(e)) + return 2 + try: + with open(CSV) as f: + lines = [ln.rstrip('\n') for ln in f] + except Exception as e: + print('UNGRADABLE: cannot read CSV: ' + str(e)) + return 2 + if not lines: + print('FAIL: CSV is empty') + return 1 + header = lines[0].split(',') + if header != EXPECTED_HEADER: + print('FAIL: header mismatch: ' + repr(header)) + return 1 + body = [ln.split(',') for ln in lines[1:]] + if len(body) != len(expected): + print('FAIL: row count %d != expected %d' % (len(body), len(expected))) + return 1 + ok = True + for i, (act, exp) in enumerate(zip(body, expected)): + for col in EXPECTED_HEADER: + if not close(act[EXPECTED_HEADER.index(col)], exp[col]): + print('FAIL row %d col %s: %r != %r' % (i, col, act[EXPECTED_HEADER.index(col)], exp[col])) + ok = False + if ok: + print('PASS: %d rows match source-derived table; header exact' % len(body)) + return 0 + return 1 + + +if __name__ == '__main__': + sys.exit(main())