[{"data":1,"prerenderedAt":328},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-python-stdlib-id":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":75,"slug":76,"excerpt":77,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":84,"series":85,"authors":198,"seoTitle":21,"seoDescription":21,"body_locale":201,"available_locales":202,"body":204,"takeway":205,"prerequisite":21},143,"Python stdlib: datetime yang Ditolak json.dumps","python-stdlib","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,[81,82,83],"python","stdlib","cli","\u002Fog\u002Fpython-stdlib.png",{"id":86,"title":87,"title_id":88,"slug":89,"description":90,"description_id":91,"articles":92,"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.",[93,113,129,144,160],{"id":94,"title":95,"title_id":96,"slug":97,"excerpt":98,"excerpt_id":99,"has_translation":12,"published_at":100,"reading_time":101,"hidden_from_listing":15,"tags":102,"thumbnail_image":21,"og_image":104,"series":105,"authors":106},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,[81,49,103,83],"tooling","\u002Fog\u002Fmenulis-dan-menjalankan-script-python.png",{"id":86,"title":87,"title_id":88,"slug":89},[107],{"authors_id":108},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},1,"Daffa Izzuddin","izzudd","Writer",{"id":114,"title":115,"title_id":116,"slug":117,"excerpt":118,"excerpt_id":119,"has_translation":12,"published_at":120,"reading_time":101,"hidden_from_listing":15,"tags":121,"thumbnail_image":21,"og_image":124,"series":125,"authors":126},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",[81,122,123],"input-output","dasar","\u002Fog\u002Fsistem-input-dan-output-pada-python-3.png",{"id":86,"title":87,"title_id":88,"slug":89},[127],{"authors_id":128},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":130,"title":131,"title_id":132,"slug":133,"excerpt":134,"excerpt_id":135,"has_translation":12,"published_at":136,"reading_time":79,"hidden_from_listing":15,"tags":137,"thumbnail_image":21,"og_image":139,"series":140,"authors":141},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",[81,138],"modules","\u002Fog\u002Fpython-modules.png",{"id":86,"title":87,"title_id":88,"slug":89},[142],{"authors_id":143},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":145,"title":146,"title_id":147,"slug":148,"excerpt":149,"excerpt_id":150,"has_translation":12,"published_at":151,"reading_time":14,"hidden_from_listing":15,"tags":152,"thumbnail_image":21,"og_image":155,"series":156,"authors":157},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",[81,153,154],"venv","pip","\u002Fog\u002Fpython-venv-pip.png",{"id":86,"title":87,"title_id":88,"slug":89},[158],{"authors_id":159},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":74,"title":161,"title_id":75,"slug":76,"excerpt":162,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":84,"series":163,"authors":164},"Python stdlib: the datetime json.dumps Refused to Write","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.",{"id":86,"title":87,"title_id":88,"slug":89},[165],{"authors_id":166},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},[],{"id":169,"title":52,"title_id":53,"slug":54,"parent_series":170,"children_series":171},10,{"id":109,"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":86,"title":87,"title_id":88,"slug":89,"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":109,"name":110,"slug":111,"role":112,"profile_picture":21},"id",[203,201],"en","Kamu mengarahkan skrip ke folder berisi tiga file log. Skrip membacanya, tidak mencetak apa pun, lalu mati dengan TypeError di baris yang menulis laporan. Dua baris terakhir error-nya begini:\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\nBaris kedua menyebut key dict yang menyimpan nilai yang ditolak json. Angka-angka di sini berasal dari Python 3.14.9 di Linux.\n\n## Empat modul, satu program kecil\n\n`daily.py` menjumlahkan menit per hari dari folder berisi log sesi. Masing-masing dari empat modul standard library punya satu tugas.\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) membaca satu argumen, yaitu folder. Kamu tidak pernah meminta `-h`, tapi opsi itu tetap ada:\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) membangun path. `glob(\"*.json\")` mendaftar file log, dan `read_text()` memberimu tiap file sebagai string. [`json`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fjson.html) mengubah teks itu menjadi list dan dict, lalu mengubah laporan kembali menjadi teks. [`datetime`](https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fdatetime.html) menyediakan timestamp.\n\nGlob itu juga cocok dengan `summary.json`, file yang akan segera ditulis skrip. `if` melewatinya. Tanpa itu, run kedua akan membaca outputnya sendiri sebagai log sesi. `- 1` di baris terakhir mengeluarkannya dari hitungan file, karena saat itu file tersebut sudah ada.\n\n## Kenapa json menolak datetime\n\nModul `json` menulis daftar tipe yang pendek: string, number, boolean, `None`, list, dan dict. `datetime` tidak ada di daftar itu.\n\nSaat `json.dumps` bertemu tipe di luar daftar itu, ia melempar `TypeError`. Ia tidak menebak format. Tanggal bisa ditulis dengan banyak cara, jadi seseorang harus memilih satu. Di sini pilihannya ISO 8601: tahun, bulan, hari, huruf `T`, lalu waktu. Cara resmi untuk mendapatkan string itu adalah `isoformat()`.\n\n## Checker-nya\n\n`check_daily.py` adalah skrip biasa tanpa framework tes. Jalankan dari folder yang berisi `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\nIa menulis folder fixture yang tetap: tiga file, dengan dua sesi pada 2026-11-06 di salah satunya. Ia menjalankan `daily.py` pada folder itu, lalu memeriksa apa yang dicetak skrip dan JSON yang ditulisnya. Terhadap skrip awal, hasilnya merah:\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\nExit code-nya 1, dan teks setelah panah adalah baris error yang diambil checker dari stderr. Ia sengaja berhenti di kegagalan pertama. Skrip yang crash tidak menulis `summary.json`, jadi pemeriksaan lain tidak punya apa-apa untuk dilihat.\n\n## Satu baris yang memperbaikinya\n\n`datetime.now()` mengembalikan waktu lokal mesin tanpa zona waktu. Tambahkan `.isoformat(timespec=\"seconds\")` dan kamu mendapat string, yang sudah bisa ditulis json. Hanya baris `generated_at` yang berubah.\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\nRun nyata pada folder berisi tiga file log mencetak persis `4 days from 3 files`. Checker setuju, dengan 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\n`summary.json` dari run nyata itu diawali timestamp, lalu empat baris hari:\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\nAngka 60 adalah hasil penjumlahan dari file yang berisi dua sesi, 25 dan 35 menit.\n\n## Yang tidak bisa dilihat checker\n\nUbah `timespec=\"seconds\"` menjadi `timespec=\"hours\"` lalu jalankan checker lagi. Ia tetap mencetak `9 of 9 checks passed` dan keluar dengan kode 0. `datetime.fromisoformat` menerima string yang lebih pendek itu. Pemeriksaan tanggal membuktikan timestamp bisa di-parse, bukan bahwa timestamp itu punya presisi yang kamu inginkan.\n\n## Sebelum artikel berikutnya\n\nHapus `sorted()` di sekitar `total.items()` lalu jalankan checker sekali lagi. Kamu mendapat `8 of 9 checks passed` dan 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\nDict menyimpan urutan penyisipan, jadi baris keluar dalam urutan file dibaca. Urutan baris yang tidak kamu sortir adalah urutan yang tidak kamu pilih.\n","- `json.dumps` menulis sekumpulan tipe dasar yang tetap, dan `datetime` bukan salah satunya, jadi encoder melempar error alih-alih memilih format untukmu.\n- `datetime.now()` mengembalikan waktu lokal mesin tanpa zona waktu, dan `isoformat(timespec=\"seconds\")` adalah cara resmi di dokumentasi untuk mengubahnya menjadi string yang bisa ditulis json.\n- `argparse` menjawab `-h` tanpa diminta, `pathlib` mengubah folder menjadi path dan teks, dan `json` mengubah teks menjadi list dan dict.\n- Checker yang hanya memastikan stamp bisa di-parse tidak bisa membedakan `timespec=\"hours\"` dari `timespec=\"seconds\"`, jadi ia membuktikan bentuknya, bukan presisinya.","\u003Cp>Kamu mengarahkan skrip ke folder berisi tiga file log. Skrip membacanya, tidak mencetak apa pun, lalu mati dengan TypeError di baris yang menulis laporan. Dua baris terakhir error-nya begini:\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=\"Salin\">\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\">Salin\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>Baris kedua menyebut key dict yang menyimpan nilai yang ditolak json. Angka-angka di sini berasal dari Python 3.14.9 di Linux.\u003C\u002Fp>\n\u003Ch2 id=\"empat-modul-satu-program-kecil\">Empat modul, satu program kecil\u003C\u002Fh2>\n\u003Cp>\u003Ccode>daily.py\u003C\u002Fcode> menjumlahkan menit per hari dari folder berisi log sesi. Masing-masing dari empat modul standard library punya satu tugas.\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=\"Salin\">\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\">Salin\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> membaca satu argumen, yaitu folder. Kamu tidak pernah meminta \u003Ccode>-h\u003C\u002Fcode>, tapi opsi itu tetap ada:\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=\"Salin\">\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\">Salin\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> membangun path. \u003Ccode>glob(&quot;*.json&quot;)\u003C\u002Fcode> mendaftar file log, dan \u003Ccode>read_text()\u003C\u002Fcode> memberimu tiap file sebagai string. \u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fjson.html\">\u003Ccode>json\u003C\u002Fcode>\u003C\u002Fa> mengubah teks itu menjadi list dan dict, lalu mengubah laporan kembali menjadi teks. \u003Ca href=\"https:\u002F\u002Fdocs.python.org\u002F3\u002Flibrary\u002Fdatetime.html\">\u003Ccode>datetime\u003C\u002Fcode>\u003C\u002Fa> menyediakan timestamp.\u003C\u002Fp>\n\u003Cp>Glob itu juga cocok dengan \u003Ccode>summary.json\u003C\u002Fcode>, file yang akan segera ditulis skrip. \u003Ccode>if\u003C\u002Fcode> melewatinya. Tanpa itu, run kedua akan membaca outputnya sendiri sebagai log sesi. \u003Ccode>- 1\u003C\u002Fcode> di baris terakhir mengeluarkannya dari hitungan file, karena saat itu file tersebut sudah ada.\u003C\u002Fp>\n\u003Ch2 id=\"kenapa-json-menolak-datetime\">Kenapa json menolak datetime\u003C\u002Fh2>\n\u003Cp>Modul \u003Ccode>json\u003C\u002Fcode> menulis daftar tipe yang pendek: string, number, boolean, \u003Ccode>None\u003C\u002Fcode>, list, dan dict. \u003Ccode>datetime\u003C\u002Fcode> tidak ada di daftar itu.\u003C\u002Fp>\n\u003Cp>Saat \u003Ccode>json.dumps\u003C\u002Fcode> bertemu tipe di luar daftar itu, ia melempar \u003Ccode>TypeError\u003C\u002Fcode>. Ia tidak menebak format. Tanggal bisa ditulis dengan banyak cara, jadi seseorang harus memilih satu. Di sini pilihannya ISO 8601: tahun, bulan, hari, huruf \u003Ccode>T\u003C\u002Fcode>, lalu waktu. Cara resmi untuk mendapatkan string itu adalah \u003Ccode>isoformat()\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"checker-nya\">Checker-nya\u003C\u002Fh2>\n\u003Cp>\u003Ccode>check_daily.py\u003C\u002Fcode> adalah skrip biasa tanpa framework tes. Jalankan dari folder yang berisi \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=\"Salin\">\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\">Salin\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>Ia menulis folder fixture yang tetap: tiga file, dengan dua sesi pada 2026-11-06 di salah satunya. Ia menjalankan \u003Ccode>daily.py\u003C\u002Fcode> pada folder itu, lalu memeriksa apa yang dicetak skrip dan JSON yang ditulisnya. Terhadap skrip awal, hasilnya merah:\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=\"Salin\">\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\">Salin\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>Exit code-nya 1, dan teks setelah panah adalah baris error yang diambil checker dari stderr. Ia sengaja berhenti di kegagalan pertama. Skrip yang crash tidak menulis \u003Ccode>summary.json\u003C\u002Fcode>, jadi pemeriksaan lain tidak punya apa-apa untuk dilihat.\u003C\u002Fp>\n\u003Ch2 id=\"satu-baris-yang-memperbaikinya\">Satu baris yang memperbaikinya\u003C\u002Fh2>\n\u003Cp>\u003Ccode>datetime.now()\u003C\u002Fcode> mengembalikan waktu lokal mesin tanpa zona waktu. Tambahkan \u003Ccode>.isoformat(timespec=&quot;seconds&quot;)\u003C\u002Fcode> dan kamu mendapat string, yang sudah bisa ditulis json. Hanya baris \u003Ccode>generated_at\u003C\u002Fcode> yang berubah.\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=\"Salin\">\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\">Salin\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>Run nyata pada folder berisi tiga file log mencetak persis \u003Ccode>4 days from 3 files\u003C\u002Fcode>. Checker setuju, dengan 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=\"Salin\">\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\">Salin\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>\u003Ccode>summary.json\u003C\u002Fcode> dari run nyata itu diawali timestamp, lalu empat baris hari:\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=\"Salin\">\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\">Salin\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>Angka 60 adalah hasil penjumlahan dari file yang berisi dua sesi, 25 dan 35 menit.\u003C\u002Fp>\n\u003Ch2 id=\"yang-tidak-bisa-dilihat-checker\">Yang tidak bisa dilihat checker\u003C\u002Fh2>\n\u003Cp>Ubah \u003Ccode>timespec=&quot;seconds&quot;\u003C\u002Fcode> menjadi \u003Ccode>timespec=&quot;hours&quot;\u003C\u002Fcode> lalu jalankan checker lagi. Ia tetap mencetak \u003Ccode>9 of 9 checks passed\u003C\u002Fcode> dan keluar dengan kode 0. \u003Ccode>datetime.fromisoformat\u003C\u002Fcode> menerima string yang lebih pendek itu. Pemeriksaan tanggal membuktikan timestamp bisa di-parse, bukan bahwa timestamp itu punya presisi yang kamu inginkan.\u003C\u002Fp>\n\u003Ch2 id=\"sebelum-artikel-berikutnya\">Sebelum artikel berikutnya\u003C\u002Fh2>\n\u003Cp>Hapus \u003Ccode>sorted()\u003C\u002Fcode> di sekitar \u003Ccode>total.items()\u003C\u002Fcode> lalu jalankan checker sekali lagi. Kamu mendapat \u003Ccode>8 of 9 checks passed\u003C\u002Fcode> dan 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=\"Salin\">\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\">Salin\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>Dict menyimpan urutan penyisipan, jadi baris keluar dalam urutan file dibaca. Urutan baris yang tidak kamu sortir adalah urutan yang tidak kamu pilih.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Ccode>json.dumps\u003C\u002Fcode> menulis sekumpulan tipe dasar yang tetap, dan \u003Ccode>datetime\u003C\u002Fcode> bukan salah satunya, jadi encoder melempar error alih-alih memilih format untukmu.\u003C\u002Fli>\n\u003Cli>\u003Ccode>datetime.now()\u003C\u002Fcode> mengembalikan waktu lokal mesin tanpa zona waktu, dan \u003Ccode>isoformat(timespec=&quot;seconds&quot;)\u003C\u002Fcode> adalah cara resmi di dokumentasi untuk mengubahnya menjadi string yang bisa ditulis json.\u003C\u002Fli>\n\u003Cli>\u003Ccode>argparse\u003C\u002Fcode> menjawab \u003Ccode>-h\u003C\u002Fcode> tanpa diminta, \u003Ccode>pathlib\u003C\u002Fcode> mengubah folder menjadi path dan teks, dan \u003Ccode>json\u003C\u002Fcode> mengubah teks menjadi list dan dict.\u003C\u002Fli>\n\u003Cli>Checker yang hanya memastikan stamp bisa di-parse tidak bisa membedakan \u003Ccode>timespec=&quot;hours&quot;\u003C\u002Fcode> dari \u003Ccode>timespec=&quot;seconds&quot;\u003C\u002Fcode>, jadi ia membuktikan bentuknya, bukan presisinya.\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":101,"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":109,"name":110,"slug":111,"role":112,"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":79,"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,123],"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":109,"name":110,"slug":111,"role":112,"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,103,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":109,"name":110,"slug":111,"role":112,"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":109,"name":110,"slug":111,"role":112,"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":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":74,"title":161,"title_id":75,"slug":76,"excerpt":162,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":84,"series":304,"authors":305},{"id":86,"title":87,"title_id":88,"slug":89},[306],{"authors_id":307},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},[309,313,316,319,322,325],{"text":310,"depth":311,"id":312},"Empat modul, satu program kecil",2,"empat-modul-satu-program-kecil",{"text":314,"depth":311,"id":315},"Kenapa json menolak datetime","kenapa-json-menolak-datetime",{"text":317,"depth":311,"id":318},"Checker-nya","checker-nya",{"text":320,"depth":311,"id":321},"Satu baris yang memperbaikinya","satu-baris-yang-memperbaikinya",{"text":323,"depth":311,"id":324},"Yang tidak bisa dilihat checker","yang-tidak-bisa-dilihat-checker",{"text":326,"depth":311,"id":327},"Sebelum artikel berikutnya","sebelum-artikel-berikutnya",1794183172544]