ledger: per-user post census from JSONPlaceholder /users + /posts
This commit is contained in:
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
|
||||
|
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user