python-substack 0.1.23__tar.gz → 0.1.25__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,381 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-substack
3
+ Version: 0.1.25
4
+ Summary: A Python wrapper around the Substack API.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: substack
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
+ Unofficial Python tools for publishing to [Substack](https://substack.com/).
39
+
40
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
41
+ ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
42
+
43
+ ## Features
44
+
45
+ - Create drafts and publish posts from Python.
46
+ - Convert Markdown into Substack's editor document format.
47
+ - Upload local images while rendering Markdown.
48
+ - Set audience, comment permissions, SEO title, SEO description, slug, sections, and tags.
49
+ - Publish now, schedule drafts, or keep drafts unpublished by default.
50
+ - Authenticate with email/password, cookies JSON, or a browser cookie string.
51
+ - Run a FastMCP server for AI-assisted publishing workflows.
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install python-substack
57
+ ```
58
+
59
+ Install the MCP server extra:
60
+
61
+ ```bash
62
+ pip install "python-substack[mcp]"
63
+ ```
64
+
65
+ ## Setup
66
+
67
+ Copy `.env.example` to `.env` and fill in one authentication method:
68
+
69
+ ```env
70
+ EMAIL=
71
+ PASSWORD=
72
+ PUBLICATION_URL=
73
+ COOKIES_PATH=
74
+ COOKIES_STRING=
75
+ ```
76
+
77
+ 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.
78
+
79
+ 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".
80
+
81
+ ## Quickstart
82
+
83
+ ```python
84
+ import os
85
+
86
+ from dotenv import load_dotenv
87
+ from substack import Api
88
+
89
+ load_dotenv()
90
+
91
+ api = Api(
92
+ email=os.getenv("EMAIL"),
93
+ password=os.getenv("PASSWORD"),
94
+ publication_url=os.getenv("PUBLICATION_URL"),
95
+ )
96
+
97
+ result = api.create_draft_from_markdown(
98
+ title="Shipping with Python",
99
+ subtitle="A short note from a script",
100
+ markdown="""
101
+ # Hello
102
+
103
+ This draft was created from **Markdown**.
104
+
105
+ ![Alt text](https://example.com/image.png "Image caption")
106
+ """,
107
+ tags=["python", "automation"],
108
+ slug="shipping-with-python",
109
+ )
110
+
111
+ print(result["draft"]["id"])
112
+ ```
113
+
114
+ `create_draft_from_markdown` creates a draft by default. It only publishes when `publish=True` is passed.
115
+
116
+ ## CLI
117
+
118
+ Check authentication without creating a draft:
119
+
120
+ ```bash
121
+ substack-auth-check
122
+ ```
123
+
124
+ With a cookies JSON file:
125
+
126
+ ```bash
127
+ substack-auth-check --cookies cookies.json
128
+ ```
129
+
130
+ Publish a Markdown file as a draft:
131
+
132
+ ```bash
133
+ substack-publish-markdown post.md --title "My Post"
134
+ ```
135
+
136
+ Create and publish:
137
+
138
+ ```bash
139
+ substack-publish-markdown post.md --title "My Post" --publish
140
+ ```
141
+
142
+ Publish from YAML:
143
+
144
+ ```bash
145
+ substack-publish-yaml draft.yaml
146
+ ```
147
+
148
+ Useful options:
149
+
150
+ ```bash
151
+ substack-publish-markdown post.md \
152
+ --title "My Post" \
153
+ --subtitle "Optional subtitle" \
154
+ --tag python \
155
+ --tag substack \
156
+ --slug my-post \
157
+ --search-engine-title "SEO title" \
158
+ --search-engine-description "SEO description"
159
+ ```
160
+
161
+ ## Cookie Authentication
162
+
163
+ Cookie authentication avoids logging in with email/password on every run and helps when Substack requires captcha or magic-link sign-in.
164
+
165
+ Use a cookies JSON file:
166
+
167
+ ```python
168
+ import os
169
+
170
+ from dotenv import load_dotenv
171
+ from substack import Api
172
+
173
+ load_dotenv()
174
+
175
+ api = Api(
176
+ cookies_path=os.getenv("COOKIES_PATH"),
177
+ publication_url=os.getenv("PUBLICATION_URL"),
178
+ )
179
+ ```
180
+
181
+ Or paste a browser cookie header into `COOKIES_STRING`:
182
+
183
+ ```python
184
+ import os
185
+
186
+ from dotenv import load_dotenv
187
+ from substack import Api
188
+
189
+ load_dotenv()
190
+
191
+ api = Api(
192
+ cookies_string=os.getenv("COOKIES_STRING"),
193
+ publication_url=os.getenv("PUBLICATION_URL"),
194
+ )
195
+ ```
196
+
197
+ To get a cookie string:
198
+
199
+ 1. Sign in to Substack in your browser.
200
+ 2. Open developer tools.
201
+ 3. Go to the network tab and refresh Substack.
202
+ 4. Select a request such as `subscription/unred/subscriptions`.
203
+ 5. Copy the full `cookie` request header value into `COOKIES_STRING`.
204
+
205
+ To export a working session to a cookies JSON file:
206
+
207
+ ```python
208
+ api.export_cookies("cookies.json")
209
+ ```
210
+
211
+ Then set:
212
+
213
+ ```env
214
+ COOKIES_PATH=cookies.json
215
+ ```
216
+
217
+ The CLI also accepts a cookie JSON path:
218
+
219
+ ```bash
220
+ substack-publish-markdown post.md --cookies cookies.json
221
+ ```
222
+
223
+ ## Low-Level Post Builder
224
+
225
+ ```python
226
+ import os
227
+
228
+ from dotenv import load_dotenv
229
+ from substack import Api
230
+ from substack.post import Post
231
+
232
+ load_dotenv()
233
+
234
+ api = Api(
235
+ email=os.getenv("EMAIL"),
236
+ password=os.getenv("PASSWORD"),
237
+ publication_url=os.getenv("PUBLICATION_URL"),
238
+ )
239
+
240
+ user_id = api.get_user_id()
241
+
242
+ post = Post(
243
+ title="How to publish a Substack post using Python",
244
+ subtitle="Created with python-substack",
245
+ user_id=user_id,
246
+ audience="everyone",
247
+ write_comment_permissions="everyone",
248
+ )
249
+
250
+ post.paragraph("This is a paragraph.")
251
+ post.add(
252
+ {
253
+ "type": "paragraph",
254
+ "content": [
255
+ {"content": "A link to "},
256
+ {
257
+ "content": "Substack",
258
+ "marks": [{"type": "link", "href": "https://substack.com"}],
259
+ },
260
+ ],
261
+ }
262
+ )
263
+ post.add({"type": "paywall"})
264
+ post.add({"type": "captionedImage", "src": "https://example.com/image.png"})
265
+
266
+ draft = api.post_draft(post.get_draft())
267
+ api.prepublish_draft(draft.get("id"))
268
+ api.publish_draft(draft.get("id"))
269
+ ```
270
+
271
+ ## Markdown Support
272
+
273
+ ```python
274
+ from substack.post import Post
275
+
276
+ post = Post("Title", "Subtitle", user_id=1)
277
+ post.from_markdown(
278
+ """
279
+ # Heading
280
+
281
+ Paragraph with **bold**, *italic*, `code`, [links](https://example.com), and footnotes.[^1]
282
+
283
+ - Lists
284
+ - Images
285
+
286
+ ![Alt](local-image.png "Caption")
287
+
288
+ [^1]: Footnote text.
289
+ """
290
+ )
291
+ ```
292
+
293
+ 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.
294
+
295
+ When an `Api` instance is passed to `from_markdown`, local image paths are uploaded before the draft is created:
296
+
297
+ ```python
298
+ post.from_markdown(markdown_content, api=api)
299
+ ```
300
+
301
+ ## YAML Drafts
302
+
303
+ ```yaml
304
+ title: "My Post Title"
305
+ subtitle: "My Post Subtitle"
306
+ audience: "everyone"
307
+ write_comment_permissions: "everyone"
308
+ search_engine_title: "SEO title"
309
+ search_engine_description: "SEO description"
310
+ slug: "my-post-title"
311
+ tags:
312
+ - python
313
+ - substack
314
+ markdown: |
315
+ # Introduction
316
+
317
+ This post body is Markdown.
318
+ ```
319
+
320
+ The lower-level node format is also supported:
321
+
322
+ ```yaml
323
+ title: "My Post Title"
324
+ subtitle: "My Post Subtitle"
325
+ body:
326
+ 0:
327
+ type: "heading"
328
+ level: 1
329
+ content: "Introduction"
330
+ 1:
331
+ type: "paragraph"
332
+ content: "This is a paragraph."
333
+ 2:
334
+ type: "captionedImage"
335
+ src: "local_image.jpg"
336
+ ```
337
+
338
+ ## MCP Server
339
+
340
+ Install the MCP extra:
341
+
342
+ ```bash
343
+ pip install "python-substack[mcp]"
344
+ ```
345
+
346
+ Run the server over stdio:
347
+
348
+ ```bash
349
+ substack-mcp
350
+ ```
351
+
352
+ Equivalent Python entry point:
353
+
354
+ ```bash
355
+ python -c "from substack_mcp.mcp_server import main; main()"
356
+ ```
357
+
358
+ Available tools:
359
+
360
+ - `post_draft_from_markdown(...)`
361
+ - `put_draft(draft_id, update_payload)`
362
+ - `add_tags(draft_id, tags)`
363
+ - `prepublish_draft(draft_id)`
364
+ - `publish_draft(draft_id, send=True, share_automatically=False)`
365
+
366
+ ## Development
367
+
368
+ ```bash
369
+ pip install pre-commit
370
+ pre-commit install
371
+ pytest
372
+ ```
373
+
374
+ Live Substack tests are opt-in. Set `RUN_SUBSTACK_E2E=1` and configure credentials before running them.
375
+
376
+ Release changes are tracked in [CHANGELOG.md](CHANGELOG.md).
377
+
378
+ ## Disclaimer
379
+
380
+ This project is not affiliated with Substack.
381
+
@@ -0,0 +1,345 @@
1
+ # Python Substack
2
+
3
+ Unofficial Python tools for publishing to [Substack](https://substack.com/).
4
+
5
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
6
+ ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
7
+
8
+ ## Features
9
+
10
+ - Create drafts and publish posts from Python.
11
+ - Convert Markdown into Substack's editor document format.
12
+ - Upload local images while rendering Markdown.
13
+ - Set audience, comment permissions, SEO title, SEO description, slug, sections, and tags.
14
+ - Publish now, schedule drafts, or keep drafts unpublished by default.
15
+ - Authenticate with email/password, cookies JSON, or a browser cookie string.
16
+ - Run a FastMCP server for AI-assisted publishing workflows.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install python-substack
22
+ ```
23
+
24
+ Install the MCP server extra:
25
+
26
+ ```bash
27
+ pip install "python-substack[mcp]"
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ Copy `.env.example` to `.env` and fill in one authentication method:
33
+
34
+ ```env
35
+ EMAIL=
36
+ PASSWORD=
37
+ PUBLICATION_URL=
38
+ COOKIES_PATH=
39
+ COOKIES_STRING=
40
+ ```
41
+
42
+ 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.
43
+
44
+ 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".
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ import os
50
+
51
+ from dotenv import load_dotenv
52
+ from substack import Api
53
+
54
+ load_dotenv()
55
+
56
+ api = Api(
57
+ email=os.getenv("EMAIL"),
58
+ password=os.getenv("PASSWORD"),
59
+ publication_url=os.getenv("PUBLICATION_URL"),
60
+ )
61
+
62
+ result = api.create_draft_from_markdown(
63
+ title="Shipping with Python",
64
+ subtitle="A short note from a script",
65
+ markdown="""
66
+ # Hello
67
+
68
+ This draft was created from **Markdown**.
69
+
70
+ ![Alt text](https://example.com/image.png "Image caption")
71
+ """,
72
+ tags=["python", "automation"],
73
+ slug="shipping-with-python",
74
+ )
75
+
76
+ print(result["draft"]["id"])
77
+ ```
78
+
79
+ `create_draft_from_markdown` creates a draft by default. It only publishes when `publish=True` is passed.
80
+
81
+ ## CLI
82
+
83
+ Check authentication without creating a draft:
84
+
85
+ ```bash
86
+ substack-auth-check
87
+ ```
88
+
89
+ With a cookies JSON file:
90
+
91
+ ```bash
92
+ substack-auth-check --cookies cookies.json
93
+ ```
94
+
95
+ Publish a Markdown file as a draft:
96
+
97
+ ```bash
98
+ substack-publish-markdown post.md --title "My Post"
99
+ ```
100
+
101
+ Create and publish:
102
+
103
+ ```bash
104
+ substack-publish-markdown post.md --title "My Post" --publish
105
+ ```
106
+
107
+ Publish from YAML:
108
+
109
+ ```bash
110
+ substack-publish-yaml draft.yaml
111
+ ```
112
+
113
+ Useful options:
114
+
115
+ ```bash
116
+ substack-publish-markdown post.md \
117
+ --title "My Post" \
118
+ --subtitle "Optional subtitle" \
119
+ --tag python \
120
+ --tag substack \
121
+ --slug my-post \
122
+ --search-engine-title "SEO title" \
123
+ --search-engine-description "SEO description"
124
+ ```
125
+
126
+ ## Cookie Authentication
127
+
128
+ Cookie authentication avoids logging in with email/password on every run and helps when Substack requires captcha or magic-link sign-in.
129
+
130
+ Use a cookies JSON file:
131
+
132
+ ```python
133
+ import os
134
+
135
+ from dotenv import load_dotenv
136
+ from substack import Api
137
+
138
+ load_dotenv()
139
+
140
+ api = Api(
141
+ cookies_path=os.getenv("COOKIES_PATH"),
142
+ publication_url=os.getenv("PUBLICATION_URL"),
143
+ )
144
+ ```
145
+
146
+ Or paste a browser cookie header into `COOKIES_STRING`:
147
+
148
+ ```python
149
+ import os
150
+
151
+ from dotenv import load_dotenv
152
+ from substack import Api
153
+
154
+ load_dotenv()
155
+
156
+ api = Api(
157
+ cookies_string=os.getenv("COOKIES_STRING"),
158
+ publication_url=os.getenv("PUBLICATION_URL"),
159
+ )
160
+ ```
161
+
162
+ To get a cookie string:
163
+
164
+ 1. Sign in to Substack in your browser.
165
+ 2. Open developer tools.
166
+ 3. Go to the network tab and refresh Substack.
167
+ 4. Select a request such as `subscription/unred/subscriptions`.
168
+ 5. Copy the full `cookie` request header value into `COOKIES_STRING`.
169
+
170
+ To export a working session to a cookies JSON file:
171
+
172
+ ```python
173
+ api.export_cookies("cookies.json")
174
+ ```
175
+
176
+ Then set:
177
+
178
+ ```env
179
+ COOKIES_PATH=cookies.json
180
+ ```
181
+
182
+ The CLI also accepts a cookie JSON path:
183
+
184
+ ```bash
185
+ substack-publish-markdown post.md --cookies cookies.json
186
+ ```
187
+
188
+ ## Low-Level Post Builder
189
+
190
+ ```python
191
+ import os
192
+
193
+ from dotenv import load_dotenv
194
+ from substack import Api
195
+ from substack.post import Post
196
+
197
+ load_dotenv()
198
+
199
+ api = Api(
200
+ email=os.getenv("EMAIL"),
201
+ password=os.getenv("PASSWORD"),
202
+ publication_url=os.getenv("PUBLICATION_URL"),
203
+ )
204
+
205
+ user_id = api.get_user_id()
206
+
207
+ post = Post(
208
+ title="How to publish a Substack post using Python",
209
+ subtitle="Created with python-substack",
210
+ user_id=user_id,
211
+ audience="everyone",
212
+ write_comment_permissions="everyone",
213
+ )
214
+
215
+ post.paragraph("This is a paragraph.")
216
+ post.add(
217
+ {
218
+ "type": "paragraph",
219
+ "content": [
220
+ {"content": "A link to "},
221
+ {
222
+ "content": "Substack",
223
+ "marks": [{"type": "link", "href": "https://substack.com"}],
224
+ },
225
+ ],
226
+ }
227
+ )
228
+ post.add({"type": "paywall"})
229
+ post.add({"type": "captionedImage", "src": "https://example.com/image.png"})
230
+
231
+ draft = api.post_draft(post.get_draft())
232
+ api.prepublish_draft(draft.get("id"))
233
+ api.publish_draft(draft.get("id"))
234
+ ```
235
+
236
+ ## Markdown Support
237
+
238
+ ```python
239
+ from substack.post import Post
240
+
241
+ post = Post("Title", "Subtitle", user_id=1)
242
+ post.from_markdown(
243
+ """
244
+ # Heading
245
+
246
+ Paragraph with **bold**, *italic*, `code`, [links](https://example.com), and footnotes.[^1]
247
+
248
+ - Lists
249
+ - Images
250
+
251
+ ![Alt](local-image.png "Caption")
252
+
253
+ [^1]: Footnote text.
254
+ """
255
+ )
256
+ ```
257
+
258
+ 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.
259
+
260
+ When an `Api` instance is passed to `from_markdown`, local image paths are uploaded before the draft is created:
261
+
262
+ ```python
263
+ post.from_markdown(markdown_content, api=api)
264
+ ```
265
+
266
+ ## YAML Drafts
267
+
268
+ ```yaml
269
+ title: "My Post Title"
270
+ subtitle: "My Post Subtitle"
271
+ audience: "everyone"
272
+ write_comment_permissions: "everyone"
273
+ search_engine_title: "SEO title"
274
+ search_engine_description: "SEO description"
275
+ slug: "my-post-title"
276
+ tags:
277
+ - python
278
+ - substack
279
+ markdown: |
280
+ # Introduction
281
+
282
+ This post body is Markdown.
283
+ ```
284
+
285
+ The lower-level node format is also supported:
286
+
287
+ ```yaml
288
+ title: "My Post Title"
289
+ subtitle: "My Post Subtitle"
290
+ body:
291
+ 0:
292
+ type: "heading"
293
+ level: 1
294
+ content: "Introduction"
295
+ 1:
296
+ type: "paragraph"
297
+ content: "This is a paragraph."
298
+ 2:
299
+ type: "captionedImage"
300
+ src: "local_image.jpg"
301
+ ```
302
+
303
+ ## MCP Server
304
+
305
+ Install the MCP extra:
306
+
307
+ ```bash
308
+ pip install "python-substack[mcp]"
309
+ ```
310
+
311
+ Run the server over stdio:
312
+
313
+ ```bash
314
+ substack-mcp
315
+ ```
316
+
317
+ Equivalent Python entry point:
318
+
319
+ ```bash
320
+ python -c "from substack_mcp.mcp_server import main; main()"
321
+ ```
322
+
323
+ Available tools:
324
+
325
+ - `post_draft_from_markdown(...)`
326
+ - `put_draft(draft_id, update_payload)`
327
+ - `add_tags(draft_id, tags)`
328
+ - `prepublish_draft(draft_id)`
329
+ - `publish_draft(draft_id, send=True, share_automatically=False)`
330
+
331
+ ## Development
332
+
333
+ ```bash
334
+ pip install pre-commit
335
+ pre-commit install
336
+ pytest
337
+ ```
338
+
339
+ Live Substack tests are opt-in. Set `RUN_SUBSTACK_E2E=1` and configure credentials before running them.
340
+
341
+ Release changes are tracked in [CHANGELOG.md](CHANGELOG.md).
342
+
343
+ ## Disclaimer
344
+
345
+ This project is not affiliated with Substack.