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.
- python_substack-0.1.26/PKG-INFO +433 -0
- python_substack-0.1.26/README.md +397 -0
- python_substack-0.1.26/pyproject.toml +60 -0
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/__init__.py +2 -2
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/api.py +93 -16
- python_substack-0.1.26/substack/cli.py +583 -0
- python_substack-0.1.26/substack_mcp/mcp_server.py +293 -0
- python_substack-0.1.24/PKG-INFO +0 -331
- python_substack-0.1.24/README.md +0 -304
- python_substack-0.1.24/pyproject.toml +0 -38
- {python_substack-0.1.24 → python_substack-0.1.26}/LICENSE +0 -0
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/exceptions.py +0 -0
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/mdrender.py +0 -0
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/nodes.py +0 -0
- {python_substack-0.1.24 → python_substack-0.1.26}/substack/post.py +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/python-substack/)
|
|
41
|
+
[](https://pypi.org/project/python-substack/)
|
|
42
|
+
[](https://github.com/ma2za/python-substack/actions/workflows/ci.yml)
|
|
43
|
+
[](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](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
|
+

|
|
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
|
+

|
|
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
|
+
|