#!/usr/bin/env python3 """Artifact tabulation — Appendix E, specification 7. Takes: a directory of produced material. Emits: a table of what is there — by file type, by size band, by year of last modification — with the totals reconciling to the item count, so that a description of a production can be checked against the production. Reports what the filesystem states. Filesystem timestamps are metadata, not evidence of authorship or transmission, and the report says so. python3 artifact_tabulate.py --dir ./production --out tabulation.json """ import argparse, json, os, platform, sys from collections import Counter from datetime import datetime, timezone VERSION = "1.0" BANDS = [(0, 1024, "under 1 KB"), (1024, 1 << 20, "1 KB to 1 MB"), (1 << 20, 10 << 20, "1 MB to 10 MB"), (10 << 20, 100 << 20, "10 MB to 100 MB"), (100 << 20, float("inf"), "over 100 MB")] def band(n): for lo, hi, label in BANDS: if lo <= n < hi: return label return "unknown" def main(): ap = argparse.ArgumentParser(description="Tabulate a production so a description of it can be checked.") ap.add_argument("--dir", required=True) ap.add_argument("--out", required=True) a = ap.parse_args() if not os.path.isdir(a.dir): sys.exit(f"not a directory: {a.dir}") by_ext, by_band, by_year = Counter(), Counter(), Counter() bytes_by_ext = Counter() items, unreadable, total_bytes = 0, [], 0 for dp, dns, fns in os.walk(a.dir): dns.sort() for fn in sorted(fns): p = os.path.join(dp, fn); rel = os.path.relpath(p, a.dir).replace(os.sep, "/") try: st = os.stat(p) except OSError as e: unreadable.append({"item": rel, "error": str(e)}); continue items += 1; total_bytes += st.st_size ext = (os.path.splitext(fn)[1].lower() or "(no extension)") by_ext[ext] += 1; bytes_by_ext[ext] += st.st_size by_band[band(st.st_size)] += 1 by_year[datetime.fromtimestamp(st.st_mtime, timezone.utc).strftime("%Y")] += 1 assert sum(by_ext.values()) == items, "extension table does not reconcile to the item count" assert sum(by_band.values()) == items, "size table does not reconcile to the item count" assert sum(by_year.values()) == items, "year table does not reconcile to the item count" report = { "specification": "Artifact tabulation (Appendix E, 7)", "script": os.path.basename(__file__), "script_version": VERSION, "run_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "python": platform.python_version(), "root": os.path.abspath(a.dir), "totals": {"items": items, "bytes": total_bytes, "unreadable": len(unreadable), "reconciles": sum(by_ext.values()) == items}, "by_file_type": [{"extension": k, "items": by_ext[k], "bytes": bytes_by_ext[k]} for k in sorted(by_ext, key=lambda k: (-by_ext[k], k))], "by_size_band": [{"band": lbl, "items": by_band[lbl]} for _, _, lbl in BANDS if by_band[lbl]], "by_year_last_modified": [{"year": y, "items": by_year[y]} for y in sorted(by_year)], "unreadable": unreadable, "caveat": ("Counts are of files as the filesystem presents them. A modification time is " "metadata: it is not evidence of when a document was authored, sent or received, " "and copying can reset it."), } with open(a.out, "w", encoding="utf-8") as f: json.dump(report, f, indent=1); f.write("\n") t = report["totals"] print(f"items={t['items']} bytes={t['bytes']} types={len(by_ext)} unreadable={t['unreadable']} -> {a.out}") if __name__ == "__main__": main()