Skip to content

feat: PDF export via headless Chrome with Playwright fallback - #15

Merged
YashvantHange merged 1 commit into
mainfrom
feat/pdf-export
Jul 16, 2026
Merged

feat: PDF export via headless Chrome with Playwright fallback#15
YashvantHange merged 1 commit into
mainfrom
feat/pdf-export

Conversation

@YashvantHange

Copy link
Copy Markdown
Owner

Adds investo analyze --pdf, rendering the RFC 2 research note to PDF. Third of four RFCs; depends on #14 (merged).

No new required dependency

PDF is genuinely optional at the dependency level. investo.export.html_to_pdf tries, in order:

  1. A system Chrome / Edge / Chromium / Brave (discovered by platform path or PATH, or INVESTO_CHROME) — the common case, zero setup.
  2. Playwright-managed Chromium, if pip install 'investo[pdf]' and playwright install chromium have been run.
  3. Otherwise a PdfExportError naming all three remedies — while the .html is still written, so a failure leaves a usable report, not an empty hand.

Four headless-Chrome details that were each a real bug

Commented at their site in _chrome_pdf, because every one is a silent failure:

  • TemporaryDirectory + a file inside it, not NamedTemporaryFile — on Windows a still-open NamedTemporaryFile can't be reopened by path, which is exactly what Chrome must do.
  • resolve().as_uri() for the URL — yields file:///C:/… with spaces percent-escaped; "file://" + str(path) gets both wrong.
  • A mandatory throwaway --user-data-dir — without it, headless can attach to an already-running browser and silently produce nothing (the docs: add Cursor Directory listing (badge + one-click install) #1 "works in CI, not locally" cause).
  • Verify the file exists and is non-empty — do not trust the exit code. --print-to-pdf can exit 0 having written nothing. There's a dedicated test for this case.

CLI flags now compose

--json, --html, --pdf each do one thing and combine — previously --html silently suppressed --json. Bare --html/--pdf write investo-<SYMBOL>-<date>.<ext> (ticker sanitised — M&M.NS is real), parents are created, and a PDF failure exits 2 with the sidecar kept and the reason on stderr.

Verification

ruff + mypy clean; 223 tests pass (was 198). The new test_export.py and test_cli.py never launch a real browser — every test stubs subprocess.run or Playwright.

The real proof, though, is a live run: investo analyze "KPIT Technologies" --pdf produced a valid 7-page A4 PDF (%PDF-1.4, MediaBox 595×842pt, embedded fonts, clean %%EOF) through headless Chrome on this machine, with the .html sidecar alongside.

⚠️ Reviewer note: Chrome's --print-to-pdf has no @page margin-box support, so there are no CSS page numbers — running header/footer only (via position: fixed). That's a documented engine limit, not an omission.

No breaking changes

--pdf is new; the pdf extra is optional; new config fields have defaults.

Add `investo analyze --pdf [FILE]`, rendering the research note to PDF with no new required
dependency. investo.export shells out to a system Chrome/Edge/Chromium/Brave when one is present
(the common case), falls back to a Playwright-managed Chromium if that package is installed, and
otherwise raises with a message naming all three fixes -- while the .html is always written first, so
a failed export still leaves a usable report on disk. Four headless-Chrome details that were each a
real bug are handled and commented at their site: a TemporaryDirectory rather than a still-open
NamedTemporaryFile (unreopenable by path on Windows), resolve().as_uri() for a correctly escaped
file:// URL, a mandatory throwaway --user-data-dir so headless never attaches to a running profile,
and verifying the output file exists and is non-empty rather than trusting Chrome's exit code.

Rework the analyze output flags to compose: --json, --html and --pdf each do one thing and combine
cleanly, where --html previously suppressed --json with no signal. Bare --html/--pdf write
investo-<SYMBOL>-<date>.<ext> (ticker sanitised, since M&M.NS is real), parents are created, and a
PDF-engine failure exits 2 with the sidecar retained and the reason on stderr. Adds the pdf optional
extra, INVESTO_CHROME / INVESTO_PDF_TIMEOUT / INVESTO_EXPORT_DIR config, and offline tests that stub
the browser -- covering the exit-zero-but-no-file case, the Windows file-URI escaping, flag
composition and the exit-2 contract -- none of which launch a real browser. Verified by generating a
7-page A4 PDF from live KPIT data through headless Chrome.
@YashvantHange
YashvantHange merged commit 633e212 into main Jul 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant