ledger: per-user post census from JSONPlaceholder /users + /posts

This commit is contained in:
ledger committed 2026-11-01 19:42:39 +00:00
1 parent d99c079bf8
commit 5932f9844c
5 files changed
+321

No files matched your search

@@ -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
1 id name username email city post_count
2 1 Leanne Graham Bret Sincere@april.biz Gwenborough 10
3 2 Ervin Howell Antonette Shanna@melissa.tv Wisokyburgh 10
4 3 Clementine Bauch Samantha Nathan@yesenia.net McKenziehaven 10
5 4 Patricia Lebsack Karianne Julianne.OConner@kory.org South Elvis 10
6 5 Chelsey Dietrich Kamren Lucio_Hettinger@annie.ca Roscoeview 10
7 6 Mrs. Dennis Schulist Leopoldo_Corkery Karley_Dach@jasper.info South Christy 10
8 7 Kurtis Weissnat Elwyn.Skiles Telly.Hoeger@billy.biz Howemouth 10
9 8 Nicholas Runolfsdottir V Maxime_Nienow Sherwood@rosamond.me Aliyaview 10
10 9 Glenna Reichert Delphine Chaim_McDermott@dana.io Bartholomebury 10
11 10 Clementina DuBuque Moriah.Stanton Rey.Padberg@karina.biz Lebsackbury 10
@@ -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
}
]
}
@@ -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 |
@@ -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()
+87
View File
@@ -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)