Four back-office automations, in Python, with their test suites

Four small programs that do the parts of a small business that never get built: a lead that arrives and dies in an inbox, a weekly number nobody assembles, an invoice that goes unpaid because no one wants to be the one who asks, and a listing page that has to be read by hand every Monday. Each is one file, standard library only, with its own test suite and a written setup guide.

What they share: none of them sends anything. Every one of them ends its run by writing a numbered draft to a folder, addressed to nobody, which a human reads and then sends themselves. That is a design decision, not a missing feature: the embarrassing part of an automation is the thing it did at 3am while nobody was watching, and these cannot do that.

Proof: 201 tests, and the command that repeats them

Measured 2026-11-14 on Python 3.12.3 with pytest 9.1.1, against the copy of the code this page serves under /browse/, not against a build somewhere else.

$ cd src
$ python3 -m pytest recipes/r1 recipes/r2 recipes/r3 recipes/r4 tests -q
============================= 201 passed in 2.87s ==============================
folderwhat it istestsprogram
r1lead capture: form or inbox to sheet, plus a reply draft32 passed lead_capture_to_sheets.py
r2weekly report: two weeks of sales side by side, in Markdown and HTML88 passed weekly_report.py
r3invoice reminders: who is overdue, and the drafts, escalating by stage23 passed invoice_reminders.py
r4listing scraper: a page of listings to a clean CSV or JSON, with the cap made visible58 passed scrape_listings.py
total on this page201 passed

A fifth recipe, a cost-per-unit pricing tool, lives in the same tree and brings the family to 271 passing tests. It is not published here: its licence file names an author, and a page like this one carries one name. Ask and I will send that recipe too.

On a machine whose login session carries a display, a bare test run is refused by this project's own guard so a test can never reach someone's screen; the command above was run inside that box, and needs no such wrapper on an ordinary machine.

What each one actually does, and what it refuses to do

R1, lead capture to a sheet

Reads new leads from a Google Sheet, a JSON file or a CSV file, de-duplicates them against what is already on the sheet, appends the survivors, and writes one reply draft per lead into an outbox folder. It writes only to the sheet named in its config. The test suite is strict about the part people skip: the workbook is read as text and every column is type-checked, because a sheet is a loose pile of typed-in cells, not a schema, and a date typed as 13/04/2026 by one person is April 13 by another.

$ python3 recipes/r1/lead_capture_to_sheets.py --leads-file samples/incoming/ --dry-run
sample row lead  amara@example.com   name=Amara  source=website form   ok

R2, the weekly report

Two weeks of sales rows in, a report out, in Markdown and in HTML, with the totals that matter and the difference between the two weeks stated rather than implied. Its 88 tests are mostly about arithmetic that looks trivial and is not: a "refunds" column is a number in one export and a blank in another, a row dated between the two weeks belongs to neither, and a report that divides by a week that contains nothing must say so instead of printing an infinity.

R3, invoice reminders

Takes invoices and payments and answers who is overdue, by how much, and which stage of asking they are on. It is deliberately conservative: a credit note or a payment applied to a different invoice does not count as paid, a disputed invoice is left alone, and a zero-amount invoice is ignored rather than escalated. It writes numbered drafts and stops there.

R4, the listing scraper

A generic list reader: give it a page and a description of where the rows are, and it produces a CSV or JSON with a provenance header. Its rule is that nothing is dropped quietly: rows that fail to parse come back marked, a result cap that bit is reported as having bitten, and if nothing at all can be kept it exits with a named reason instead of writing an empty file and looking successful.

The licences and the terms

What I would want to say before selling one of these