When I first wrote about firefly-reports, the install instructions were a git clone, a cd two levels deep, and a loose python demo.py. Fine for a weekend project. Not fine for something you’re asking strangers to point at their financial data.
Three releases later it’s pip install firefly-iii-reports. This is the short version of what happened in between — mostly the parts that went wrong.
Packaging, briefly
The distribution name is firefly-iii-reports because firefly-reports was already taken on PyPI by an unrelated project. The command you actually type is still firefly-reports, via [project.scripts].
Lesson learned: Check PyPI before you name anything.
Internal imports went from flat (from client import FireflyClient) to package-qualified (from firefly_reports.client import ...), which is mandatory once the code lives in site-packages. Running from source is now python -m firefly_reports.main rather than python main.py.
Publishing uses OIDC Trusted Publishing: no PyPI API token in GitHub secrets, just permissions: id-token: write and a publisher registered on PyPI’s side. One nice side effect is that PyPI records PEP 740 attestations
, so every wheel is traceable to a workflow file at a specific commit.
The same workflow builds a single-file Windows executable with PyInstaller, because “self-hosted” doesn’t imply “has a working Python toolchain”.
That executable is where it got interesting (so to say).
Two bugs, zero failing tests
Version 1.0.1 shipped green: ruff, mypy, 175 tests on python 3.11 and 3.12, pip-audit clean, Windows build smoke-tested. Yet the binary was broken twice over.
One: --collect-data collected nothing. Translations ship as TOML files, so PyInstaller can’t find them by following imports. I used --collect-data firefly_reports, which resolves data files through the installed package metadata. In my build job it resolved to an empty set — and finding nothing isn’t an error, it’s a no-op. Exit code 0, valid executable, zero translations inside. Every run past --help died with FileNotFoundError.
The fix was to name the files explicitly:
--add-data "firefly_reports/translations/*.toml;firefly_reports/translations"
The real fix was the smoke test. Mine ran --help and checked the exit code — but argparse handles --help before any translation loading, so it passed on a bundle that could do nothing else. If your smoke test only checks --help, it’s testing argparse, not your program.
Two: the euro sign. Windows consoles still default to legacy codepages, and sys.stdout inherits that encoding. Help text containing € and — produced a hard UnicodeEncodeError on startup for affected users. An encoding error in the output path doesn’t degrade — it unwinds and kills the process. First interaction with the tool: a traceback.
Three lines, before anything prints:
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
stream.reconfigure(errors="replace")
The pattern
Both bugs share a common shape:
Your CI tests the source you wrote, in the environment you wrote it in. Other users run an application in an environment you’ve never seen.
Ubuntu runner, UTF-8 locale, editable install, imports resolving from your working tree — four assumptions baked into every green check mark, all four violated by a PyInstaller bundle on a legacy-codepage console. Neither bug was a logic error. Packaging errors only exist in the packaged thing.
Build the artifact in CI, then run the artifact in CI, and make the test do something a trivial code path can’t fake.
Where it stands
pip install firefly-iii-reports
# zero setup, no Firefly III instance needed
python -m firefly_reports.demo --out ./output --year 2025 --lang en
# against a real instance
firefly-reports \
--url https://your-firefly-instance.example.com \
--year 2025 \
--owner "Your Name" --out ./output
firefly-reports init # persistent config, recommended
Python 3.11+. Windows users can skip all of it and grab the executable from the latest release . Docs, all CLI flags and a reference for the 26 reports live on the wiki . GPL v3, issues and PRs welcome.