python-substack 0.1.24__tar.gz → 0.1.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-substack
3
+ Version: 0.1.26
4
+ Summary: A Python SDK and CLI for managing Substack publications and drafts.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: substack,substack-api,cli,newsletter,publishing,automation,mcp
8
+ Author: Paolo Mazza
9
+ Author-email: mazzapaolo2019@gmail.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Communications :: Email
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Provides-Extra: mcp
24
+ Requires-Dist: PyYAML (>=6.0,<7.0)
25
+ Requires-Dist: fastmcp (>=3.1.1,<4.0.0) ; extra == "mcp"
26
+ Requires-Dist: markdown-it-py (>=3.0,<4.0)
27
+ Requires-Dist: mdit-py-plugins (>=0.4,<0.5)
28
+ Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
29
+ Requires-Dist: requests (>=2.32.0,<3.0.0)
30
+ Project-URL: Changelog, https://github.com/ma2za/python-substack/blob/main/CHANGELOG.md
31
+ Project-URL: Homepage, https://github.com/ma2za/python-substack
32
+ Project-URL: Issues, https://github.com/ma2za/python-substack/issues
33
+ Project-URL: Repository, https://github.com/ma2za/python-substack
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Python Substack
37
+
38
+ An unofficial Python SDK and CLI for managing [Substack](https://substack.com/) publications and drafts.
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/python-substack)](https://pypi.org/project/python-substack/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/python-substack)](https://pypi.org/project/python-substack/)
42
+ [![Tests](https://github.com/ma2za/python-substack/actions/workflows/ci.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci.yml)
43
+ [![Release](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml)
44
+ [![License](https://img.shields.io/pypi/l/python-substack)](LICENSE)
45
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
46
+
47
+ ## Features
48
+
49
+ - Inspect authentication and publication status from the terminal.
50
+ - List publications and inspect, schedule, publish, or delete drafts.
51
+ - Use stable JSON output in scripts and automation.
52
+ - Create drafts and publish posts from Python.
53
+ - Convert Markdown into Substack's editor document format.
54
+ - Upload local images while rendering Markdown.
55
+ - Set audience, comment permissions, SEO title, SEO description, slug, sections, and tags.
56
+ - Publish now, schedule drafts, or keep drafts unpublished by default.
57
+ - Authenticate with email/password, cookies JSON, or a browser cookie string.
58
+ - Run a FastMCP server for AI-assisted publishing workflows.
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install python-substack
64
+ ```
65
+
66
+ Install the MCP server extra:
67
+
68
+ ```bash
69
+ pip install "python-substack[mcp]"
70
+ ```
71
+
72
+ ## Setup
73
+
74
+ Copy `.env.example` to `.env` and fill in one authentication method:
75
+
76
+ ```env
77
+ EMAIL=
78
+ PASSWORD=
79
+ PUBLICATION_URL=
80
+ COOKIES_PATH=
81
+ COOKIES_STRING=
82
+ ```
83
+
84
+ Use either `EMAIL` and `PASSWORD`, or cookie-based authentication with `COOKIES_PATH` or `COOKIES_STRING`. Cookie authentication is usually the better option if Substack prompts for captcha or magic-link sign-in.
85
+
86
+ Newer Substack accounts may only have magic-link sign-in enabled. To set a password, sign out of Substack, choose "Sign in with password", then choose "Set a new password".
87
+
88
+ ## CLI Operations
89
+
90
+ Check authentication, the selected publication, and subscriber count:
91
+
92
+ ```bash
93
+ substack status
94
+ ```
95
+
96
+ List available publications or target one without changing `.env`:
97
+
98
+ ```bash
99
+ substack publications list
100
+ substack --publication-url https://example.substack.com drafts list
101
+ ```
102
+
103
+ List and inspect drafts:
104
+
105
+ ```bash
106
+ substack drafts list --limit 10
107
+ substack drafts get 12345
108
+ ```
109
+
110
+ Schedule with a timezone-aware ISO 8601 timestamp, or remove a schedule:
111
+
112
+ ```bash
113
+ substack drafts schedule 12345 --at 2026-08-01T09:00:00+03:00
114
+ substack drafts unschedule 12345
115
+ ```
116
+
117
+ Publishing and deletion prompt for confirmation. Use `--yes` for intentional non-interactive execution:
118
+
119
+ ```bash
120
+ substack drafts publish 12345 --no-send
121
+ substack drafts delete 12345 --yes
122
+ ```
123
+
124
+ Global options must appear before the command. `--json` returns stable envelopes containing the raw Substack responses:
125
+
126
+ ```bash
127
+ substack --json drafts list
128
+ substack --cookies cookies.json --json status
129
+ ```
130
+
131
+ ## Quickstart
132
+
133
+ ```python
134
+ import os
135
+
136
+ from dotenv import load_dotenv
137
+ from substack import Api
138
+
139
+ load_dotenv()
140
+
141
+ api = Api(
142
+ email=os.getenv("EMAIL"),
143
+ password=os.getenv("PASSWORD"),
144
+ publication_url=os.getenv("PUBLICATION_URL"),
145
+ )
146
+
147
+ result = api.create_draft_from_markdown(
148
+ title="Shipping with Python",
149
+ subtitle="A short note from a script",
150
+ markdown="""
151
+ # Hello
152
+
153
+ This draft was created from **Markdown**.
154
+
155
+ ![Alt text](https://example.com/image.png "Image caption")
156
+ """,
157
+ tags=["python", "automation"],
158
+ slug="shipping-with-python",
159
+ )
160
+
161
+ print(result["draft"]["id"])
162
+ ```
163
+
164
+ `create_draft_from_markdown` creates a draft by default. It only publishes when `publish=True` is passed.
165
+
166
+ ## Content Publishing CLI
167
+
168
+ Check authentication without creating a draft:
169
+
170
+ ```bash
171
+ substack-auth-check
172
+ ```
173
+
174
+ With a cookies JSON file:
175
+
176
+ ```bash
177
+ substack-auth-check --cookies cookies.json
178
+ ```
179
+
180
+ Publish a Markdown file as a draft:
181
+
182
+ ```bash
183
+ substack-publish-markdown post.md --title "My Post"
184
+ ```
185
+
186
+ Create and publish:
187
+
188
+ ```bash
189
+ substack-publish-markdown post.md --title "My Post" --publish
190
+ ```
191
+
192
+ Publish from YAML:
193
+
194
+ ```bash
195
+ substack-publish-yaml draft.yaml
196
+ ```
197
+
198
+ Useful options:
199
+
200
+ ```bash
201
+ substack-publish-markdown post.md \
202
+ --title "My Post" \
203
+ --subtitle "Optional subtitle" \
204
+ --tag python \
205
+ --tag substack \
206
+ --slug my-post \
207
+ --search-engine-title "SEO title" \
208
+ --search-engine-description "SEO description"
209
+ ```
210
+
211
+ ## Cookie Authentication
212
+
213
+ Cookie authentication avoids logging in with email/password on every run and helps when Substack requires captcha or magic-link sign-in.
214
+
215
+ Use a cookies JSON file:
216
+
217
+ ```python
218
+ import os
219
+
220
+ from dotenv import load_dotenv
221
+ from substack import Api
222
+
223
+ load_dotenv()
224
+
225
+ api = Api(
226
+ cookies_path=os.getenv("COOKIES_PATH"),
227
+ publication_url=os.getenv("PUBLICATION_URL"),
228
+ )
229
+ ```
230
+
231
+ Or paste a browser cookie header into `COOKIES_STRING`:
232
+
233
+ ```python
234
+ import os
235
+
236
+ from dotenv import load_dotenv
237
+ from substack import Api
238
+
239
+ load_dotenv()
240
+
241
+ api = Api(
242
+ cookies_string=os.getenv("COOKIES_STRING"),
243
+ publication_url=os.getenv("PUBLICATION_URL"),
244
+ )
245
+ ```
246
+
247
+ To get a cookie string:
248
+
249
+ 1. Sign in to Substack in your browser.
250
+ 2. Open developer tools.
251
+ 3. Go to the network tab and refresh Substack.
252
+ 4. Select a request such as `subscription/unred/subscriptions`.
253
+ 5. Copy the full `cookie` request header value into `COOKIES_STRING`.
254
+
255
+ To export a working session to a cookies JSON file:
256
+
257
+ ```python
258
+ api.export_cookies("cookies.json")
259
+ ```
260
+
261
+ Then set:
262
+
263
+ ```env
264
+ COOKIES_PATH=cookies.json
265
+ ```
266
+
267
+ The CLI also accepts a cookie JSON path:
268
+
269
+ ```bash
270
+ substack-publish-markdown post.md --cookies cookies.json
271
+ ```
272
+
273
+ ## Low-Level Post Builder
274
+
275
+ ```python
276
+ import os
277
+
278
+ from dotenv import load_dotenv
279
+ from substack import Api
280
+ from substack.post import Post
281
+
282
+ load_dotenv()
283
+
284
+ api = Api(
285
+ email=os.getenv("EMAIL"),
286
+ password=os.getenv("PASSWORD"),
287
+ publication_url=os.getenv("PUBLICATION_URL"),
288
+ )
289
+
290
+ user_id = api.get_user_id()
291
+
292
+ post = Post(
293
+ title="How to publish a Substack post using Python",
294
+ subtitle="Created with python-substack",
295
+ user_id=user_id,
296
+ audience="everyone",
297
+ write_comment_permissions="everyone",
298
+ )
299
+
300
+ post.paragraph("This is a paragraph.")
301
+ post.add(
302
+ {
303
+ "type": "paragraph",
304
+ "content": [
305
+ {"content": "A link to "},
306
+ {
307
+ "content": "Substack",
308
+ "marks": [{"type": "link", "href": "https://substack.com"}],
309
+ },
310
+ ],
311
+ }
312
+ )
313
+ post.add({"type": "paywall"})
314
+ post.add({"type": "captionedImage", "src": "https://example.com/image.png"})
315
+
316
+ draft = api.post_draft(post.get_draft())
317
+ api.prepublish_draft(draft.get("id"))
318
+ api.publish_draft(draft.get("id"))
319
+ ```
320
+
321
+ ## Markdown Support
322
+
323
+ ```python
324
+ from substack.post import Post
325
+
326
+ post = Post("Title", "Subtitle", user_id=1)
327
+ post.from_markdown(
328
+ """
329
+ # Heading
330
+
331
+ Paragraph with **bold**, *italic*, `code`, [links](https://example.com), and footnotes.[^1]
332
+
333
+ - Lists
334
+ - Images
335
+
336
+ ![Alt](local-image.png "Caption")
337
+
338
+ [^1]: Footnote text.
339
+ """
340
+ )
341
+ ```
342
+
343
+ Supported Markdown includes headings, paragraphs, bold, italic, inline code, strikethrough, links, images, linked images, image captions, code blocks, blockquotes, ordered lists, unordered lists, horizontal rules, and footnotes.
344
+
345
+ When an `Api` instance is passed to `from_markdown`, local image paths are uploaded before the draft is created:
346
+
347
+ ```python
348
+ post.from_markdown(markdown_content, api=api)
349
+ ```
350
+
351
+ ## YAML Drafts
352
+
353
+ ```yaml
354
+ title: "My Post Title"
355
+ subtitle: "My Post Subtitle"
356
+ audience: "everyone"
357
+ write_comment_permissions: "everyone"
358
+ search_engine_title: "SEO title"
359
+ search_engine_description: "SEO description"
360
+ slug: "my-post-title"
361
+ tags:
362
+ - python
363
+ - substack
364
+ markdown: |
365
+ # Introduction
366
+
367
+ This post body is Markdown.
368
+ ```
369
+
370
+ The lower-level node format is also supported:
371
+
372
+ ```yaml
373
+ title: "My Post Title"
374
+ subtitle: "My Post Subtitle"
375
+ body:
376
+ 0:
377
+ type: "heading"
378
+ level: 1
379
+ content: "Introduction"
380
+ 1:
381
+ type: "paragraph"
382
+ content: "This is a paragraph."
383
+ 2:
384
+ type: "captionedImage"
385
+ src: "local_image.jpg"
386
+ ```
387
+
388
+ ## MCP Server
389
+
390
+ Install the MCP extra:
391
+
392
+ ```bash
393
+ pip install "python-substack[mcp]"
394
+ ```
395
+
396
+ Run the server over stdio:
397
+
398
+ ```bash
399
+ substack-mcp
400
+ ```
401
+
402
+ Equivalent Python entry point:
403
+
404
+ ```bash
405
+ python -c "from substack_mcp.mcp_server import main; main()"
406
+ ```
407
+
408
+ Available tools:
409
+
410
+ - `post_draft_from_markdown(...)`
411
+ - `put_draft(draft_id, update_payload)`
412
+ - `add_tags(draft_id, tags)`
413
+ - `prepublish_draft(draft_id)`
414
+ - `publish_draft(draft_id, send=True, share_automatically=False)`
415
+
416
+ ## Development
417
+
418
+ ```bash
419
+ pip install pre-commit
420
+ pre-commit install
421
+ pytest
422
+ ```
423
+
424
+ Live Substack tests are opt-in. Set `RUN_SUBSTACK_E2E=1` and configure credentials before running them.
425
+
426
+ The CLI operations smoke test is separately opt-in. Set `RUN_SUBSTACK_CLI_E2E=1` to create, schedule, unschedule, inspect, and delete a disposable draft. It never publishes the draft.
427
+
428
+ Release changes are tracked in [CHANGELOG.md](CHANGELOG.md).
429
+
430
+ ## Disclaimer
431
+
432
+ This project is not affiliated with Substack.
433
+