python-substack 0.1.24__py3-none-any.whl → 0.1.25__py3-none-any.whl

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.
@@ -1,331 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-substack
3
- Version: 0.1.24
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: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Programming Language :: Python :: 3.14
18
- Requires-Dist: PyYAML (>=6.0,<7.0)
19
- Requires-Dist: markdown-it-py (>=3.0,<4.0)
20
- Requires-Dist: mdit-py-plugins (>=0.4,<0.5)
21
- Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
22
- Requires-Dist: requests (>=2.32.0,<3.0.0)
23
- Project-URL: Homepage, https://github.com/ma2za/python-substack
24
- Project-URL: Repository, https://github.com/ma2za/python-substack
25
- Description-Content-Type: text/markdown
26
-
27
- # Python Substack
28
-
29
- This is an unofficial library providing a Python interface for [Substack](https://substack.com/).
30
- I am in no way affiliated with Substack.
31
-
32
- [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
33
- ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
34
- ---
35
-
36
- # Installation
37
-
38
- You can install python-substack using:
39
-
40
- $ pip install python-substack
41
-
42
- For the MCP server tools, install the extra dependency set:
43
-
44
- $ poetry install --with mcp
45
-
46
- > NOTE: We had to upgrade the package requirements to support Python 3.10 because 3.9 is basically vintage now. If you still run 3.9, please join us in the future (or bring snacks).
47
-
48
- ---
49
-
50
- # Setup
51
-
52
- Set the following environment variables by creating a **.env** file:
53
-
54
- EMAIL=
55
- PASSWORD=
56
- PUBLICATION_URL= # Optional: your publication URL
57
- COOKIES_PATH= # Optional: path to cookies JSON file
58
- COOKIES_STRING= # Optional: cookie string for authentication
59
-
60
- ## If you don't have a password
61
-
62
- Recently Substack has been setting up new accounts without a password. If you sign out and sign back in, it just uses
63
- your email address with a "magic" link.
64
-
65
- Set a password:
66
-
67
- - Sign out of Substack
68
- - At the sign-in page, click "Sign in with password" under the `Email` text box
69
- - Then choose, "Set a new password"
70
-
71
- The .env file will be ignored by git but always be careful.
72
-
73
- ---
74
-
75
- # Usage
76
-
77
- Check out the examples folder for some examples 😃 🚀
78
-
79
- ## Basic Authentication
80
-
81
- ```python
82
- import os
83
- from dotenv import load_dotenv
84
-
85
- from substack import Api
86
- from substack.post import Post
87
-
88
- load_dotenv()
89
-
90
- # Authenticate with email and password
91
- api = Api(
92
- email=os.getenv("EMAIL"),
93
- password=os.getenv("PASSWORD"),
94
- publication_url=os.getenv("PUBLICATION_URL"),
95
- )
96
- ```
97
-
98
- ## Cookie-based Authentication
99
-
100
- You can also authenticate using cookies instead of email/password:
101
-
102
- ```python
103
- import os
104
- from dotenv import load_dotenv
105
-
106
- from substack import Api
107
-
108
- load_dotenv()
109
-
110
- # Authenticate with cookies (alternative to email/password)
111
- api = Api(
112
- cookies_path=os.getenv("COOKIES_PATH"), # Path to cookies JSON file
113
- # OR
114
- cookies_string=os.getenv("COOKIES_STRING"), # Cookie string
115
- publication_url=os.getenv("PUBLICATION_URL"),
116
- )
117
- ```
118
-
119
- ## Creating and Publishing Posts
120
-
121
- ```python
122
- user_id = api.get_user_id()
123
-
124
- # Switch Publications - The library defaults to your user's primary publication. You can retrieve all your publications and change which one you want to use.
125
-
126
- # primary publication
127
- user_publication = api.get_user_primary_publication()
128
- # all publications
129
- user_publications = api.get_user_publications()
130
-
131
- # This step is only necessary if you are not using your primary publication
132
- # api.change_publication(user_publication)
133
-
134
- # Create a post with basic settings
135
- post = Post(
136
- title="How to publish a Substack post using the Python API",
137
- subtitle="This post was published using the Python API",
138
- user_id=user_id
139
- )
140
-
141
- # Create a post with audience and comment permissions
142
- post = Post(
143
- title="My Post Title",
144
- subtitle="My Post Subtitle",
145
- user_id=user_id,
146
- audience="everyone", # Options: "everyone", "only_paid", "founding", "only_free"
147
- write_comment_permissions="everyone" # Options: "none", "only_paid", "everyone"
148
- )
149
-
150
- post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
151
-
152
- # bolden text
153
- post.add({'type': "paragraph",
154
- 'content': [{'content': "This is how you "}, {'content': "bolden ", 'marks': [{'type': "strong"}]},
155
- {'content': "a word."}]})
156
-
157
- # add hyperlink to text
158
- post.add({'type': 'paragraph', 'content': [
159
- {'content': "View Link", 'marks': [{'type': "link", 'href': 'https://whoraised.substack.com/'}]}]})
160
-
161
- # set paywall boundary
162
- post.add({'type': 'paywall'})
163
-
164
- # add image
165
- post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
166
-
167
- # add local image
168
- image = api.get_image('image.png')
169
- post.add({"type": "captionedImage", "src": image.get("url")})
170
-
171
- # embed publication
172
- embedded = api.publication_embed("https://jackio.substack.com/")
173
- post.add({"type": "embeddedPublication", "url": embedded})
174
-
175
- # create post from Markdown
176
- markdown_content = """
177
- # My Heading
178
-
179
- This is a paragraph with **bold** and *italic* text.
180
-
181
- ![Image Alt](https://example.com/image.jpg)
182
- """
183
- post.from_markdown(markdown_content, api=api)
184
-
185
- # Markdown footnotes are supported too. References become inline anchors and
186
- # definitions become footnote blocks, numbered by order of first appearance.
187
- # Labels can be numbers or names (e.g. [^1] or [^source]).
188
- footnote_markdown = """
189
- A claim that needs support.[^1] Another, with a named label.[^source]
190
-
191
- [^1]: The supporting detail, with a [link](https://example.com).
192
- [^source]: Author, *Title* (2025).
193
- """
194
- post.from_markdown(footnote_markdown, api=api)
195
-
196
- # Or build footnotes manually:
197
- post.paragraph(content=[{"content": "Some claim."}]).footnote_anchor(1)
198
- post.footnote(1, "The note text, with **formatting** allowed.")
199
-
200
-
201
- draft = api.post_draft(post.get_draft())
202
-
203
- # set section (can only be done after first posting the draft)
204
- # post.set_section("rick rolling", api.get_sections())
205
- # api.put_draft(draft.get("id"), draft_section_id=post.draft_section_id)
206
-
207
- api.prepublish_draft(draft.get("id"))
208
-
209
- api.publish_draft(draft.get("id"))
210
- ```
211
-
212
- ## Loading Posts from YAML Files
213
-
214
- You can define your posts in YAML files for easier management:
215
-
216
- ```python
217
- import yaml
218
- import os
219
- from dotenv import load_dotenv
220
-
221
- from substack import Api
222
- from substack.post import Post
223
-
224
- load_dotenv()
225
-
226
- # Load post data from YAML file
227
- with open("draft.yaml", "r") as fp:
228
- post_data = yaml.safe_load(fp)
229
-
230
- # Authenticate (using cookies or email/password)
231
- cookies_path = os.getenv("COOKIES_PATH")
232
- cookies_string = os.getenv("COOKIES_STRING")
233
-
234
- api = Api(
235
- email=os.getenv("EMAIL") if not cookies_path and not cookies_string else None,
236
- password=os.getenv("PASSWORD") if not cookies_path and not cookies_string else None,
237
- cookies_path=cookies_path,
238
- cookies_string=cookies_string,
239
- publication_url=os.getenv("PUBLICATION_URL"),
240
- )
241
-
242
- user_id = api.get_user_id()
243
-
244
- # Create post from YAML data
245
- post = Post(
246
- post_data.get("title"),
247
- post_data.get("subtitle", ""),
248
- user_id,
249
- audience=post_data.get("audience", "everyone"),
250
- write_comment_permissions=post_data.get("write_comment_permissions", "everyone"),
251
- )
252
-
253
- # Add body content from YAML
254
- body = post_data.get("body", {})
255
- for _, item in body.items():
256
- # Handle local images - upload them first
257
- if item.get("type") == "captionedImage" and not item.get("src").startswith("http"):
258
- image = api.get_image(item.get("src"))
259
- item.update({"src": image.get("url")})
260
- post.add(item)
261
-
262
- draft = api.post_draft(post.get_draft())
263
- put_draft_kwargs = {
264
- "draft_section_id": post.draft_section_id,
265
- "search_engine_title": post_data.get("search_engine_title"),
266
- "search_engine_description": post_data.get("search_engine_description"),
267
- "slug": post_data.get("slug"),
268
- }
269
- put_draft_kwargs = {k: v for k, v in put_draft_kwargs.items() if v is not None}
270
- api.put_draft(draft.get("id"), **put_draft_kwargs)
271
-
272
- # Publish the draft
273
- api.prepublish_draft(draft.get("id"))
274
- api.publish_draft(draft.get("id"))
275
- ```
276
-
277
- Example YAML structure:
278
-
279
- ```yaml
280
- title: "My Post Title"
281
- subtitle: "My Post Subtitle"
282
- audience: "everyone" # everyone, only_paid, founding, only_free
283
- write_comment_permissions: "everyone" # none, only_paid, everyone
284
- section: "my-section"
285
- body:
286
- 0:
287
- type: "heading"
288
- level: 1
289
- content: "Introduction"
290
- 1:
291
- type: "paragraph"
292
- content: "This is a paragraph."
293
- 2:
294
- type: "captionedImage"
295
- src: "local_image.jpg" # Local images will be uploaded automatically
296
- ```
297
-
298
- ## MCP FastMCP server
299
-
300
- This package now includes a FastMCP server in `substack/mcp_fastmcp.py` with the following tools:
301
-
302
- - `post_draft_from_markdown(...)`: create draft from markdown, optional tag/add/prepublish/publish, and control send/share_automatically.
303
- - `put_draft(draft_id, update_payload)`: update draft fields.
304
- - `add_tags(draft_id, tags)`: add tags to a draft/post.
305
- - `prepublish_draft(draft_id)`: prepublish a draft.
306
- - `publish_draft(draft_id, send=True, share_automatically=False)`: publish a draft.
307
-
308
- Use via stdio transport:
309
-
310
- ```bash
311
- python -c "from substack.mcp_fastmcp import main; main()"
312
- ```
313
-
314
- # Contributing
315
-
316
- Install pre-commit:
317
-
318
- ```shell
319
- pip install pre-commit
320
- ```
321
-
322
- Set up pre-commit
323
-
324
- ```shell
325
- pre-commit install
326
- ```
327
-
328
- ## Cookie Help
329
-
330
- To get a cookie string, after login, go to dev tools (F12), network tab, refresh and find one of the requests like subscription/unred/subscriptions, right click and copy as fetch (Node.js), paste somewhere and get the entire cookie string assigned to the cookie header and put it in the env variables as COOKIES_STRING, et voila!
331
-
@@ -1,10 +0,0 @@
1
- substack/__init__.py,sha256=esdUffBuSMESXcG4GDcHU81TIPXLyDosmBN3OCtAFY4,390
2
- substack/api.py,sha256=VPzQ_bpqGcE4ZXWD2lUppnvWsuLRjvtBN7bHmW4Bgww,20422
3
- substack/exceptions.py,sha256=BbP5W5UpzFcM5SYIxx6snWD_Rmj7F_YjYIYC_r03gZY,911
4
- substack/mdrender.py,sha256=cB0fLFzOF7CmWHk-9eRvQormP8StqIgy9hYE6CSKPH0,7770
5
- substack/nodes.py,sha256=eFVxoVwi684g_DLbz8BRHfU7nyHsJb-m8w17UHocXgY,4106
6
- substack/post.py,sha256=nXeZMAZ6-lx1Qf4yuAlrEQ0lcWFYF2afai1_TxVEl0Q,19704
7
- python_substack-0.1.24.dist-info/METADATA,sha256=0SKSa0SYQ9Ju5jy7br0JOwAKKIttnCmGd70qMrcQmDk,9868
8
- python_substack-0.1.24.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
9
- python_substack-0.1.24.dist-info/licenses/LICENSE,sha256=L6jk148I5HhhVbfUvkO3EO7eAoU5zToLio4-ApkCkxg,1062
10
- python_substack-0.1.24.dist-info/RECORD,,