[{"data":1,"prerenderedAt":328},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-python-stdlib-en":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_id":8,"slug":9,"excerpt":10,"excerpt_id":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"When You Pick a Framework Just to Look Different","Ketika Pilih Framework Cuma Biar Kelihatan Beda","ketika-pilih-framework-cuma-biar-kelihatan-beda","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_id":26,"slug":27},9,"Articles","Artikel","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_id":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_id":36,"slug":37},"Artificial Intelligence","Kecerdasan Artifisial","kecerdasan-artifisial",{"title":39,"title_id":40,"slug":41},"Programming Fundamentals","Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":43,"title_id":44,"slug":45},"Algorithms and Data Structures","Algoritma dan Struktur Data","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_id":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_id":53,"slug":54},"Learning Python","Belajar Python","belajar-python",{"title":56,"title_id":57,"slug":58},"Learning Go","Belajar Golang","belajar-golang",{"title":60,"title_id":61,"slug":62},"Learning Java","Belajar Java","belajar-java",{"title":64,"title_id":65,"slug":66},"Learning Pascal","Belajar Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_id":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":206,"renderedTakeaway":207,"latestArticles":208,"toc":308},{"id":74,"title":75,"title_id":76,"slug":77,"excerpt":78,"excerpt_id":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":15,"tags":82,"thumbnail_image":21,"og_image":86,"series":87,"authors":198,"seoTitle":21,"seoDescription":21,"body_locale":201,"available_locales":202,"body":204,"takeway":205,"prerequisite":21},143,"Python stdlib: the datetime json.dumps Refused to Write","Python stdlib: datetime yang Ditolak json.dumps","python-stdlib","A script that totals minutes per day dies on the line that writes the report. Four stdlib modules carry it, and one TypeError stands in the way.","Skrip yang menjumlahkan menit per hari mati tepat di baris yang menulis laporan. Empat modul stdlib membawanya, dan satu TypeError menghadang.","2026-11-08T01:00:00.000Z",7,[83,84,85],"python","stdlib","cli","\u002Fog\u002Fpython-stdlib.png",{"id":88,"title":89,"title_id":90,"slug":91,"description":92,"description_id":93,"articles":94,"children_series":167,"parent_series":168},18,"Working with Python Scripts","Menggunakan Script Python","python-pemrograman-dengan-script","Leave the interpreter and start writing real .py files, including how to take input and print output.","Keluar dari interpreter dan mulai nulis file .py sendiri, lengkap dengan cara mengambil input dan menampilkan output.",[95,115,131,146,162],{"id":96,"title":97,"title_id":98,"slug":99,"excerpt":100,"excerpt_id":101,"has_translation":12,"published_at":102,"reading_time":103,"hidden_from_listing":15,"tags":104,"thumbnail_image":21,"og_image":106,"series":107,"authors":108},90,"Writing and Running a Python Script","Menulis dan Menjalankan Script Python","menulis-dan-menjalankan-script-python","A Python script is a file holding Python code, and it usually carries the .py extension.","Script python merupakan file berisi kode dengan bahasa Python yang biasanya memiliki ekstensi .py","2021-03-21T14:49:24.000Z",4,[83,49,105,85],"tooling","\u002Fog\u002Fmenulis-dan-menjalankan-script-python.png",{"id":88,"title":89,"title_id":90,"slug":91},[109],{"authors_id":110},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},1,"Daffa Izzuddin","izzudd","Writer",{"id":116,"title":117,"title_id":118,"slug":119,"excerpt":120,"excerpt_id":121,"has_translation":12,"published_at":122,"reading_time":103,"hidden_from_listing":15,"tags":123,"thumbnail_image":21,"og_image":126,"series":127,"authors":128},91,"Input and Output Systems in Python 3","Sistem Input dan Output pada Python 3","sistem-input-dan-output-pada-python-3","A few functions, print() and input() among them, are the standard tools most I\u002FO work in Python is built on.","Beberapa fungsi seperti print() dan input(), merupakan fungsi standar yang banyak digunakan untuk melakukan I\u002FO.","2021-05-19T14:57:25.000Z",[83,124,125],"input-output","dasar","\u002Fog\u002Fsistem-input-dan-output-pada-python-3.png",{"id":88,"title":89,"title_id":90,"slug":91},[129],{"authors_id":130},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":132,"title":133,"title_id":134,"slug":135,"excerpt":136,"excerpt_id":137,"has_translation":12,"published_at":138,"reading_time":81,"hidden_from_listing":15,"tags":139,"thumbnail_image":21,"og_image":141,"series":142,"authors":143},136,"Python Modules: the Import That Printed the Report","Modul Python: Import yang Mencetak Laporannya","python-modules","You pull one function out into its own file, import it, and the whole report runs again and eats your input. A module is a file, `import` runs it top to bottom, and one line decides which of the two jobs it is doing.","Kamu memindahkan satu fungsi ke file sendiri, meng-import-nya, dan seluruh laporan jalan lagi lalu memakan inputmu. Modul adalah file, `import` menjalankannya dari atas ke bawah, dan satu baris menentukan pekerjaan mana yang sedang dijalankan file itu.","2026-11-06T01:00:00.000Z",[83,140],"modules","\u002Fog\u002Fpython-modules.png",{"id":88,"title":89,"title_id":90,"slug":91},[144],{"authors_id":145},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":147,"title":148,"title_id":149,"slug":150,"excerpt":151,"excerpt_id":152,"has_translation":12,"published_at":153,"reading_time":14,"hidden_from_listing":15,"tags":154,"thumbnail_image":21,"og_image":157,"series":158,"authors":159},138,"Python venv: the Install That Went Into the Wrong Python","venv Python: Instalasi yang Masuk ke Python yang Salah","python-venv-pip","`pip install` printed Successfully installed, and the next run raised ModuleNotFoundError on the same machine. Two interpreters, one install, and the variable your script checks is the one the interpreter never sets.","`pip install` mencetak Successfully installed, lalu jalan berikutnya melempar ModuleNotFoundError di mesin yang sama. Dua interpreter, satu instalasi, dan variabel yang skripmu periksa justru tidak pernah dipasang oleh interpreter.","2026-11-07T12:00:00.000Z",[83,155,156],"venv","pip","\u002Fog\u002Fpython-venv-pip.png",{"id":88,"title":89,"title_id":90,"slug":91},[160],{"authors_id":161},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":74,"title":75,"title_id":76,"slug":77,"excerpt":78,"excerpt_id":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":15,"tags":82,"thumbnail_image":21,"og_image":86,"series":163,"authors":164},{"id":88,"title":89,"title_id":90,"slug":91},[165],{"authors_id":166},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},[],{"id":169,"title":52,"title_id":53,"slug":54,"parent_series":170,"children_series":171},10,{"id":111,"title":48,"title_id":48,"slug":49},[172,177,182,183,188,193],{"id":173,"title":174,"title_id":175,"slug":176,"cover_image":21},16,"Getting Started with Python","Berkenalan dengan Bahasa Python","python-intro",{"id":178,"title":179,"title_id":180,"slug":181,"cover_image":21},17,"Variables, Types, and Operations","Variabel, Tipe Data, dan Operasi","python-variabel-tipe-data",{"id":88,"title":89,"title_id":90,"slug":91,"cover_image":21},{"id":184,"title":185,"title_id":186,"slug":187,"cover_image":21},19,"Control Structures","Struktur Kontrol","python-struktur-kontrol",{"id":189,"title":190,"title_id":191,"slug":192,"cover_image":21},20,"Functional Programming with Python","Pemrograman Fungsional dengan Python","python-pemrograman-fungsional",{"id":194,"title":195,"title_id":196,"slug":197,"cover_image":21},30,"Collections and Strings","Koleksi dan String","python-collections",[199],{"authors_id":200},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},"en",[201,203],"id","You point the script at a folder of three log files. It reads them, prints nothing, and dies with a TypeError on the line that writes the report. The last two lines of the error are these:\n\n```text\n$ python3 daily.py logs\nTypeError: Object of type datetime is not JSON serializable\nwhen serializing dict item 'generated_at'\n```\n\nThe second line names the dict key that held the value json refused. The numbers here come from Python 3.14.9 on Linux.\n\n## Four modules, one small program\n\n`daily.py` adds up minutes per day from a folder of session logs. Each of four standard-library modules has one job.\n\n```python\n# daily.py\nimport argparse\nimport json\nfrom datetime import datetime\nfrom pathlib import Path\n\nparser = argparse.ArgumentParser(description=\"Minutes per day from a folder of session logs\")\nparser.add_argument(\"folder\")\nargs = parser.parse_args()\n\ntotal = {}\nfor path in sorted(Path(args.folder).glob(\"*.json\")):\n    if path.name == \"summary.json\":\n        continue\n    for session in json.loads(path.read_text()):\n        total[session[\"date\"]] = total.get(session[\"date\"], 0) + session[\"minutes\"]\n\nreport = {\n    \"generated_at\": datetime.now(),\n    \"days\": [{\"date\": day, \"minutes\": minutes} for day, minutes in sorted(total.items())],\n}\n\nout = Path(args.folder) \u002F \"summary.json\"\nout.write_text(json.dumps(report, indent=2))\nprint(f\"{len(report['days'])} days from {len(list(Path(args.folder).glob('*.json'))) - 1} files\")\n```\n\n[`argparse`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fargparse.html) reads one argument, the folder. You never asked for `-h`, but it exists anyway:\n\n```text\n$ python3 daily.py --help\nusage: daily.py [-h] folder\n\nMinutes per day from a folder of session logs\n\npositional arguments:\n  folder\n\noptions:\n  -h, --help  show this help message and exit\n```\n\n[`pathlib`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fpathlib.html) builds the paths. `glob(\"*.json\")` lists the log files, and `read_text()` hands you each one as a string. [`json`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fjson.html) turns that text into lists and dicts, then turns the report back into text. [`datetime`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fdatetime.html) supplies the stamp.\n\nThe glob also matches `summary.json`, the file the script is about to write. The `if` skips it. Without that, a second run would read its own output as a session log. The `- 1` in the last line removes it from the file count, since it exists by then.\n\n## Why json will not take a datetime\n\nThe `json` module writes a short list of types: string, number, boolean, `None`, list and dict. A `datetime` is not on it.\n\nWhen `json.dumps` meets a type outside that list, it raises a `TypeError`. It does not guess a format. A date can be written many ways, so someone has to choose one. ISO 8601 is the choice here: year, month, day, a `T`, then the time. The documented way to get that string is `isoformat()`.\n\n## The checker\n\n`check_daily.py` is a plain script with no test framework. Run it from the folder that holds `daily.py`.\n\n```python\n# check_daily.py\nimport json\nimport subprocess\nimport sys\nimport tempfile\nfrom datetime import datetime\nfrom pathlib import Path\n\nFIXTURE = {\n    \"mon.json\": [{\"date\": \"2026-11-06\", \"minutes\": 25}, {\"date\": \"2026-11-06\", \"minutes\": 35}],\n    \"tue.json\": [{\"date\": \"2026-11-07\", \"minutes\": 40}],\n    \"wed.json\": [{\"date\": \"2026-11-02\", \"minutes\": 15}, {\"date\": \"2026-11-08\", \"minutes\": 50}],\n}\nEXPECTED = {\"2026-11-02\": 15, \"2026-11-06\": 60, \"2026-11-07\": 40, \"2026-11-08\": 50}\n\n\ndef report(results):\n    for label, ok, detail in results:\n        print(f\"{'PASS' if ok else 'FAIL'}  {label}\" + (f\"  \u003C- {detail}\" if not ok and detail else \"\"))\n    passed = sum(1 for _, ok, _ in results if ok)\n    print(f\"\\n{passed} of {len(results)} checks passed\")\n    return passed == len(results)\n\n\ndef main():\n    folder = Path(tempfile.mkdtemp())\n    for name, sessions in FIXTURE.items():\n        (folder \u002F name).write_text(json.dumps(sessions))\n\n    run = subprocess.run([sys.executable, \"daily.py\", str(folder)], capture_output=True, text=True)\n    crash = \"\"\n    for line in reversed(run.stderr.strip().splitlines()):\n        if line.split(\":\")[0].endswith(\"Error\"):\n            crash = line\n            break\n    if run.returncode != 0:\n        report([(\"the script runs without crashing\", False, crash)])\n        return 1\n\n    results = [(\"the script runs without crashing\", True, \"\")]\n    out = folder \u002F \"summary.json\"\n    results.append((\"summary.json is written\", out.exists(), \"no file\"))\n    if not out.exists():\n        report(results)\n        return 1\n\n    text = out.read_text()\n    try:\n        summary = json.loads(text)\n    except json.JSONDecodeError as exc:\n        report(results + [(\"summary.json is valid JSON\", False, str(exc))])\n        return 1\n    results.append((\"summary.json is valid JSON\", True, \"\"))\n\n    days = summary.get(\"days\", [])\n    results.append((\"one row per calendar day\", len(days) == 4, f\"got {len(days)}\"))\n    minutes = {row[\"date\"]: row[\"minutes\"] for row in days if isinstance(row.get(\"date\"), str)}\n    results.append((\"the two sessions on 2026-11-06 are added up\", minutes.get(\"2026-11-06\") == 60, f\"got {minutes.get('2026-11-06')}\"))\n    results.append((\"every day carries its own total\", minutes == EXPECTED, str(minutes)))\n    results.append((\"the rows come out in date order\", [row[\"date\"] for row in days] == sorted(EXPECTED), str([row[\"date\"] for row in days])))\n\n    stamp = summary.get(\"generated_at\")\n    iso = False\n    if isinstance(stamp, str):\n        try:\n            iso = datetime.fromisoformat(stamp) is not None\n        except ValueError:\n            iso = False\n    results.append((\"generated_at is an ISO 8601 string\", iso, repr(stamp)))\n    results.append((\"the script prints its own summary line\", run.stdout.strip() == \"4 days from 3 files\", repr(run.stdout.strip())))\n\n    return 0 if report(results) else 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\nIt writes a fixed fixture folder: three files, with two sessions on 2026-11-06 in one of them. It runs `daily.py` on that folder, then asserts on what the script printed and on the JSON it wrote. Against the starter script, the result is red:\n\n```text\n$ python3 check_daily.py\nFAIL  the script runs without crashing  \u003C- TypeError: Object of type datetime is not JSON serializable\n\n0 of 1 checks passed\n```\n\nThe exit code is 1. The text after the arrow is the error line the checker pulled from stderr. It stops at the first failure on purpose. A crashed script writes no `summary.json`, so the other checks would have nothing to look at.\n\n## The one line that fixes it\n\n`datetime.now()` returns the machine's local time with no timezone attached. Add `.isoformat(timespec=\"seconds\")` and you get a string, which json already knows how to write. Only the `generated_at` line changes.\n\n```python\n# daily.py\nimport argparse\nimport json\nfrom datetime import datetime\nfrom pathlib import Path\n\nparser = argparse.ArgumentParser(description=\"Minutes per day from a folder of session logs\")\nparser.add_argument(\"folder\")\nargs = parser.parse_args()\n\ntotal = {}\nfor path in sorted(Path(args.folder).glob(\"*.json\")):\n    if path.name == \"summary.json\":\n        continue\n    for session in json.loads(path.read_text()):\n        total[session[\"date\"]] = total.get(session[\"date\"], 0) + session[\"minutes\"]\n\nreport = {\n    \"generated_at\": datetime.now().isoformat(timespec=\"seconds\"),\n    \"days\": [{\"date\": day, \"minutes\": minutes} for day, minutes in sorted(total.items())],\n}\n\nout = Path(args.folder) \u002F \"summary.json\"\nout.write_text(json.dumps(report, indent=2))\nprint(f\"{len(report['days'])} days from {len(list(Path(args.folder).glob('*.json'))) - 1} files\")\n```\n\nA real run on a folder with three log files printed exactly `4 days from 3 files`. The checker agrees, with exit code 0:\n\n```text\n$ python3 check_daily.py\nPASS  the script runs without crashing\nPASS  summary.json is written\nPASS  summary.json is valid JSON\nPASS  one row per calendar day\nPASS  the two sessions on 2026-11-06 are added up\nPASS  every day carries its own total\nPASS  the rows come out in date order\nPASS  generated_at is an ISO 8601 string\nPASS  the script prints its own summary line\n\n9 of 9 checks passed\n```\n\nThe `summary.json` from that real run began with the stamp, then four day rows:\n\n```json\n{\n  \"generated_at\": \"2026-11-08T00:01:43\",\n  \"days\": [\n    {\n      \"date\": \"2026-11-02\",\n      \"minutes\": 15\n    },\n    {\n      \"date\": \"2026-11-06\",\n      \"minutes\": 60\n    },\n    {\n      \"date\": \"2026-11-07\",\n      \"minutes\": 40\n    },\n    {\n      \"date\": \"2026-11-08\",\n      \"minutes\": 50\n    }\n  ]\n}\n```\n\nThe 60 is a sum. That file held two sessions, 25 and 35 minutes.\n\n## What the checker cannot see\n\nChange `timespec=\"seconds\"` to `timespec=\"hours\"` and run the checker again. It still prints `9 of 9 checks passed` and exits 0. `datetime.fromisoformat` accepts that shorter string. The date check proves the stamp parses. It does not prove the stamp has the precision you wanted.\n\n## Before the next article\n\nDelete `sorted()` around `total.items()` and run the checker once more. You get 8 of 9 checks and exit code 1:\n\n```text\n$ python3 check_daily.py\nPASS  the script runs without crashing\nPASS  summary.json is written\nPASS  summary.json is valid JSON\nPASS  one row per calendar day\nPASS  the two sessions on 2026-11-06 are added up\nPASS  every day carries its own total\nFAIL  the rows come out in date order  \u003C- ['2026-11-06', '2026-11-07', '2026-11-02', '2026-11-08']\nPASS  generated_at is an ISO 8601 string\nPASS  the script prints its own summary line\n\n8 of 9 checks passed\n```\n\nDicts keep insertion order, so the rows come out in the order the files were read. A row order you did not sort is an order you did not choose.\n","- `json.dumps` writes a fixed set of basic types, and a `datetime` is not one of them, so the encoder raises instead of choosing a format for you.\n- `datetime.now()` returns the machine's local time with no timezone, and `isoformat(timespec=\"seconds\")` is the documented way to turn it into the string json can write.\n- `argparse` answers `-h` without being asked, `pathlib` turns a folder into paths and text, and `json` turns text into lists and dicts.\n- A checker that only asserts the stamp parses cannot tell `timespec=\"hours\"` from `timespec=\"seconds\"`, so it proves the shape, not the precision.","\u003Cp>You point the script at a folder of three log files. It reads them, prints nothing, and dies with a TypeError on the line that writes the report. The last two lines of the error are these:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20daily.py%20logs%0ATypeError%3A%20Object%20of%20type%20datetime%20is%20not%20JSON%20serializable%0Awhen%20serializing%20dict%20item%20'generated_at'\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 daily.py logs\nTypeError: Object of type datetime is not JSON serializable\nwhen serializing dict item 'generated_at'\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>The second line names the dict key that held the value json refused. The numbers here come from Python 3.14.9 on Linux.\u003C\u002Fp>\n\u003Ch2 id=\"four-modules-one-small-program\">Four modules, one small program\u003C\u002Fh2>\n\u003Cp>\u003Ccode>daily.py\u003C\u002Fcode> adds up minutes per day from a folder of session logs. Each of four standard-library modules has one job.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20daily.py%0Aimport%20argparse%0Aimport%20json%0Afrom%20datetime%20import%20datetime%0Afrom%20pathlib%20import%20Path%0A%0Aparser%20%3D%20argparse.ArgumentParser(description%3D%22Minutes%20per%20day%20from%20a%20folder%20of%20session%20logs%22)%0Aparser.add_argument(%22folder%22)%0Aargs%20%3D%20parser.parse_args()%0A%0Atotal%20%3D%20%7B%7D%0Afor%20path%20in%20sorted(Path(args.folder).glob(%22*.json%22))%3A%0A%20%20%20%20if%20path.name%20%3D%3D%20%22summary.json%22%3A%0A%20%20%20%20%20%20%20%20continue%0A%20%20%20%20for%20session%20in%20json.loads(path.read_text())%3A%0A%20%20%20%20%20%20%20%20total%5Bsession%5B%22date%22%5D%5D%20%3D%20total.get(session%5B%22date%22%5D%2C%200)%20%2B%20session%5B%22minutes%22%5D%0A%0Areport%20%3D%20%7B%0A%20%20%20%20%22generated_at%22%3A%20datetime.now()%2C%0A%20%20%20%20%22days%22%3A%20%5B%7B%22date%22%3A%20day%2C%20%22minutes%22%3A%20minutes%7D%20for%20day%2C%20minutes%20in%20sorted(total.items())%5D%2C%0A%7D%0A%0Aout%20%3D%20Path(args.folder)%20%2F%20%22summary.json%22%0Aout.write_text(json.dumps(report%2C%20indent%3D2))%0Aprint(f%22%7Blen(report%5B'days'%5D)%7D%20days%20from%20%7Blen(list(Path(args.folder).glob('*.json')))%20-%201%7D%20files%22)\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># daily.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> argparse\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> pathlib \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">parser \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> argparse.ArgumentParser(\u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">description\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"Minutes per day from a folder of session logs\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">parser.add_argument(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"folder\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">args \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> parser.parse_args()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">total \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> path \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(Path(args.folder).glob(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"*.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> path.name \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"summary.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        continue\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json.loads(path.read_text()):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        total[session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> total.get(session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">], \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">report \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"generated_at\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: datetime.now(),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"days\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: day, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: minutes} \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> day, minutes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(total.items())],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">out \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path(args.folder) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"summary.json\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">out.write_text(json.dumps(report, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">indent\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(report[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'days'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">])\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> days from \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">list\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(Path(args.folder).glob(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'*.json'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> files\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>\u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fargparse.html\">\u003Ccode>argparse\u003C\u002Fcode>\u003C\u002Fa> reads one argument, the folder. You never asked for \u003Ccode>-h\u003C\u002Fcode>, but it exists anyway:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20daily.py%20--help%0Ausage%3A%20daily.py%20%5B-h%5D%20folder%0A%0AMinutes%20per%20day%20from%20a%20folder%20of%20session%20logs%0A%0Apositional%20arguments%3A%0A%20%20folder%0A%0Aoptions%3A%0A%20%20-h%2C%20--help%20%20show%20this%20help%20message%20and%20exit\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 daily.py --help\nusage: daily.py [-h] folder\n\nMinutes per day from a folder of session logs\n\npositional arguments:\n  folder\n\noptions:\n  -h, --help  show this help message and exit\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>\u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fpathlib.html\">\u003Ccode>pathlib\u003C\u002Fcode>\u003C\u002Fa> builds the paths. \u003Ccode>glob(&quot;*.json&quot;)\u003C\u002Fcode> lists the log files, and \u003Ccode>read_text()\u003C\u002Fcode> hands you each one as a string. \u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fjson.html\">\u003Ccode>json\u003C\u002Fcode>\u003C\u002Fa> turns that text into lists and dicts, then turns the report back into text. \u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fdatetime.html\">\u003Ccode>datetime\u003C\u002Fcode>\u003C\u002Fa> supplies the stamp.\u003C\u002Fp>\n\u003Cp>The glob also matches \u003Ccode>summary.json\u003C\u002Fcode>, the file the script is about to write. The \u003Ccode>if\u003C\u002Fcode> skips it. Without that, a second run would read its own output as a session log. The \u003Ccode>- 1\u003C\u002Fcode> in the last line removes it from the file count, since it exists by then.\u003C\u002Fp>\n\u003Ch2 id=\"why-json-will-not-take-a-datetime\">Why json will not take a datetime\u003C\u002Fh2>\n\u003Cp>The \u003Ccode>json\u003C\u002Fcode> module writes a short list of types: string, number, boolean, \u003Ccode>None\u003C\u002Fcode>, list and dict. A \u003Ccode>datetime\u003C\u002Fcode> is not on it.\u003C\u002Fp>\n\u003Cp>When \u003Ccode>json.dumps\u003C\u002Fcode> meets a type outside that list, it raises a \u003Ccode>TypeError\u003C\u002Fcode>. It does not guess a format. A date can be written many ways, so someone has to choose one. ISO 8601 is the choice here: year, month, day, a \u003Ccode>T\u003C\u002Fcode>, then the time. The documented way to get that string is \u003Ccode>isoformat()\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"the-checker\">The checker\u003C\u002Fh2>\n\u003Cp>\u003Ccode>check_daily.py\u003C\u002Fcode> is a plain script with no test framework. Run it from the folder that holds \u003Ccode>daily.py\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20check_daily.py%0Aimport%20json%0Aimport%20subprocess%0Aimport%20sys%0Aimport%20tempfile%0Afrom%20datetime%20import%20datetime%0Afrom%20pathlib%20import%20Path%0A%0AFIXTURE%20%3D%20%7B%0A%20%20%20%20%22mon.json%22%3A%20%5B%7B%22date%22%3A%20%222026-11-06%22%2C%20%22minutes%22%3A%2025%7D%2C%20%7B%22date%22%3A%20%222026-11-06%22%2C%20%22minutes%22%3A%2035%7D%5D%2C%0A%20%20%20%20%22tue.json%22%3A%20%5B%7B%22date%22%3A%20%222026-11-07%22%2C%20%22minutes%22%3A%2040%7D%5D%2C%0A%20%20%20%20%22wed.json%22%3A%20%5B%7B%22date%22%3A%20%222026-11-02%22%2C%20%22minutes%22%3A%2015%7D%2C%20%7B%22date%22%3A%20%222026-11-08%22%2C%20%22minutes%22%3A%2050%7D%5D%2C%0A%7D%0AEXPECTED%20%3D%20%7B%222026-11-02%22%3A%2015%2C%20%222026-11-06%22%3A%2060%2C%20%222026-11-07%22%3A%2040%2C%20%222026-11-08%22%3A%2050%7D%0A%0A%0Adef%20report(results)%3A%0A%20%20%20%20for%20label%2C%20ok%2C%20detail%20in%20results%3A%0A%20%20%20%20%20%20%20%20print(f%22%7B'PASS'%20if%20ok%20else%20'FAIL'%7D%20%20%7Blabel%7D%22%20%2B%20(f%22%20%20%3C-%20%7Bdetail%7D%22%20if%20not%20ok%20and%20detail%20else%20%22%22))%0A%20%20%20%20passed%20%3D%20sum(1%20for%20_%2C%20ok%2C%20_%20in%20results%20if%20ok)%0A%20%20%20%20print(f%22%5Cn%7Bpassed%7D%20of%20%7Blen(results)%7D%20checks%20passed%22)%0A%20%20%20%20return%20passed%20%3D%3D%20len(results)%0A%0A%0Adef%20main()%3A%0A%20%20%20%20folder%20%3D%20Path(tempfile.mkdtemp())%0A%20%20%20%20for%20name%2C%20sessions%20in%20FIXTURE.items()%3A%0A%20%20%20%20%20%20%20%20(folder%20%2F%20name).write_text(json.dumps(sessions))%0A%0A%20%20%20%20run%20%3D%20subprocess.run(%5Bsys.executable%2C%20%22daily.py%22%2C%20str(folder)%5D%2C%20capture_output%3DTrue%2C%20text%3DTrue)%0A%20%20%20%20crash%20%3D%20%22%22%0A%20%20%20%20for%20line%20in%20reversed(run.stderr.strip().splitlines())%3A%0A%20%20%20%20%20%20%20%20if%20line.split(%22%3A%22)%5B0%5D.endswith(%22Error%22)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20crash%20%3D%20line%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20if%20run.returncode%20!%3D%200%3A%0A%20%20%20%20%20%20%20%20report(%5B(%22the%20script%20runs%20without%20crashing%22%2C%20False%2C%20crash)%5D)%0A%20%20%20%20%20%20%20%20return%201%0A%0A%20%20%20%20results%20%3D%20%5B(%22the%20script%20runs%20without%20crashing%22%2C%20True%2C%20%22%22)%5D%0A%20%20%20%20out%20%3D%20folder%20%2F%20%22summary.json%22%0A%20%20%20%20results.append((%22summary.json%20is%20written%22%2C%20out.exists()%2C%20%22no%20file%22))%0A%20%20%20%20if%20not%20out.exists()%3A%0A%20%20%20%20%20%20%20%20report(results)%0A%20%20%20%20%20%20%20%20return%201%0A%0A%20%20%20%20text%20%3D%20out.read_text()%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20summary%20%3D%20json.loads(text)%0A%20%20%20%20except%20json.JSONDecodeError%20as%20exc%3A%0A%20%20%20%20%20%20%20%20report(results%20%2B%20%5B(%22summary.json%20is%20valid%20JSON%22%2C%20False%2C%20str(exc))%5D)%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20results.append((%22summary.json%20is%20valid%20JSON%22%2C%20True%2C%20%22%22))%0A%0A%20%20%20%20days%20%3D%20summary.get(%22days%22%2C%20%5B%5D)%0A%20%20%20%20results.append((%22one%20row%20per%20calendar%20day%22%2C%20len(days)%20%3D%3D%204%2C%20f%22got%20%7Blen(days)%7D%22))%0A%20%20%20%20minutes%20%3D%20%7Brow%5B%22date%22%5D%3A%20row%5B%22minutes%22%5D%20for%20row%20in%20days%20if%20isinstance(row.get(%22date%22)%2C%20str)%7D%0A%20%20%20%20results.append((%22the%20two%20sessions%20on%202026-11-06%20are%20added%20up%22%2C%20minutes.get(%222026-11-06%22)%20%3D%3D%2060%2C%20f%22got%20%7Bminutes.get('2026-11-06')%7D%22))%0A%20%20%20%20results.append((%22every%20day%20carries%20its%20own%20total%22%2C%20minutes%20%3D%3D%20EXPECTED%2C%20str(minutes)))%0A%20%20%20%20results.append((%22the%20rows%20come%20out%20in%20date%20order%22%2C%20%5Brow%5B%22date%22%5D%20for%20row%20in%20days%5D%20%3D%3D%20sorted(EXPECTED)%2C%20str(%5Brow%5B%22date%22%5D%20for%20row%20in%20days%5D)))%0A%0A%20%20%20%20stamp%20%3D%20summary.get(%22generated_at%22)%0A%20%20%20%20iso%20%3D%20False%0A%20%20%20%20if%20isinstance(stamp%2C%20str)%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20iso%20%3D%20datetime.fromisoformat(stamp)%20is%20not%20None%0A%20%20%20%20%20%20%20%20except%20ValueError%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20iso%20%3D%20False%0A%20%20%20%20results.append((%22generated_at%20is%20an%20ISO%208601%20string%22%2C%20iso%2C%20repr(stamp)))%0A%20%20%20%20results.append((%22the%20script%20prints%20its%20own%20summary%20line%22%2C%20run.stdout.strip()%20%3D%3D%20%224%20days%20from%203%20files%22%2C%20repr(run.stdout.strip())))%0A%0A%20%20%20%20return%200%20if%20report(results)%20else%201%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20sys.exit(main())\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># check_daily.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> subprocess\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tempfile\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> pathlib \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">FIXTURE\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"mon.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-06\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">25\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}, {\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-06\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">35\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"tue.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-07\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">40\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"wed.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-02\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">15\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}, {\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-08\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">50\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-02\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">15\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-06\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">60\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-07\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">40\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-08\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">50\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> report\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(results):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> label, ok, detail \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> results:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">        print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'PASS'\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ok \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> 'FAIL'\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">  {\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">label\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"  &#x3C;- \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">detail\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ok \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">and\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> detail \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    passed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sum\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> _, ok, _ \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> results \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ok)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">passed\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> of \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(results)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> checks passed\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> passed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(results)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> main\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">():\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    folder \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path(tempfile.mkdtemp())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> name, sessions \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> FIXTURE\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">.items():\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (folder \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> name).write_text(json.dumps(sessions))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    run \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> subprocess.run([sys.executable, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"daily.py\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(folder)], \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">capture_output\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">text\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    crash \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> reversed\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(run.stderr.strip().splitlines()):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line.split(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\":\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">].endswith(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"Error\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            crash \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            break\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run.returncode \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">!=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        report([(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the script runs without crashing\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">False\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, crash)])\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the script runs without crashing\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    out \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> folder \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"summary.json\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"summary.json is written\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, out.exists(), \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"no file\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> out.exists():\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        report(results)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    text \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> out.read_text()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    try\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        summary \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json.loads(text)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    except\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json.JSONDecodeError \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">as\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> exc:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        report(results \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"summary.json is valid JSON\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">False\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(exc))])\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"summary.json is valid JSON\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    days \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> summary.get(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"days\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, [])\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"one row per calendar day\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(days) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 4\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"got \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(days)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    minutes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {row[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]: row[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> row \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> days \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> isinstance\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(row.get(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">), \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the two sessions on 2026-11-06 are added up\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, minutes.get(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-06\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 60\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"got \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">minutes.get(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'2026-11-06'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"every day carries its own total\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, minutes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(minutes)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the rows come out in date order\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, [row[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> row \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> days] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">), \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">([row[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> row \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> days])))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    stamp \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> summary.get(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"generated_at\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    iso \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> False\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> isinstance\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(stamp, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        try\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            iso \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime.fromisoformat(stamp) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">is\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> None\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        except\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> ValueError\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            iso \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> False\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"generated_at is an ISO 8601 string\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, iso, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">repr\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(stamp)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the script prints its own summary line\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, run.stdout.strip() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"4 days from 3 files\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">repr\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(run.stdout.strip())))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> report(results) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> __name__\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> ==\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"__main__\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    sys.exit(main())\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>It writes a fixed fixture folder: three files, with two sessions on 2026-11-06 in one of them. It runs \u003Ccode>daily.py\u003C\u002Fcode> on that folder, then asserts on what the script printed and on the JSON it wrote. Against the starter script, the result is red:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20check_daily.py%0AFAIL%20%20the%20script%20runs%20without%20crashing%20%20%3C-%20TypeError%3A%20Object%20of%20type%20datetime%20is%20not%20JSON%20serializable%0A%0A0%20of%201%20checks%20passed\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 check_daily.py\nFAIL  the script runs without crashing  &lt;- TypeError: Object of type datetime is not JSON serializable\n\n0 of 1 checks passed\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>The exit code is 1. The text after the arrow is the error line the checker pulled from stderr. It stops at the first failure on purpose. A crashed script writes no \u003Ccode>summary.json\u003C\u002Fcode>, so the other checks would have nothing to look at.\u003C\u002Fp>\n\u003Ch2 id=\"the-one-line-that-fixes-it\">The one line that fixes it\u003C\u002Fh2>\n\u003Cp>\u003Ccode>datetime.now()\u003C\u002Fcode> returns the machine&#39;s local time with no timezone attached. Add \u003Ccode>.isoformat(timespec=&quot;seconds&quot;)\u003C\u002Fcode> and you get a string, which json already knows how to write. Only the \u003Ccode>generated_at\u003C\u002Fcode> line changes.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20daily.py%0Aimport%20argparse%0Aimport%20json%0Afrom%20datetime%20import%20datetime%0Afrom%20pathlib%20import%20Path%0A%0Aparser%20%3D%20argparse.ArgumentParser(description%3D%22Minutes%20per%20day%20from%20a%20folder%20of%20session%20logs%22)%0Aparser.add_argument(%22folder%22)%0Aargs%20%3D%20parser.parse_args()%0A%0Atotal%20%3D%20%7B%7D%0Afor%20path%20in%20sorted(Path(args.folder).glob(%22*.json%22))%3A%0A%20%20%20%20if%20path.name%20%3D%3D%20%22summary.json%22%3A%0A%20%20%20%20%20%20%20%20continue%0A%20%20%20%20for%20session%20in%20json.loads(path.read_text())%3A%0A%20%20%20%20%20%20%20%20total%5Bsession%5B%22date%22%5D%5D%20%3D%20total.get(session%5B%22date%22%5D%2C%200)%20%2B%20session%5B%22minutes%22%5D%0A%0Areport%20%3D%20%7B%0A%20%20%20%20%22generated_at%22%3A%20datetime.now().isoformat(timespec%3D%22seconds%22)%2C%0A%20%20%20%20%22days%22%3A%20%5B%7B%22date%22%3A%20day%2C%20%22minutes%22%3A%20minutes%7D%20for%20day%2C%20minutes%20in%20sorted(total.items())%5D%2C%0A%7D%0A%0Aout%20%3D%20Path(args.folder)%20%2F%20%22summary.json%22%0Aout.write_text(json.dumps(report%2C%20indent%3D2))%0Aprint(f%22%7Blen(report%5B'days'%5D)%7D%20days%20from%20%7Blen(list(Path(args.folder).glob('*.json')))%20-%201%7D%20files%22)\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># daily.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> argparse\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> datetime\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> pathlib \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">parser \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> argparse.ArgumentParser(\u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">description\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"Minutes per day from a folder of session logs\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">parser.add_argument(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"folder\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">args \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> parser.parse_args()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">total \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> path \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(Path(args.folder).glob(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"*.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> path.name \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"summary.json\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        continue\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> json.loads(path.read_text()):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        total[session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> total.get(session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">], \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">report \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"generated_at\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: datetime.now().isoformat(\u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">timespec\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"seconds\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">),\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"days\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: day, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: minutes} \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> day, minutes \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(total.items())],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">out \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> Path(args.folder) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"summary.json\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">out.write_text(json.dumps(report, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">indent\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(report[\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'days'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">])\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> days from \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">list\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(Path(args.folder).glob(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'*.json'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> files\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>A real run on a folder with three log files printed exactly \u003Ccode>4 days from 3 files\u003C\u002Fcode>. The checker agrees, with exit code 0:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20check_daily.py%0APASS%20%20the%20script%20runs%20without%20crashing%0APASS%20%20summary.json%20is%20written%0APASS%20%20summary.json%20is%20valid%20JSON%0APASS%20%20one%20row%20per%20calendar%20day%0APASS%20%20the%20two%20sessions%20on%202026-11-06%20are%20added%20up%0APASS%20%20every%20day%20carries%20its%20own%20total%0APASS%20%20the%20rows%20come%20out%20in%20date%20order%0APASS%20%20generated_at%20is%20an%20ISO%208601%20string%0APASS%20%20the%20script%20prints%20its%20own%20summary%20line%0A%0A9%20of%209%20checks%20passed\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 check_daily.py\nPASS  the script runs without crashing\nPASS  summary.json is written\nPASS  summary.json is valid JSON\nPASS  one row per calendar day\nPASS  the two sessions on 2026-11-06 are added up\nPASS  every day carries its own total\nPASS  the rows come out in date order\nPASS  generated_at is an ISO 8601 string\nPASS  the script prints its own summary line\n\n9 of 9 checks passed\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>The \u003Ccode>summary.json\u003C\u002Fcode> from that real run began with the stamp, then four day rows:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">JSON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%7B%0A%20%20%22generated_at%22%3A%20%222026-11-08T00%3A01%3A43%22%2C%0A%20%20%22days%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22date%22%3A%20%222026-11-02%22%2C%0A%20%20%20%20%20%20%22minutes%22%3A%2015%0A%20%20%20%20%7D%2C%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22date%22%3A%20%222026-11-06%22%2C%0A%20%20%20%20%20%20%22minutes%22%3A%2060%0A%20%20%20%20%7D%2C%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22date%22%3A%20%222026-11-07%22%2C%0A%20%20%20%20%20%20%22minutes%22%3A%2040%0A%20%20%20%20%7D%2C%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22date%22%3A%20%222026-11-08%22%2C%0A%20%20%20%20%20%20%22minutes%22%3A%2050%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"generated_at\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-08T00:01:43\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"days\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: [\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-02\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">15\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-06\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">60\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-07\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">40\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"date\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"2026-11-08\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"minutes\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">50\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>The 60 is a sum. That file held two sessions, 25 and 35 minutes.\u003C\u002Fp>\n\u003Ch2 id=\"what-the-checker-cannot-see\">What the checker cannot see\u003C\u002Fh2>\n\u003Cp>Change \u003Ccode>timespec=&quot;seconds&quot;\u003C\u002Fcode> to \u003Ccode>timespec=&quot;hours&quot;\u003C\u002Fcode> and run the checker again. It still prints \u003Ccode>9 of 9 checks passed\u003C\u002Fcode> and exits 0. \u003Ccode>datetime.fromisoformat\u003C\u002Fcode> accepts that shorter string. The date check proves the stamp parses. It does not prove the stamp has the precision you wanted.\u003C\u002Fp>\n\u003Ch2 id=\"before-the-next-article\">Before the next article\u003C\u002Fh2>\n\u003Cp>Delete \u003Ccode>sorted()\u003C\u002Fcode> around \u003Ccode>total.items()\u003C\u002Fcode> and run the checker once more. You get 8 of 9 checks and exit code 1:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20check_daily.py%0APASS%20%20the%20script%20runs%20without%20crashing%0APASS%20%20summary.json%20is%20written%0APASS%20%20summary.json%20is%20valid%20JSON%0APASS%20%20one%20row%20per%20calendar%20day%0APASS%20%20the%20two%20sessions%20on%202026-11-06%20are%20added%20up%0APASS%20%20every%20day%20carries%20its%20own%20total%0AFAIL%20%20the%20rows%20come%20out%20in%20date%20order%20%20%3C-%20%5B'2026-11-06'%2C%20'2026-11-07'%2C%20'2026-11-02'%2C%20'2026-11-08'%5D%0APASS%20%20generated_at%20is%20an%20ISO%208601%20string%0APASS%20%20the%20script%20prints%20its%20own%20summary%20line%0A%0A8%20of%209%20checks%20passed\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 check_daily.py\nPASS  the script runs without crashing\nPASS  summary.json is written\nPASS  summary.json is valid JSON\nPASS  one row per calendar day\nPASS  the two sessions on 2026-11-06 are added up\nPASS  every day carries its own total\nFAIL  the rows come out in date order  &lt;- ['2026-11-06', '2026-11-07', '2026-11-02', '2026-11-08']\nPASS  generated_at is an ISO 8601 string\nPASS  the script prints its own summary line\n\n8 of 9 checks passed\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Dicts keep insertion order, so the rows come out in the order the files were read. A row order you did not sort is an order you did not choose.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Ccode>json.dumps\u003C\u002Fcode> writes a fixed set of basic types, and a \u003Ccode>datetime\u003C\u002Fcode> is not one of them, so the encoder raises instead of choosing a format for you.\u003C\u002Fli>\n\u003Cli>\u003Ccode>datetime.now()\u003C\u002Fcode> returns the machine&#39;s local time with no timezone, and \u003Ccode>isoformat(timespec=&quot;seconds&quot;)\u003C\u002Fcode> is the documented way to turn it into the string json can write.\u003C\u002Fli>\n\u003Cli>\u003Ccode>argparse\u003C\u002Fcode> answers \u003Ccode>-h\u003C\u002Fcode> without being asked, \u003Ccode>pathlib\u003C\u002Fcode> turns a folder into paths and text, and \u003Ccode>json\u003C\u002Fcode> turns text into lists and dicts.\u003C\u002Fli>\n\u003Cli>A checker that only asserts the stamp parses cannot tell \u003Ccode>timespec=&quot;hours&quot;\u003C\u002Fcode> from \u003Ccode>timespec=&quot;seconds&quot;\u003C\u002Fcode>, so it proves the shape, not the precision.\u003C\u002Fli>\n\u003C\u002Ful>\n",[209,231,250,267,286,303],{"id":210,"title":211,"title_id":212,"slug":213,"excerpt":214,"excerpt_id":215,"has_translation":12,"published_at":216,"reading_time":103,"hidden_from_listing":15,"tags":217,"thumbnail_image":21,"og_image":222,"series":223,"authors":228},48,"What a Linked List Actually Is","Tentang Struktur Data LinkedList","tentang-struktur-data-linked-list","A linked list is a linear chain of nodes, each holding data and a pointer to the next node. Here is what that chain looks like, what it costs next to an array, and the same minimal implementation in Java, Python, and JavaScript.","Linked list adalah rantai linear berisi node, dan setiap node menyimpan data plus pointer ke node berikutnya. Ini bentuk rantainya, ongkosnya dibanding array, dan implementasi paling sederhana dalam Java, Python, serta JavaScript.","2026-11-21T01:00:00.000Z",[218,219,220,221],"struktur-data","linked-list","pointer","konsep","\u002Fog\u002Ftentang-struktur-data-linked-list.png",{"id":224,"title":225,"title_id":226,"slug":227},28,"Linear Data Structures","Struktur Data Linear","asd-struktur-linear",[229],{"authors_id":230},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":232,"title":233,"title_id":234,"slug":235,"excerpt":236,"excerpt_id":237,"has_translation":12,"published_at":238,"reading_time":81,"hidden_from_listing":15,"tags":239,"thumbnail_image":21,"og_image":241,"series":242,"authors":247},47,"Why Understanding Algorithms and Data Structures Actually Matters","Pentingnya Memahami Algoritma dan Struktur Data","pentingnya-memahami-algoritma-dan-struktur-data","An algorithm is an ordered list of steps, and that order decides how expensive a problem is. The examples are on the back of an instant noodle packet, in a factorial, and behind ctrl+z.","Sebagai konsep fundamental, algoritma menentukan seberapa mahal sebuah masalah diselesaikan. Contohnya ada di bungkus mie instan, di bilangan faktorial, dan di tombol undo.","2026-11-19T01:00:00.000Z",[240,218,221,125],"algoritma","\u002Fog\u002Fpentingnya-memahami-algoritma-dan-struktur-data.png",{"id":243,"title":244,"title_id":245,"slug":246},27,"About Algorithms and Data Structures","Tentang Algoritma dan Struktur Data","asd-intro",[248],{"authors_id":249},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":251,"title":252,"title_id":253,"slug":254,"excerpt":255,"excerpt_id":256,"has_translation":12,"published_at":257,"reading_time":14,"hidden_from_listing":15,"tags":258,"thumbnail_image":21,"og_image":262,"series":263,"authors":264},108,"At 11 a.m. the AI Quota Runs Out, and the Work Stops There","Jam 11 siang kuota AI-nya habis, dan kerjaan berhenti di situ","jam-11-kuota-ai-habis-kerjaan-berhenti","The panel says the usage limit is reached and the work stops, not because it is finished. Three guesses explain it, and all three are wrong: the limit is shaped like a cliff, and the day is built on top of one tool.","Panel di layar bilang batas pemakaian tercapai, dan kerja berhenti di situ. Bukan karena selesai. Ada tiga tebakan kenapa ini terjadi, dan ketiganya salah: batasnya berbentuk tebing, dan harinya dibangun di atas satu alat.","2026-11-12T01:00:00.000Z",[259,105,260,261],"ai","rate-limit","produktivitas","\u002Fog\u002Fjam-11-kuota-ai-habis-kerjaan-berhenti.png",{"id":24,"title":25,"title_id":26,"slug":27},[265],{"authors_id":266},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":268,"title":269,"title_id":270,"slug":271,"excerpt":272,"excerpt_id":273,"has_translation":12,"published_at":274,"reading_time":275,"hidden_from_listing":15,"tags":276,"thumbnail_image":21,"og_image":280,"series":281,"authors":283},141,"A 6,000-Token Handbook and a 0% Cache Hit Rate","Buku Panduan 6.000 Token dan Hit Rate Cache 0%","cache-hit-rate-zero","Twenty-four turns, caching switched on, and a bill 21% higher than running with no cache at all. A support agent whose prompt looks static, and the three lines that quietly change it every turn.","Dua puluh empat giliran, caching menyala, dan tagihan 21% lebih tinggi daripada menjalankannya tanpa cache sama sekali. Agen support yang prompt-nya kelihatan statis, dan tiga baris yang diam-diam mengubahnya tiap giliran.","2026-11-08T01:20:00.000Z",3,[70,277,278,279],"llm","cache","api","\u002Fog\u002Fcache-hit-rate-zero.png",{"id":282,"title":69,"title_id":69,"slug":70},29,[284],{"authors_id":285},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":287,"title":288,"title_id":289,"slug":290,"excerpt":291,"excerpt_id":292,"has_translation":12,"published_at":293,"reading_time":294,"hidden_from_listing":15,"tags":295,"thumbnail_image":21,"og_image":297,"series":298,"authors":300},140,"What a Prompt Cache Actually Remembers","Apa yang Sebenarnya Diingat Prompt Cache","what-a-prompt-cache-remembers","Four providers cache the same bytes under four different rulebooks. The question that decides your bill: when the second request is longer than the first, does it reuse the whole prefix or only the part that was already written?","Empat provider meng-cache byte yang sama dengan empat buku aturan berbeda. Pertanyaan yang menentukan tagihanmu: saat permintaan kedua lebih panjang, apakah seluruh prefix dipakai ulang atau cuma bagian yang sudah pernah ditulis?","2026-11-08T01:10:00.000Z",6,[277,296,278,279],"ai-agent","\u002Fog\u002Fwhat-a-prompt-cache-remembers.png",{"id":299,"title":35,"title_id":36,"slug":37},8,[301],{"authors_id":302},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},{"id":74,"title":75,"title_id":76,"slug":77,"excerpt":78,"excerpt_id":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":15,"tags":82,"thumbnail_image":21,"og_image":86,"series":304,"authors":305},{"id":88,"title":89,"title_id":90,"slug":91},[306],{"authors_id":307},{"id":111,"name":112,"slug":113,"role":114,"profile_picture":21},[309,313,316,319,322,325],{"text":310,"depth":311,"id":312},"Four modules, one small program",2,"four-modules-one-small-program",{"text":314,"depth":311,"id":315},"Why json will not take a datetime","why-json-will-not-take-a-datetime",{"text":317,"depth":311,"id":318},"The checker","the-checker",{"text":320,"depth":311,"id":321},"The one line that fixes it","the-one-line-that-fixes-it",{"text":323,"depth":311,"id":324},"What the checker cannot see","what-the-checker-cannot-see",{"text":326,"depth":311,"id":327},"Before the next article","before-the-next-article",1794183172508]