-
-
Notifications
You must be signed in to change notification settings - Fork 28
Permalink
Choose a base ref
{{ refName }}
default
Choose a head ref
{{ refName }}
default
Checking mergeability…
Don’t worry, you can still create the pull request.
Comparing changes
Choose two branches to see what’s changed or to start a new pull request.
If you need to, you can also or
learn more about diff comparisons.
Open a pull request
Create a new pull request by comparing changes across two branches. If you need to, you can also .
Learn more about diff comparisons here.
base repository: ma2za/python-substack
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Could not load branches
Nothing to show
Loading
Could not load tags
Nothing to show
{{ refName }}
default
Loading
...
head repository: ericpberry/python-substack
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Could not load branches
Nothing to show
Loading
Could not load tags
Nothing to show
{{ refName }}
default
Loading
- 5 commits
- 19 files changed
- 2 contributors
Commits on May 26, 2026
-
chore: add scripts for sandbox verification and HAR extraction
- scripts/verify_baseline.py exercises auth, draft create/publish/delete via the FastMCP tool surface against the configured sandbox publication. - scripts/extract_har.py prints a digestible per-entry summary of a HAR capture (URL, method, status, headers, body), useful for reverse-engineering Substack flows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for 19776ee - Browse repository at this point
Copy the full SHA 19776eeView commit details -
feat(api): add upload_podcast_audio for podcast audio upload + transcode
Implements the full Substack podcast audio upload sequence as a single public method on Api: 1. POST /audio/upload to initiate (returns media UUID, multipart upload id, and pre-signed S3 PUT URL(s)) 2. PUT raw audio bytes to each pre-signed S3 URL (single part today; loop iterates so future multi-part upload requires no changes) 3. POST /audio/upload/{uuid}/transcode with locally-computed mutagen duration + S3 etags (quotes preserved) to finalize multipart upload and trigger async transcoding 4. Poll GET /audio/upload/{uuid} until state == "transcoded" The S3 PUT is the only HTTP call in the codebase that does not route through Api._handle_response: S3 returns an empty body on success and the signal we need (ETag header) is in response.headers, not the body. On S3 failure we raise SubstackRequestException with status code and truncated body to preserve diagnostic info. Dependencies: - mutagen (^1.47) added as a runtime dep; reads MP3 duration headers client-side without ffmpeg - lameenc (^1.8) added as a dev dep; conftest uses it to generate a tiny silent MP3 fixture for the integration test (pure-Python wheels, no ffmpeg system dependency) - pytest (^9.0) added explicitly to dev deps Tests: - 22 unit tests in tests/substack/test_audio_upload.py mock at the requests.Session level (deliberate departure from the codebase's no-mock convention -- documented in docs/STYLE.md) - 1 integration test gated on RUN_INTEGRATION_TESTS=1 with the new "integration" pytest marker; creates a real podcast draft on the configured sandbox, uploads the fixture, verifies transcoded state and non-zero duration, deletes the draft in a finally block Tooling: - "integration" marker registered in pyproject.toml so default pytest runs (or -m "not integration") cleanly skip it - tests/fixtures/ added to .gitignore (regenerated on demand) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Configuration menu - View commit details
-
Copy full SHA for f1f0c6c - Browse repository at this point
Copy the full SHA f1f0c6cView commit details -
feat(podcast): add PodcastPost class for podcast draft assembly
PodcastPost is the podcast-side counterpart to Post: in-memory state for a Substack podcast draft, mirroring Post's `vars(self)` serialization so every public attribute maps directly to a Substack API field. Key differences from Post: - Holds two distinct ProseMirror documents: `draft_body` (post-page body, same field Post uses) and `podcast_description` (show notes shown alongside the audio player). Both are optional, both JSON-encoded by get_draft. - Carries podcast-specific fields: `type="podcast"`, `draft_podcast_upload_id`, `draft_podcast_url`, `draft_podcast_duration`. - Carries `last_updated_at` for optimistic concurrency on multi-step flows (create -> PUT updates). Initially None and omitted from get_draft; callers set it from the previous draft response's `draft_updated_at` before each subsequent PUT. - get_draft is non-destructive (shallow-copies vars(self) before JSON-encoding) so the same instance can be reused across create / PUT / publish. Post has a known footgun where get_draft replaces self.draft_body in place; PodcastPost avoids it. Composition over inheritance: the markdown setters (set_body_from_markdown, set_show_notes_from_markdown) construct a throwaway Post instance, run from_markdown, and steal the resulting draft_body dict. Reuses the full Post markdown parser (headings, lists, inline formatting, links, code blocks, blockquotes, etc.) without duplicating any of it. Both setters accept an optional `api` parameter forwarded to Post.from_markdown for inline image upload. Builders return self: set_section, set_audio, set_body_from_markdown, set_show_notes_from_markdown -- consistent with STYLE.md section 10. No new Api method needed -- post_draft and put_draft already accept arbitrary JSON bodies / kwargs, so the existing endpoints handle podcast drafts unchanged. type="podcast" in the create body is the only discriminator the server needs. Re-exported from `substack` package top level alongside Api. Tests: - 47 unit tests in tests/substack/test_podcast.py (pure unit, no mocks): construction defaults, get_draft serialization (both docs JSON-encoded, last_updated_at omit-when-None / include-when-set, non-destructive repeat calls), markdown setters delegate to Post correctly without cross-contaminating the two docs, set_audio/set_section/fluent chaining, last_updated_at lifecycle. - 1 integration test gated on RUN_INTEGRATION_TESTS=1 that creates a podcast draft against the sandbox, uploads a fixture MP3 via Api.upload_podcast_audio, attaches via PUT /drafts, and asserts the server echoes title/subtitle/audio UUID/both ProseMirror docs AND that draft_updated_at advances after the PUT (proof that last_updated_at threading is actually round-tripping, not just satisfying mock assertions). Cleanup in a finally block. No publish. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for a1b6598 - Browse repository at this point
Copy the full SHA a1b6598View commit details -
feat(podcast): wire up publish flow + end-to-end example
No new Api methods needed. Both Api.prepublish_draft and Api.publish_draft are generic over draft type (no podcast-specific branching at either endpoint; the Substack server is tolerant of the share_automatically key on podcast publish even though the captured frontend omits it). No client-side guard-rails added either -- the /prepublish endpoint is the authoritative validator and duplicating it here would drift from server truth. What ships: - tests/substack/test_podcast_publish_integration.py: full end-to-end flow against the sandbox -- create podcast draft, upload tiny fixture MP3, attach via PUT with last_updated_at threading, prepublish (assert errors=[]), publish with send=False (no email goes out), assert is_published=True / type=podcast / audio reference preserved, delete the post in a finally block. Gated on RUN_INTEGRATION_TESTS=1 and the `integration` marker. - examples/publish_podcast.py: argparse-driven example mirroring examples/publish_markdown.py style. Demonstrates the canonical six-step flow (build PodcastPost, create draft, upload audio, attach, prepublish, publish) with --publish/--no-send/--audience flags and optional --body-file / --show-notes-file Markdown inputs. Defaults are safe (stops at draft stage unless --publish is given). With this commit, the library can publish a podcast end-to-end purely through the Api + PodcastPost surface, with no MCP layer required. The MCP tool wrappers are Phase F. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for 8b37b2a - Browse repository at this point
Copy the full SHA 8b37b2aView commit details -
feat(mcp): add podcast tools (upload + end-to-end create-and-publish)
Two new FastMCP tools matching the existing tool-surface pattern (async def, full type hints, full prose docstrings with Args/Returns/ Examples): - upload_podcast_audio(file_path, draft_id): thin wrapper around Api.upload_podcast_audio. The audio UUID returned is what attaches the upload to a draft. - post_podcast_draft_from_markdown(...): composes the full podcast flow in a single call -- build PodcastPost, post_draft, optionally upload + attach audio (threading last_updated_at from the create response into the attach PUT, matching the architecture's optimistic-concurrency guidance), optionally tag, optionally prepublish, optionally publish. show_notes_markdown and post_body_markdown are independently optional and populate the two distinct ProseMirror fields on a podcast draft. Returns a 5-key dict ({draft, upload, tags, prepublish, publish}) surfacing each composed step's result. No client-side guard-rails added (no "must have audio" / "must have title" checks). prepublish_draft is the authoritative validator and duplicating its logic here would drift from server truth. Tests: - tests/substack_mcp/test_mcp_server_podcast.py: 33 unit tests covering PodcastPost construction wiring, the two-ProseMirror-doc split, audio attach flow (including the critical last_updated_at threading assertion that reads from put_draft.call_args.kwargs to catch the "attribute set but stripped by get_draft" bug class), tags normalization, prepublish/publish gating, return shape, and the full call ordering (post_draft -> upload -> put -> tags -> prepublish -> publish). Mocks get_api per the pattern flagged in tests/substack/test_audio_upload.py. - tests/substack_mcp/test_mcp_podcast_integration.py: the v1 acceptance test. One MCP call takes MP3 + Markdown for body + show notes, creates a podcast draft, uploads the audio, attaches via PUT, prepublishes, publishes with send=False, and deletes via finally block. Gated on RUN_INTEGRATION_TESTS=1. README: - Fix wrong MCP path reference (substack/mcp_fastmcp.py was already wrong before this commit; it lives in substack_mcp/mcp_server.py). - Add "Publishing a Podcast Episode" section showing the Api + PodcastPost flow end-to-end, with a pointer to examples/publish_podcast.py. - Split the MCP tools list into Newsletter and Podcast subsections; document both new tools. With this commit, podcast publishing works end-to-end from both the direct-Python and MCP surfaces. The complete phase sequence: Phase C: Api.upload_podcast_audio Phase D: PodcastPost Phase E: publish flow + example Phase F: MCP tool wrappers constitutes v1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Configuration menu - View commit details
-
Copy full SHA for 233c707 - Browse repository at this point
Copy the full SHA 233c707View commit details
Loading
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff main...main