"""logreport, refactored — the legacy log summariser rebuilt from pure functions. What it does Same job and byte-for-byte the same output as add-tests-to-a-legacy-script-with-an-agent.py: counts log lines per level, reports the first and last timestamp seen, warns on malformed lines. What changed (structure only, behaviour preserved on purpose) * No module-level state. parse_line() returns an Entry or None; summarise() folds entries into a Summary; render() turns a Summary into text; run() composes them. Each function is testable in isolation. * The DD/MM/YY reformatting and the string comparison that picks first/last are kept exactly as they were, bug included, because a refactor commit must not change behaviour. The fix is a separate, one-line commit that flips one pinned test (see the tutorial). Inputs python refactor-with-an-agent-characterization-tests.py or run(lines) -> str from Python. Tests: python -m unittest refactor-with-an-agent-characterization-tests_test.py Python 3.13, standard library only. """ from __future__ import annotations import sys from dataclasses import dataclass, field from typing import Iterable @dataclass(frozen=True) class Entry: stamp: str # "DD/MM/YY HH:MM:SS", the legacy display form level: str @dataclass class Summary: total: int = 0 bad: int = 0 counts: dict[str, int] = field(default_factory=dict) first: str | None = None last: str | None = None warnings: list[str] = field(default_factory=list) def fmt_date(iso: str) -> str: """2026-09-05 -> 05/09/26 (legacy display form, kept as-is).""" return iso[8:10] + "/" + iso[5:7] + "/" + iso[2:4] def parse_line(line: str) -> Entry | None: """Return an Entry for a well-formed line, None for a malformed one.""" parts = line.rstrip("\n").split(" ", 3) if len(parts) < 4 or len(parts[0]) != 10 or len(parts[1]) != 8: return None return Entry(stamp=fmt_date(parts[0]) + " " + parts[1], level=parts[2]) def summarise(lines: Iterable[str]) -> Summary: s = Summary() for line in lines: s.total += 1 entry = parse_line(line) if entry is None: s.bad += 1 s.warnings.append("WARN skipping line %d: %r" % (s.total, line.rstrip("\n"))) continue s.counts[entry.level] = s.counts.get(entry.level, 0) + 1 # Legacy behaviour preserved: lexical comparison of the DD/MM/YY form. if s.first is None or entry.stamp < s.first: s.first = entry.stamp if s.last is None or entry.stamp > s.last: s.last = entry.stamp return s def render(s: Summary) -> str: lines = list(s.warnings) lines.append("lines: %d (skipped %d)" % (s.total, s.bad)) for level in sorted(s.counts, key=lambda k: (-s.counts[k], k)): lines.append(" %-8s %d" % (level, s.counts[level])) if s.first is not None: lines.append("first: " + s.first) lines.append("last: " + s.last) return "\n".join(lines) + "\n" def run(lines: Iterable[str]) -> str: return render(summarise(lines)) def main(argv: list[str]) -> int: if len(argv) != 2: print("usage: logreport ", file=sys.stderr) return 1 with open(argv[1], encoding="utf-8") as fh: sys.stdout.write(run(fh)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))