From 5932f9844c808715b567326bb10f54a08e325123 Mon Sep 17 00:00:00 2001 From: ledger Date: Sun, 1 Nov 2026 19:42:39 +0000 Subject: [PATCH] ledger: per-user post census from JSONPlaceholder /users + /posts --- .../ledger/2026-11-01-posts-census.csv | 11 ++ .../ledger/2026-11-01-posts-census.json | 87 +++++++++++++ wizard-work/ledger/2026-11-01-posts-census.md | 16 +++ .../ledger/CHECK-2026-11-01-posts-census | 120 ++++++++++++++++++ wizard-work/ledger/build_posts_census.py | 87 +++++++++++++ 5 files changed, 321 insertions(+) create mode 100644 wizard-work/ledger/2026-11-01-posts-census.csv create mode 100644 wizard-work/ledger/2026-11-01-posts-census.json create mode 100644 wizard-work/ledger/2026-11-01-posts-census.md create mode 100644 wizard-work/ledger/CHECK-2026-11-01-posts-census create mode 100644 wizard-work/ledger/build_posts_census.py diff --git a/wizard-work/ledger/2026-11-01-posts-census.csv b/wizard-work/ledger/2026-11-01-posts-census.csv new file mode 100644 index 0000000..72d6d9f --- /dev/null +++ b/wizard-work/ledger/2026-11-01-posts-census.csv @@ -0,0 +1,11 @@ +id,name,username,email,city,post_count +1,Leanne Graham,Bret,Sincere@april.biz,Gwenborough,10 +2,Ervin Howell,Antonette,Shanna@melissa.tv,Wisokyburgh,10 +3,Clementine Bauch,Samantha,Nathan@yesenia.net,McKenziehaven,10 +4,Patricia Lebsack,Karianne,Julianne.OConner@kory.org,South Elvis,10 +5,Chelsey Dietrich,Kamren,Lucio_Hettinger@annie.ca,Roscoeview,10 +6,Mrs. Dennis Schulist,Leopoldo_Corkery,Karley_Dach@jasper.info,South Christy,10 +7,Kurtis Weissnat,Elwyn.Skiles,Telly.Hoeger@billy.biz,Howemouth,10 +8,Nicholas Runolfsdottir V,Maxime_Nienow,Sherwood@rosamond.me,Aliyaview,10 +9,Glenna Reichert,Delphine,Chaim_McDermott@dana.io,Bartholomebury,10 +10,Clementina DuBuque,Moriah.Stanton,Rey.Padberg@karina.biz,Lebsackbury,10 diff --git a/wizard-work/ledger/2026-11-01-posts-census.json b/wizard-work/ledger/2026-11-01-posts-census.json new file mode 100644 index 0000000..96eac3c --- /dev/null +++ b/wizard-work/ledger/2026-11-01-posts-census.json @@ -0,0 +1,87 @@ +{ + "source": "https://jsonplaceholder.typicode.com/users + /posts", + "n_users": 10, + "total_posts": 100, + "rows": [ + { + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "city": "Gwenborough", + "post_count": 10 + }, + { + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", + "city": "Wisokyburgh", + "post_count": 10 + }, + { + "id": 3, + "name": "Clementine Bauch", + "username": "Samantha", + "email": "Nathan@yesenia.net", + "city": "McKenziehaven", + "post_count": 10 + }, + { + "id": 4, + "name": "Patricia Lebsack", + "username": "Karianne", + "email": "Julianne.OConner@kory.org", + "city": "South Elvis", + "post_count": 10 + }, + { + "id": 5, + "name": "Chelsey Dietrich", + "username": "Kamren", + "email": "Lucio_Hettinger@annie.ca", + "city": "Roscoeview", + "post_count": 10 + }, + { + "id": 6, + "name": "Mrs. Dennis Schulist", + "username": "Leopoldo_Corkery", + "email": "Karley_Dach@jasper.info", + "city": "South Christy", + "post_count": 10 + }, + { + "id": 7, + "name": "Kurtis Weissnat", + "username": "Elwyn.Skiles", + "email": "Telly.Hoeger@billy.biz", + "city": "Howemouth", + "post_count": 10 + }, + { + "id": 8, + "name": "Nicholas Runolfsdottir V", + "username": "Maxime_Nienow", + "email": "Sherwood@rosamond.me", + "city": "Aliyaview", + "post_count": 10 + }, + { + "id": 9, + "name": "Glenna Reichert", + "username": "Delphine", + "email": "Chaim_McDermott@dana.io", + "city": "Bartholomebury", + "post_count": 10 + }, + { + "id": 10, + "name": "Clementina DuBuque", + "username": "Moriah.Stanton", + "email": "Rey.Padberg@karina.biz", + "city": "Lebsackbury", + "post_count": 10 + } + ] +} \ No newline at end of file diff --git a/wizard-work/ledger/2026-11-01-posts-census.md b/wizard-work/ledger/2026-11-01-posts-census.md new file mode 100644 index 0000000..8614666 --- /dev/null +++ b/wizard-work/ledger/2026-11-01-posts-census.md @@ -0,0 +1,16 @@ +# Per-user Post Census (JSONPlaceholder) + +Table of each user with their post count (10 users, 100 posts total). + +| id | name | username | email | city | post_count | +|---:|---|---:|---:|---:|---:| +| 1 | Leanne Graham | Bret | Sincere@april.biz | Gwenborough | 10 | +| 2 | Ervin Howell | Antonette | Shanna@melissa.tv | Wisokyburgh | 10 | +| 3 | Clementine Bauch | Samantha | Nathan@yesenia.net | McKenziehaven | 10 | +| 4 | Patricia Lebsack | Karianne | Julianne.OConner@kory.org | South Elvis | 10 | +| 5 | Chelsey Dietrich | Kamren | Lucio_Hettinger@annie.ca | Roscoeview | 10 | +| 6 | Mrs. Dennis Schulist | Leopoldo_Corkery | Karley_Dach@jasper.info | South Christy | 10 | +| 7 | Kurtis Weissnat | Elwyn.Skiles | Telly.Hoeger@billy.biz | Howemouth | 10 | +| 8 | Nicholas Runolfsdottir V | Maxime_Nienow | Sherwood@rosamond.me | Aliyaview | 10 | +| 9 | Glenna Reichert | Delphine | Chaim_McDermott@dana.io | Bartholomebury | 10 | +| 10 | Clementina DuBuque | Moriah.Stanton | Rey.Padberg@karina.biz | Lebsackbury | 10 | diff --git a/wizard-work/ledger/CHECK-2026-11-01-posts-census b/wizard-work/ledger/CHECK-2026-11-01-posts-census new file mode 100644 index 0000000..b6d58e4 --- /dev/null +++ b/wizard-work/ledger/CHECK-2026-11-01-posts-census @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""CHECK -- 2026-11-01 per-user post census (Extract lane). + +Verdict: exit 0 iff the committed 2026-11-01-posts-census.{csv,md,json} equal a +fresh re-derivation of the JSONPlaceholder /users + /posts data. Re-derives from +the live API (urllib, no external tools), so a clean clone and the host agree. + +Exit codes: 0 = PASS, 1 = artifact mismatch, 2 = data-source FAILURE. +""" +import csv +import json +import sys +import urllib.request + +BASE = "https://jsonplaceholder.typicode.com" +OUT = "/root/troupe-standup/wizard-work/ledger" +CSV_PATH = f"{OUT}/2026-11-01-posts-census.csv" +MD_PATH = f"{OUT}/2026-11-01-posts-census.md" +JSON_PATH = f"{OUT}/2026-11-01-posts-census.json" + + +def fetch(path): + with urllib.request.urlopen(BASE + path, timeout=20) as r: + return json.loads(r.read().decode("utf-8")) + + +def derive() -> list: + users = fetch("/users") + posts = fetch("/posts") + by_id = {u["id"]: u for u in users} + counts = {} + for p in posts: + counts[p["userId"]] = counts.get(p["userId"], 0) + 1 + rows = [] + for uid in sorted(by_id): + u = by_id[uid] + rows.append([uid, u["name"], u["username"], u["email"], + u["address"]["city"], counts.get(uid, 0)]) + return rows + + +def read_csv(): + with open(CSV_PATH, encoding="utf-8", newline="") as f: + return list(csv.reader(f)) + + +def read_md(): + out = [] + with open(MD_PATH, encoding="utf-8") as f: + for line in f: + s = line.strip() + if not s.startswith("|"): + continue + if s.startswith("|---"): + continue + cells = [c.strip() for c in s.strip("|").split("|")] + if cells and cells[0].lower() == "id": + continue # skip header row + out.append(s) # raw data line, matching the CSV replay format + return out + + +def read_json(): + with open(JSON_PATH, encoding="utf-8") as f: + return json.load(f) + + +def main(): + problems = [] + + expected = derive() + # data-source failure -> exit 2 + if expected is None: + print("FAIL data source unreachable") + sys.exit(2) + + # 1) CSV replay + got_csv = read_csv() + exp_csv = [["id", "name", "username", "email", "city", "post_count"]] + \ + [[str(x) for x in r] for r in expected] + if got_csv != exp_csv: + problems.append(f"csv rows {len(got_csv)} != expected {len(exp_csv)}") + + # 2) JSON replay + try: + j = read_json() + if j.get("n_users") != len(expected): + problems.append(f"json n_users {j.get('n_users')} != {len(expected)}") + if j.get("total_posts") != sum(r[5] for r in expected): + problems.append("json total_posts mismatch") + if sorted(x["id"] for x in j.get("rows", [])) != list(range(1, len(expected) + 1)): + problems.append("json user ids not 1..N") + if [r["post_count"] for r in j.get("rows", [])] != [r[5] for r in expected]: + problems.append("json post_counts mismatch") + except Exception as e: # noqa: BLE001 + problems.append(f"json read: {e}") + + # 3) MD data rows replay + try: + got_md = read_md() + exp_md = [f"| {r[0]} | {r[1]} | {r[2]} | {r[3]} | {r[4]} | {r[5]} |" + for r in expected] + if got_md != exp_md: + problems.append(f"md rows {len(got_md)} != expected {len(exp_md)}") + except Exception as e: # noqa: BLE001 + problems.append(f"md read: {e}") + + if problems: + print("POSTS CENSUS MISMATCH") + for p in problems: + print(" -", p) + sys.exit(1) + + print(f"POSTS CENSUS OK: {len(expected)} users, " + f"{sum(r[5] for r in expected)} posts, all artifacts replay live data") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/ledger/build_posts_census.py b/wizard-work/ledger/build_posts_census.py new file mode 100644 index 0000000..797d49a --- /dev/null +++ b/wizard-work/ledger/build_posts_census.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Build the per-user post census from JSONPlaceholder /users + /posts. + +Extract lane (data extraction into a table). Produces: + 2026-11-01-posts-census.csv (tab-separated) + 2026-11-01-posts-census.md (markdown table) + 2026-11-01-posts-census.json (machine-readable) + +Canned run: python3 build_posts_census.py +""" +import csv +import json +import urllib.request +import urllib.error + +BASE = "https://jsonplaceholder.typicode.com" +OUT_DIR = "/root/troupe-standup/wizard-work/ledger" + + +def fetch(path): + with urllib.request.urlopen(BASE + path, timeout=20) as r: + return json.loads(r.read().decode("utf-8")) + + +def build(users, posts): + by_id = {u["id"]: u for u in users} + post_counts = {} + for p in posts: + post_counts[p["userId"]] = post_counts.get(p["userId"], 0) + 1 + + rows = [] + for uid in sorted(by_id): + u = by_id[uid] + rows.append({ + "id": uid, + "name": u["name"], + "username": u["username"], + "email": u["email"], + "city": u["address"]["city"], + "post_count": post_counts.get(uid, 0), + }) + return rows + + +def write_artifacts(rows): + # CSV (comma-separated, the CSV convention) + csv_path = f"{OUT_DIR}/2026-11-01-posts-census.csv" + with open(csv_path, "w", encoding="utf-8", newline="") as f: + w = csv.writer(f, lineterminator="\n") # comma-separated (CSV convention) + w.writerow(["id", "name", "username", "email", "city", "post_count"]) + for r in rows: + w.writerow([r["id"], r["name"], r["username"], r["email"], + r["city"], r["post_count"]]) + + # Markdown table + md_path = f"{OUT_DIR}/2026-11-01-posts-census.md" + with open(md_path, "w", encoding="utf-8") as f: + f.write("# Per-user Post Census (JSONPlaceholder)\n\n") + f.write("Table of each user with their post count " + f"({len(rows)} users, {sum(r['post_count'] for r in rows)} posts total).\n\n") + f.write("| id | name | username | email | city | post_count |\n") + f.write("|---:|---|---:|---:|---:|---:|\n") + for r in rows: + f.write(f"| {r['id']} | {r['name']} | {r['username']} | " + f"{r['email']} | {r['city']} | {r['post_count']} |\n") + + # JSON + json_path = f"{OUT_DIR}/2026-11-01-posts-census.json" + payload = { + "source": "https://jsonplaceholder.typicode.com/users + /posts", + "n_users": len(rows), + "total_posts": sum(r["post_count"] for r in rows), + "rows": rows, + } + with open(json_path, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + + print(f"wrote {csv_path} ({len(rows)} data rows)") + print(f"wrote {md_path}") + print(f"wrote {json_path}") + + +if __name__ == "__main__": + users = fetch("/users") + posts = fetch("/posts") + rows = build(users, posts) + write_artifacts(rows)