"""Generate a standalone HTML report from category,amount CSV input.""" import argparse import csv from decimal import Decimal, InvalidOperation from html import escape from pathlib import Path def totals(source): reader = csv.DictReader(source) if not {"category", "amount"}.issubset(reader.fieldnames or []): raise ValueError("CSV requires category and amount columns") result = {} for line, row in enumerate(reader, 2): category = (row.get("category") or "").strip() try: amount = Decimal(row.get("amount") or "") except InvalidOperation as error: raise ValueError(f"Invalid amount on line {line}") from error if not category or not amount.is_finite(): raise ValueError(f"Invalid category or amount on line {line}") result[category] = result.get(category, Decimal(0)) + amount return result def render(values): rows = "".join(f"{escape(key)}{value:,.2f}" for key, value in sorted(values.items())) return """Category report

Category report

""" + rows + f"
Amounts grouped by category
CategoryAmount
Total{sum(values.values(), Decimal(0)):,.2f}
" if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input", type=Path) parser.add_argument("--output", type=Path, default=Path("report.html")) args = parser.parse_args() try: with args.input.open(encoding="utf-8-sig", newline="") as source: html = render(totals(source)) if args.input.resolve() == args.output.resolve(): parser.error("Output must not overwrite the input CSV") args.output.write_text(html, encoding="utf-8") except (ValueError, OSError) as error: parser.error(str(error)) print(f"Created {args.output}")