# swehockey-schedule (Python) The Python twin of the Go crawler in the parent directory. Same layout profiles, same invariants, same canonical order — and, on purpose, **the same bytes**. ```console $ python3 -m swehockey.cli schedule --season 20961 --format csv | head -2 season_id,game_id,event_id,date,time,home,away,home_goals,away_goals,periods,spectators,venue 20961,90001001,1109941,2026-09-19,15:15,Frölunda HC,Växjö Lakers HC,5,4,1:1-1 2:2-1 3:2-2,12044,Scandinavium $ python3 -m swehockey.cli crawl --league 20961 # every season, as NDJSON $ python3 -m swehockey.cli leagues # 119 competitions $ python3 -m unittest discover -s tests -t . # 23 offline tests $ SWEHOCKEY_LIVE=1 python3 -m unittest discover -s tests -t . # + the site ``` With `pip install -e .` the same CLI is on `$PATH` as `swehockey-schedule`. ## Why it can agree byte for byte - Same `LAYOUTS` profiles, keyed on the header row; same per-field shape checks; same dual-render agreement for the modern layout; same carry-forward for the date heading and the round number; same invariants (periods sum to the score, ids unique, played ⇔ event link). - Same canonical sort by `(date, time, game_id)`, because the site orders rows by round and puts re-scheduled games where their round says. - The output carries only page-derived data — no fetch timestamp, no unordered iteration, fixed key order — and `json.dumps(..., indent=2, ensure_ascii=False)` is byte-compatible with Go's `json.Encoder`. - The tests read the **Go golden files** in `../testdata`, so the suite doubles as a cross-implementation check. Verified on the live page, not just on fixtures: ``` $ python3 -m swehockey.cli schedule --season 20961 --out py.json $ ../swehockey schedule -season 20961 -out go.json $ cmp py.json go.json && echo identical identical # 364 games, 102536 bytes # and the same for CSV: 365 identical lines # and for the whole archive: 27 seasons, 2119 games, 1779 played ``` ## No runtime dependencies The tree is built by a ~120-line builder on `html.parser`, because the only part of the HTML5 tree construction algorithm this crawler relies on is that stray `` elements land in an implicit `` — which `table_rows()` handles explicitly, exactly as in Go. One deliberate difference from a browser: the site writes empty cells as ``, and HTML5 ignores that slash, so a real browser leaves the cell open and swallows its siblings. This builder treats the tag as an empty element, which is what the author meant and what keeps `child_elements()` honest. That is also why the Go and Python trees agree on the header row (7 `th`, 6 non-empty labels) even though the Go parser closes the tag implicitly. ## Modules | module | what it does | | ----------------- | --------------------------------------------------------- | | `dom.py` | the HTML tree builder and the node helpers | | `parse.py` | layout profiles, the game reader, the invariants | | `seasons.py` | league and season discovery | | `fetch.py` | one GET per page, charset guard, skip-unchanged state file | | `output.py` | JSON, NDJSON, CSV, golden-file checking | | `cli.py` | `schedule`, `crawl`, `leagues` and the exit codes | Exit codes match the Go CLI: `0` ok, `1` error, `2` usage, `3` markup drift or golden mismatch, `4` timeout.