Skip to content

feat: add subscribers export — async CSV export via /api/v1/publication_export - #11

Open
griffincapmisc wants to merge 2 commits into
griffinwork40:mainfrom
griffincapmisc:feat/csv-export
Open

feat: add subscribers export — async CSV export via /api/v1/publication_export#11
griffincapmisc wants to merge 2 commits into
griffinwork40:mainfrom
griffincapmisc:feat/csv-export

Conversation

@griffincapmisc

Copy link
Copy Markdown

What

Adds substack subscribers export command that triggers an async CSV export job via the Substack dashboard API, polls until complete, and downloads the result.

200–300× faster than paginated stats for full subscriber pulls (seconds vs minutes for 22K+ subscribers).

New functions in read.py

  • trigger_csv_export(client)POST /api/v1/publication_export {} to kick off the job
  • poll_csv_export(client, timeout_s=300)GET /api/v1/publication_export loop until status=="complete" and a download URL is present; tolerates bare-list / {exports:[...]} / single-job-dict response shapes
  • download_csv(client, url) — new SubstackClient.download(url) method for raw-bytes downloads from absolute URLs (S3 pre-signed links)

CLI usage

# Stream CSV to stdout
substack subscribers export

# Write to file
substack subscribers export --output subscribers.csv

# Custom timeout (default 300s)
substack subscribers export --timeout 600

Tests

15 new tests in tests/test_read_csv_export.py covering happy paths, envelope-shape variants, polling retry, timeout, terminal failure, unexpected shapes, and CLI end-to-end.

332/332 tests passing.

Note

The trigger POST body shape ({}) is based on community documentation. If the endpoint returns 400, the likely alternatives are {"type": "subscribers"} or {"export_type": "subscribers"} — noted as a TODO in the code. Verification against the live Substack dashboard DevTools would confirm the exact body.

Depends on: #10 (pagination feature)

griffincapmisc and others added 2 commits August 22, 2026 14:41
Co-authored-by: Capital Mischief <capitalmischief@CapitalacStudio.lan>
Add `substack subscribers export` — a new command that triggers Substack's
async CSV export job, polls until complete, and downloads the result. This is
significantly faster than paginating `subscriber-stats` 25–100 rows at a time
for large subscriber lists (gap griffinwork40#4 in FEATURE-GAPS.md).

## What changed

### client.py
- Add `SubstackClient.download(url: str) -> bytes`: fetches an absolute URL
  (e.g. pre-signed S3 link) and returns raw bytes. Throttled, raises
  SubstackApiError on non-2xx or transport errors.

### read.py
- Add `trigger_csv_export(client)`: POST /api/v1/publication_export with
  empty body {}. Returns the raw API response (job object).
  TODO: body shape unverified — empty {} tried first per spec; see comment if
  server returns 400.
- Add `poll_csv_export(client, timeout_s=300)`: GET /api/v1/publication_export
  in a polling loop until a complete job with download_url appears. Tolerates
  bare-list, {exports:[...]}, and single-job-dict response shapes. Raises
  SubstackApiError on terminal failure or timeout.
- Add `download_csv(client, download_url)`: delegates to client.download().
- Add `@subscribers_app.command("export")` CLI command:
    `substack subscribers export [--output FILE] [--timeout N]`
  Writes CSV to stdout by default; --output FILE writes to disk + emits JSON
  confirmation. Progress printed to stderr throughout.

### tests/test_read_csv_export.py (new, 15 tests)
- trigger: POST body shape, API error propagation
- poll: immediate-complete, envelope shapes, multi-job recency, pending-then-
  complete retry, failed-job terminal, timeout, unexpected-shape errors
- download: happy path, 404 propagation
- CLI: stdout path, --output FILE path, trigger-failure exit-1

## Test results
332 passed, 0 failed (full suite)

## Confidence note
POST /api/v1/publication_export endpoint is community-confirmed; the trigger
body shape ({}) and exact response field names (status, download_url) are
**medium confidence** — unverified against a live response. Re-probe with
DevTools capture on the Substack dashboard export page before relying on this
in production. See FEATURE-GAPS.md griffinwork40#4 and references/substack-api.md for
the full confidence note.
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