ledger: extract committed-file inventory table
This commit is contained in:
1 parent
01aac38c5a
commit
e98c6bd260
6 files changed
+4545
No files matched your search
@@ -0,0 +1,11 @@
|
||||
city,country,lat,lon,current_time,temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,weather_desc,wind_speed_10m,today_max,today_min,today_precip,today_sunrise,today_sunset
|
||||
Berlin,Germany,52.52437,13.41053,2026-11-02T08:00,7.3,5.6,87,2,Partly cloudy,3.3,9.3,6.2,0.3,2026-11-02T07:04,2026-11-02T16:34
|
||||
Mumbai,India,19.07283,72.88261,2026-11-02T12:30,34.1,38.0,41,1,Mainly clear,1.4,34.3,27.4,0.0,2026-11-02T06:39,2026-11-02T18:04
|
||||
Dubai,United Arab Emirates,25.07725,55.30927,2026-11-02T11:00,33.2,35.5,34,0,Clear sky,4.8,35.2,24.0,0.0,2026-11-02T06:26,2026-11-02T17:37
|
||||
Toronto,Canada,43.70643,-79.39864,2026-11-02T02:00,7.1,4.1,77,0,Clear sky,9.9,16.9,6.3,0.0,2026-11-02T06:54,2026-11-02T17:06
|
||||
Buenos Aires,Argentina,-34.61315,-58.37723,2026-11-02T04:00,16.6,17.9,91,0,Clear sky,2.6,24.7,15.5,0.1,2026-11-02T05:50,2026-11-02T19:24
|
||||
Jakarta,Indonesia,-6.21462,106.84513,2026-11-02T14:00,31.7,36.8,60,51,Light drizzle,7.0,33.9,25.2,2.5,2026-11-02T05:26,2026-11-02T17:46
|
||||
Reykjavik,Iceland,64.13548,-21.89541,2026-11-02T07:00,9.3,5.3,86,3,Overcast,22.3,9.8,9.1,1.2,2026-11-02T09:13,2026-11-02T17:07
|
||||
Lagos,Nigeria,6.45407,3.39467,2026-11-02T08:00,28.2,33.7,79,1,Mainly clear,5.2,31.2,27.4,1.0,2026-11-02T06:33,2026-11-02T18:26
|
||||
Seoul,South Korea,37.566,126.9784,2026-11-02T16:00,13.3,10.4,49,0,Clear sky,9.1,13.6,6.9,0.0,2026-11-02T06:57,2026-11-02T17:33
|
||||
Auckland,New Zealand,-36.84853,174.76349,2026-11-02T20:00,17.0,14.9,68,3,Overcast,16.5,18.1,14.8,0.0,2026-11-02T06:15,2026-11-02T19:54
|
||||
|
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"Berlin": {
|
||||
"geocode": {
|
||||
"lat": 52.52437,
|
||||
"lon": 13.41053,
|
||||
"country": "Germany"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T08:00",
|
||||
"temperature_2m": 7.3,
|
||||
"apparent_temperature": 5.6,
|
||||
"relative_humidity_2m": 87,
|
||||
"weather_code": 2,
|
||||
"weather_desc": "Partly cloudy",
|
||||
"wind_speed_10m": 3.3,
|
||||
"today_max": 9.3,
|
||||
"today_min": 6.2,
|
||||
"today_precip": 0.3,
|
||||
"today_sunrise": "2026-11-02T07:04",
|
||||
"today_sunset": "2026-11-02T16:34"
|
||||
}
|
||||
},
|
||||
"Mumbai": {
|
||||
"geocode": {
|
||||
"lat": 19.07283,
|
||||
"lon": 72.88261,
|
||||
"country": "India"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T12:30",
|
||||
"temperature_2m": 34.1,
|
||||
"apparent_temperature": 38.0,
|
||||
"relative_humidity_2m": 41,
|
||||
"weather_code": 1,
|
||||
"weather_desc": "Mainly clear",
|
||||
"wind_speed_10m": 1.4,
|
||||
"today_max": 34.3,
|
||||
"today_min": 27.4,
|
||||
"today_precip": 0.0,
|
||||
"today_sunrise": "2026-11-02T06:39",
|
||||
"today_sunset": "2026-11-02T18:04"
|
||||
}
|
||||
},
|
||||
"Dubai": {
|
||||
"geocode": {
|
||||
"lat": 25.07725,
|
||||
"lon": 55.30927,
|
||||
"country": "United Arab Emirates"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T11:00",
|
||||
"temperature_2m": 33.2,
|
||||
"apparent_temperature": 35.5,
|
||||
"relative_humidity_2m": 34,
|
||||
"weather_code": 0,
|
||||
"weather_desc": "Clear sky",
|
||||
"wind_speed_10m": 4.8,
|
||||
"today_max": 35.2,
|
||||
"today_min": 24.0,
|
||||
"today_precip": 0.0,
|
||||
"today_sunrise": "2026-11-02T06:26",
|
||||
"today_sunset": "2026-11-02T17:37"
|
||||
}
|
||||
},
|
||||
"Toronto": {
|
||||
"geocode": {
|
||||
"lat": 43.70643,
|
||||
"lon": -79.39864,
|
||||
"country": "Canada"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T02:00",
|
||||
"temperature_2m": 7.1,
|
||||
"apparent_temperature": 4.1,
|
||||
"relative_humidity_2m": 77,
|
||||
"weather_code": 0,
|
||||
"weather_desc": "Clear sky",
|
||||
"wind_speed_10m": 9.9,
|
||||
"today_max": 16.9,
|
||||
"today_min": 6.3,
|
||||
"today_precip": 0.0,
|
||||
"today_sunrise": "2026-11-02T06:54",
|
||||
"today_sunset": "2026-11-02T17:06"
|
||||
}
|
||||
},
|
||||
"Buenos Aires": {
|
||||
"geocode": {
|
||||
"lat": -34.61315,
|
||||
"lon": -58.37723,
|
||||
"country": "Argentina"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T04:00",
|
||||
"temperature_2m": 16.6,
|
||||
"apparent_temperature": 17.9,
|
||||
"relative_humidity_2m": 91,
|
||||
"weather_code": 0,
|
||||
"weather_desc": "Clear sky",
|
||||
"wind_speed_10m": 2.6,
|
||||
"today_max": 24.7,
|
||||
"today_min": 15.5,
|
||||
"today_precip": 0.1,
|
||||
"today_sunrise": "2026-11-02T05:50",
|
||||
"today_sunset": "2026-11-02T19:24"
|
||||
}
|
||||
},
|
||||
"Jakarta": {
|
||||
"geocode": {
|
||||
"lat": -6.21462,
|
||||
"lon": 106.84513,
|
||||
"country": "Indonesia"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T14:00",
|
||||
"temperature_2m": 31.7,
|
||||
"apparent_temperature": 36.8,
|
||||
"relative_humidity_2m": 60,
|
||||
"weather_code": 51,
|
||||
"weather_desc": "Light drizzle",
|
||||
"wind_speed_10m": 7.0,
|
||||
"today_max": 33.9,
|
||||
"today_min": 25.2,
|
||||
"today_precip": 2.5,
|
||||
"today_sunrise": "2026-11-02T05:26",
|
||||
"today_sunset": "2026-11-02T17:46"
|
||||
}
|
||||
},
|
||||
"Reykjavik": {
|
||||
"geocode": {
|
||||
"lat": 64.13548,
|
||||
"lon": -21.89541,
|
||||
"country": "Iceland"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T07:00",
|
||||
"temperature_2m": 9.3,
|
||||
"apparent_temperature": 5.3,
|
||||
"relative_humidity_2m": 86,
|
||||
"weather_code": 3,
|
||||
"weather_desc": "Overcast",
|
||||
"wind_speed_10m": 22.3,
|
||||
"today_max": 9.8,
|
||||
"today_min": 9.1,
|
||||
"today_precip": 1.2,
|
||||
"today_sunrise": "2026-11-02T09:13",
|
||||
"today_sunset": "2026-11-02T17:07"
|
||||
}
|
||||
},
|
||||
"Lagos": {
|
||||
"geocode": {
|
||||
"lat": 6.45407,
|
||||
"lon": 3.39467,
|
||||
"country": "Nigeria"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T08:00",
|
||||
"temperature_2m": 28.2,
|
||||
"apparent_temperature": 33.7,
|
||||
"relative_humidity_2m": 79,
|
||||
"weather_code": 1,
|
||||
"weather_desc": "Mainly clear",
|
||||
"wind_speed_10m": 5.2,
|
||||
"today_max": 31.2,
|
||||
"today_min": 27.4,
|
||||
"today_precip": 1.0,
|
||||
"today_sunrise": "2026-11-02T06:33",
|
||||
"today_sunset": "2026-11-02T18:26"
|
||||
}
|
||||
},
|
||||
"Seoul": {
|
||||
"geocode": {
|
||||
"lat": 37.566,
|
||||
"lon": 126.9784,
|
||||
"country": "South Korea"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T16:00",
|
||||
"temperature_2m": 13.3,
|
||||
"apparent_temperature": 10.4,
|
||||
"relative_humidity_2m": 49,
|
||||
"weather_code": 0,
|
||||
"weather_desc": "Clear sky",
|
||||
"wind_speed_10m": 9.1,
|
||||
"today_max": 13.6,
|
||||
"today_min": 6.9,
|
||||
"today_precip": 0.0,
|
||||
"today_sunrise": "2026-11-02T06:57",
|
||||
"today_sunset": "2026-11-02T17:33"
|
||||
}
|
||||
},
|
||||
"Auckland": {
|
||||
"geocode": {
|
||||
"lat": -36.84853,
|
||||
"lon": 174.76349,
|
||||
"country": "New Zealand"
|
||||
},
|
||||
"weather": {
|
||||
"current_time": "2026-11-02T20:00",
|
||||
"temperature_2m": 17.0,
|
||||
"apparent_temperature": 14.9,
|
||||
"relative_humidity_2m": 68,
|
||||
"weather_code": 3,
|
||||
"weather_desc": "Overcast",
|
||||
"wind_speed_10m": 16.5,
|
||||
"today_max": 18.1,
|
||||
"today_min": 14.8,
|
||||
"today_precip": 0.0,
|
||||
"today_sunrise": "2026-11-02T06:15",
|
||||
"today_sunset": "2026-11-02T19:54"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# CHECK — ledger: open-meteo cities extract (2026-11-02).
|
||||
#
|
||||
# Exits 0 iff the committed table (2026-11-02-openmeteo-cities.csv) is a
|
||||
# faithful re-derivation of the committed raw source snapshot
|
||||
# (2026-11-02-openmeteo-cities.json). The CSV's own generator is NOT imported;
|
||||
# the table is rebuilt cell-by-cell from the source JSON with plain python.
|
||||
# No `|| true` / `; exit 0` fallback: the last command's exit code is the verdict.
|
||||
set -u
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$DIR" || exit 2
|
||||
|
||||
python3 - <<'PY'
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
|
||||
src = json.load(open("2026-11-02-openmeteo-cities.json"))
|
||||
with open("2026-11-02-openmeteo-cities.csv", newline="", encoding="utf-8") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
|
||||
|
||||
def cell(value):
|
||||
# Numbers round-tripped by the generator: keep as str() so csv and json
|
||||
# representations must agree exactly.
|
||||
return str(value)
|
||||
|
||||
|
||||
expected = []
|
||||
for city, info in src.items():
|
||||
g = info["geocode"]
|
||||
w = info["weather"]
|
||||
expected.append({
|
||||
"city": city,
|
||||
"country": g["country"],
|
||||
"lat": cell(g["lat"]),
|
||||
"lon": cell(g["lon"]),
|
||||
"current_time": w["current_time"],
|
||||
"temperature_2m": cell(w["temperature_2m"]),
|
||||
"apparent_temperature": cell(w["apparent_temperature"]),
|
||||
"relative_humidity_2m": cell(w["relative_humidity_2m"]),
|
||||
"weather_code": cell(w["weather_code"]),
|
||||
"weather_desc": w["weather_desc"],
|
||||
"wind_speed_10m": cell(w["wind_speed_10m"]),
|
||||
"today_max": cell(w["today_max"]),
|
||||
"today_min": cell(w["today_min"]),
|
||||
"today_precip": cell(w["today_precip"]),
|
||||
"today_sunrise": w["today_sunrise"],
|
||||
"today_sunset": w["today_sunset"],
|
||||
})
|
||||
|
||||
|
||||
if len(rows) != len(expected):
|
||||
print("ROW COUNT MISMATCH csv=%d expected=%d" % (len(rows), len(expected)))
|
||||
sys.exit(1)
|
||||
|
||||
for r, e in zip(rows, expected):
|
||||
for key in e:
|
||||
if r.get(key) != e[key]:
|
||||
print("CELL MISMATCH row=%r column=%r csv=%r src=%r"
|
||||
% (r.get("city"), key, r.get(key), e[key]))
|
||||
sys.exit(1)
|
||||
|
||||
print("rows=%d cities=%s re-derived from source snapshot — PASS"
|
||||
% (len(rows), ", ".join(r["city"] for r in rows)))
|
||||
PY
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a current + daily weather table for a fixed city set (Extract lane).
|
||||
|
||||
Source: Open-Meteo free public API (no vendor, no key). Each city is resolved
|
||||
through the geocoding endpoint (``geocoding-api.open-meteo.com/v1/search``) to
|
||||
get its latitude/longitude/country, then the forecast endpoint
|
||||
(``api.open-meteo.com/v1/forecast``) returns current conditions plus today's
|
||||
max/min/precip sum/sunrise/sunset.
|
||||
|
||||
Artifacts written beside this script:
|
||||
- 2026-11-02-openmeteo-cities.csv extracted table (16 columns x 10 cities)
|
||||
- 2026-11-02-openmeteo-cities.json raw per-city source snapshot (the evidence)
|
||||
|
||||
Deterministic check: the committed CSV is re-derived cell-by-cell from the
|
||||
committed source JSON by ``CHECK-2026-11-02-openmeteo-cities``.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
CITIES = [
|
||||
"Berlin", "Mumbai", "Dubai", "Toronto", "Buenos Aires",
|
||||
"Jakarta", "Reykjavik", "Lagos", "Seoul", "Auckland",
|
||||
]
|
||||
FIELDS = [
|
||||
"city", "country", "lat", "lon", "current_time",
|
||||
"temperature_2m", "apparent_temperature", "relative_humidity_2m",
|
||||
"weather_code", "weather_desc", "wind_speed_10m",
|
||||
"today_max", "today_min", "today_precip",
|
||||
"today_sunrise", "today_sunset",
|
||||
]
|
||||
WMO = {
|
||||
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
|
||||
45: "Fog", 48: "Fog",
|
||||
51: "Light drizzle", 53: "Drizzle", 55: "Heavy drizzle",
|
||||
61: "Slight rain", 63: "Rain", 65: "Heavy rain",
|
||||
71: "Slight snow", 73: "Snow", 75: "Heavy snow",
|
||||
80: "Slight showers", 81: "Showers", 82: "Heavy showers",
|
||||
85: "Snow showers", 86: "Heavy snow showers",
|
||||
95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail",
|
||||
}
|
||||
|
||||
|
||||
def http_get(url, timeout=25):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "ledger-extract/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8")
|
||||
|
||||
|
||||
def geocode(name):
|
||||
url = ("https://geocoding-api.open-meteo.com/v1/search?name=" + urllib.parse.quote(name)
|
||||
+ "&count=1&language=en&format=json")
|
||||
data = json.loads(http_get(url))
|
||||
hits = data.get("results") or []
|
||||
if not hits:
|
||||
raise RuntimeError("no geocode hit for %r" % name)
|
||||
h = hits[0]
|
||||
return (h["latitude"], h["longitude"],
|
||||
h.get("country") or h.get("country_code") or "n/a")
|
||||
|
||||
|
||||
def fetch_weather(lat, lon):
|
||||
lat_s = "%.6f" % lat
|
||||
lon_s = "%.6f" % lon
|
||||
url = ("https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
|
||||
"¤t=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,wind_speed_10m"
|
||||
"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,sunrise,sunset"
|
||||
"&timezone=auto".format(lat=lat_s, lon=lon_s))
|
||||
data = json.loads(http_get(url))
|
||||
cur = data["current"]
|
||||
day = data["daily"]
|
||||
return {
|
||||
"current_time": cur["time"],
|
||||
"temperature_2m": cur["temperature_2m"],
|
||||
"apparent_temperature": cur["apparent_temperature"],
|
||||
"relative_humidity_2m": cur["relative_humidity_2m"],
|
||||
"weather_code": cur["weather_code"],
|
||||
"weather_desc": WMO.get(cur["weather_code"], "Unknown"),
|
||||
"wind_speed_10m": cur["wind_speed_10m"],
|
||||
"today_max": day["temperature_2m_max"][0],
|
||||
"today_min": day["temperature_2m_min"][0],
|
||||
"today_precip": day["precipitation_sum"][0],
|
||||
"today_sunrise": day["sunrise"][0],
|
||||
"today_sunset": day["sunset"][0],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
rows = []
|
||||
source = {}
|
||||
for city in CITIES:
|
||||
try:
|
||||
lat, lon, country = geocode(city)
|
||||
w = fetch_weather(lat, lon)
|
||||
rows.append({"city": city, "country": country, "lat": lat, "lon": lon, **w})
|
||||
source[city] = {"geocode": {"lat": lat, "lon": lon, "country": country},
|
||||
"weather": w}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
rows.append({"city": city, "country": "ERROR", "lat": "", "lon": "",
|
||||
"current_time": "", "temperature_2m": "", "apparent_temperature": "",
|
||||
"relative_humidity_2m": "", "weather_code": "", "weather_desc": "FAILED",
|
||||
"wind_speed_10m": "", "today_max": "", "today_min": "",
|
||||
"today_precip": "", "today_sunrise": "", "today_sunset": ""})
|
||||
source[city] = {"error": repr(exc)}
|
||||
|
||||
csv_path = HERE / "2026-11-02-openmeteo-cities.csv"
|
||||
with csv_path.open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=FIELDS, lineterminator="\n")
|
||||
writer.writeheader()
|
||||
for r in rows:
|
||||
writer.writerow(r)
|
||||
|
||||
(HERE / "2026-11-02-openmeteo-cities.json").write_text(
|
||||
json.dumps(source, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,12 @@
|
||||
import os, stat
|
||||
p = 'extraction_manifest.json'
|
||||
print('exists', os.path.exists(p))
|
||||
print('isfile', os.path.isfile(p))
|
||||
print('listdir', repr(os.listdir('.')))
|
||||
try:
|
||||
st = os.stat(p)
|
||||
print('mode', oct(stat.S_IFMT(st.st_mode)), oct(st.st_mode), 'size', st.st_size)
|
||||
print('inod', st.st_ino)
|
||||
print('contents_b200', repr(open(p, 'rb').read(200)))
|
||||
except Exception as e:
|
||||
print('stat_err', repr(e))
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user