python-substack 0.1.12__tar.gz → 0.1.14__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.12 → python_substack-0.1.14}/PKG-INFO +57 -15
- python_substack-0.1.14/README.md +118 -0
- {python_substack-0.1.12 → python_substack-0.1.14}/pyproject.toml +2 -2
- {python_substack-0.1.12 → python_substack-0.1.14}/substack/api.py +213 -32
- {python_substack-0.1.12 → python_substack-0.1.14}/substack/exceptions.py +4 -0
- {python_substack-0.1.12 → python_substack-0.1.14}/substack/post.py +58 -22
- python_substack-0.1.12/README.md +0 -76
- {python_substack-0.1.12 → python_substack-0.1.14}/LICENSE +0 -0
- {python_substack-0.1.12 → python_substack-0.1.14}/substack/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-substack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Summary: A Python wrapper around the Substack API.
|
|
5
5
|
Home-page: https://github.com/ma2za/python-substack
|
|
6
6
|
License: MIT
|
|
@@ -44,15 +44,19 @@ You can install python-substack using:
|
|
|
44
44
|
|
|
45
45
|
Set the following environment variables by creating a **.env** file:
|
|
46
46
|
|
|
47
|
-
PUBLICATION_URL=https://ma2za.substack.com
|
|
48
47
|
EMAIL=
|
|
49
48
|
PASSWORD=
|
|
50
|
-
USER_ID=
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
## If you don't have a password
|
|
51
|
+
|
|
52
|
+
Recently Substack has been setting up new accounts without a password. If you sign-out and sign back in it just uses
|
|
53
|
+
your email address with a "magic" link.
|
|
54
|
+
|
|
55
|
+
Set a password:
|
|
56
|
+
|
|
57
|
+
- Sign-out of Substack
|
|
58
|
+
- At the sign-in page click, "Sign in with password" under the `Email` text box
|
|
59
|
+
- Then choose, "Set a new password"
|
|
56
60
|
|
|
57
61
|
The .env file will be ignored by git but always be careful.
|
|
58
62
|
|
|
@@ -71,31 +75,69 @@ from substack.post import Post
|
|
|
71
75
|
api = Api(
|
|
72
76
|
email=os.getenv("EMAIL"),
|
|
73
77
|
password=os.getenv("PASSWORD"),
|
|
74
|
-
publication_url=os.getenv("PUBLICATION_URL"),
|
|
75
78
|
)
|
|
76
79
|
|
|
80
|
+
user_id = api.get_user_id()
|
|
81
|
+
|
|
82
|
+
# Switch Publications - The library defaults to your users primary publication. You can retrieve all your publications and change which one you want to use.
|
|
83
|
+
|
|
84
|
+
# primary publication
|
|
85
|
+
user_publication = api.get_user_primary_publication()
|
|
86
|
+
# all publications
|
|
87
|
+
user_publications = api.get_user_publications()
|
|
88
|
+
|
|
89
|
+
# This step is only necessary if you are not using your primary publication
|
|
90
|
+
# api.change_publication(user_publication)
|
|
91
|
+
|
|
77
92
|
post = Post(
|
|
78
93
|
title="How to publish a Substack post using the Python API",
|
|
79
94
|
subtitle="This post was published using the Python API",
|
|
80
|
-
user_id=
|
|
95
|
+
user_id=user_id
|
|
81
96
|
)
|
|
82
97
|
|
|
83
98
|
post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
|
|
84
99
|
|
|
85
|
-
#bolden text
|
|
86
|
-
post.add({'type': "paragraph",
|
|
100
|
+
# bolden text
|
|
101
|
+
post.add({'type': "paragraph",
|
|
102
|
+
'content': [{'content': "This is how you "}, {'content': "bolden ", 'marks': [{'type': "strong"}]},
|
|
103
|
+
{'content': "a word."}]})
|
|
104
|
+
|
|
105
|
+
# add hyperlink to text
|
|
106
|
+
post.add({'type': 'paragraph', 'content': [
|
|
107
|
+
{'content': "View Link", 'marks': [{'type': "link", 'href': 'https://whoraised.substack.com/'}]}]})
|
|
87
108
|
|
|
88
|
-
#
|
|
89
|
-
post.add({'type':
|
|
109
|
+
# set paywall boundary
|
|
110
|
+
post.add({'type': 'paywall'})
|
|
90
111
|
|
|
91
|
-
#add image
|
|
92
|
-
post.add({'type':'captionedImage','src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
112
|
+
# add image
|
|
113
|
+
post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
114
|
+
|
|
115
|
+
# add local image
|
|
116
|
+
image = api.get_image('image.png')
|
|
117
|
+
post.add({"type": "captionedImage", "src": image.get("url")})
|
|
93
118
|
|
|
94
119
|
draft = api.post_draft(post.get_draft())
|
|
95
120
|
|
|
121
|
+
# set section (THIS CAN BE DONE ONLY AFTER HAVING FIRST POSTED THE DRAFT)
|
|
122
|
+
post.set_section("rick rolling", api.get_sections())
|
|
123
|
+
api.put_draft(draft.get("id"), draft_section_id=post.draft_section_id)
|
|
124
|
+
|
|
96
125
|
api.prepublish_draft(draft.get("id"))
|
|
97
126
|
|
|
98
127
|
api.publish_draft(draft.get("id"))
|
|
99
128
|
```
|
|
100
129
|
|
|
130
|
+
# Contributing
|
|
131
|
+
|
|
132
|
+
Install pre-commit:
|
|
133
|
+
|
|
134
|
+
```shell
|
|
135
|
+
pip install pre-commit
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Set up pre-commit
|
|
139
|
+
|
|
140
|
+
```shell
|
|
141
|
+
pre-commit install
|
|
142
|
+
```
|
|
101
143
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Python Substack
|
|
2
|
+
|
|
3
|
+
This is an unofficial library providing a Python interface for [Substack](https://substack.com/).
|
|
4
|
+
I am in no way affiliated with Substack.
|
|
5
|
+
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://pepy.tech/project/python-substack)
|
|
8
|
+

|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Installation
|
|
12
|
+
|
|
13
|
+
You can install python-substack using:
|
|
14
|
+
|
|
15
|
+
$ pip install python-substack
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Setup
|
|
20
|
+
|
|
21
|
+
Set the following environment variables by creating a **.env** file:
|
|
22
|
+
|
|
23
|
+
EMAIL=
|
|
24
|
+
PASSWORD=
|
|
25
|
+
|
|
26
|
+
## If you don't have a password
|
|
27
|
+
|
|
28
|
+
Recently Substack has been setting up new accounts without a password. If you sign-out and sign back in it just uses
|
|
29
|
+
your email address with a "magic" link.
|
|
30
|
+
|
|
31
|
+
Set a password:
|
|
32
|
+
|
|
33
|
+
- Sign-out of Substack
|
|
34
|
+
- At the sign-in page click, "Sign in with password" under the `Email` text box
|
|
35
|
+
- Then choose, "Set a new password"
|
|
36
|
+
|
|
37
|
+
The .env file will be ignored by git but always be careful.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# Usage
|
|
42
|
+
|
|
43
|
+
Check out the examples folder for some examples 😃 🚀
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
from substack import Api
|
|
49
|
+
from substack.post import Post
|
|
50
|
+
|
|
51
|
+
api = Api(
|
|
52
|
+
email=os.getenv("EMAIL"),
|
|
53
|
+
password=os.getenv("PASSWORD"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
user_id = api.get_user_id()
|
|
57
|
+
|
|
58
|
+
# Switch Publications - The library defaults to your users primary publication. You can retrieve all your publications and change which one you want to use.
|
|
59
|
+
|
|
60
|
+
# primary publication
|
|
61
|
+
user_publication = api.get_user_primary_publication()
|
|
62
|
+
# all publications
|
|
63
|
+
user_publications = api.get_user_publications()
|
|
64
|
+
|
|
65
|
+
# This step is only necessary if you are not using your primary publication
|
|
66
|
+
# api.change_publication(user_publication)
|
|
67
|
+
|
|
68
|
+
post = Post(
|
|
69
|
+
title="How to publish a Substack post using the Python API",
|
|
70
|
+
subtitle="This post was published using the Python API",
|
|
71
|
+
user_id=user_id
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
|
|
75
|
+
|
|
76
|
+
# bolden text
|
|
77
|
+
post.add({'type': "paragraph",
|
|
78
|
+
'content': [{'content': "This is how you "}, {'content': "bolden ", 'marks': [{'type': "strong"}]},
|
|
79
|
+
{'content': "a word."}]})
|
|
80
|
+
|
|
81
|
+
# add hyperlink to text
|
|
82
|
+
post.add({'type': 'paragraph', 'content': [
|
|
83
|
+
{'content': "View Link", 'marks': [{'type': "link", 'href': 'https://whoraised.substack.com/'}]}]})
|
|
84
|
+
|
|
85
|
+
# set paywall boundary
|
|
86
|
+
post.add({'type': 'paywall'})
|
|
87
|
+
|
|
88
|
+
# add image
|
|
89
|
+
post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
90
|
+
|
|
91
|
+
# add local image
|
|
92
|
+
image = api.get_image('image.png')
|
|
93
|
+
post.add({"type": "captionedImage", "src": image.get("url")})
|
|
94
|
+
|
|
95
|
+
draft = api.post_draft(post.get_draft())
|
|
96
|
+
|
|
97
|
+
# set section (THIS CAN BE DONE ONLY AFTER HAVING FIRST POSTED THE DRAFT)
|
|
98
|
+
post.set_section("rick rolling", api.get_sections())
|
|
99
|
+
api.put_draft(draft.get("id"), draft_section_id=post.draft_section_id)
|
|
100
|
+
|
|
101
|
+
api.prepublish_draft(draft.get("id"))
|
|
102
|
+
|
|
103
|
+
api.publish_draft(draft.get("id"))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
# Contributing
|
|
107
|
+
|
|
108
|
+
Install pre-commit:
|
|
109
|
+
|
|
110
|
+
```shell
|
|
111
|
+
pip install pre-commit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Set up pre-commit
|
|
115
|
+
|
|
116
|
+
```shell
|
|
117
|
+
pre-commit install
|
|
118
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-substack"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.14"
|
|
4
4
|
description = "A Python wrapper around the Substack API."
|
|
5
5
|
authors = ["Paolo Mazza <mazzapaolo2019@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -28,4 +28,4 @@ PyYAML = "^6.0"
|
|
|
28
28
|
|
|
29
29
|
[build-system]
|
|
30
30
|
requires = ["poetry-core>=1.0.0"]
|
|
31
|
-
build-backend = "poetry.core.masonry.api"
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
API Wrapper
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import base64
|
|
8
|
+
import json
|
|
2
9
|
import logging
|
|
3
10
|
import os
|
|
4
11
|
from datetime import datetime
|
|
@@ -21,12 +28,13 @@ class Api:
|
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
def __init__(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
self,
|
|
32
|
+
email=None,
|
|
33
|
+
password=None,
|
|
34
|
+
cookies_path=None,
|
|
35
|
+
base_url=None,
|
|
36
|
+
publication_url=None,
|
|
37
|
+
debug=False,
|
|
30
38
|
):
|
|
31
39
|
"""
|
|
32
40
|
|
|
@@ -37,12 +45,15 @@ class Api:
|
|
|
37
45
|
Args:
|
|
38
46
|
email:
|
|
39
47
|
password:
|
|
48
|
+
cookies_path
|
|
49
|
+
To re-use your session without logging in each time, you can save your cookies to a json file and
|
|
50
|
+
then load them in the next session.
|
|
51
|
+
Make sure to re-save your cookies, as they do update over time.
|
|
40
52
|
base_url:
|
|
41
53
|
The base URL to use to contact the Substack API.
|
|
42
54
|
Defaults to https://substack.com/api/v1.
|
|
43
55
|
"""
|
|
44
56
|
self.base_url = base_url or "https://substack.com/api/v1"
|
|
45
|
-
self.publication_url = urljoin(publication_url, "api/v1")
|
|
46
57
|
|
|
47
58
|
if debug:
|
|
48
59
|
logging.basicConfig()
|
|
@@ -50,8 +61,42 @@ class Api:
|
|
|
50
61
|
|
|
51
62
|
self._session = requests.Session()
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
# Load cookies from file if provided
|
|
65
|
+
# Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud
|
|
66
|
+
if cookies_path is not None:
|
|
67
|
+
with open(cookies_path) as f:
|
|
68
|
+
cookies = json.load(f)
|
|
69
|
+
self._session.cookies.update(cookies)
|
|
70
|
+
|
|
71
|
+
elif email is not None and password is not None:
|
|
54
72
|
self.login(email, password)
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Must provide email and password or cookies_path to authenticate."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
user_publication = None
|
|
79
|
+
# if the user provided a publication url, then use that
|
|
80
|
+
if publication_url:
|
|
81
|
+
import re
|
|
82
|
+
|
|
83
|
+
# Regular expression to extract subdomain name
|
|
84
|
+
match = re.search(r"https://(.*).substack.com", publication_url.lower())
|
|
85
|
+
subdomain = match.group(1) if match else None
|
|
86
|
+
|
|
87
|
+
user_publications = self.get_user_publications()
|
|
88
|
+
# search through publications to find the publication with the matching subdomain
|
|
89
|
+
for publication in user_publications:
|
|
90
|
+
if publication["subdomain"] == subdomain:
|
|
91
|
+
# set the current publication to the users publication
|
|
92
|
+
user_publication = publication
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
# get the users primary publication
|
|
96
|
+
user_publication = self.get_user_primary_publication()
|
|
97
|
+
|
|
98
|
+
# set the current publication to the users primary publication
|
|
99
|
+
self.change_publication(user_publication)
|
|
55
100
|
|
|
56
101
|
def login(self, email, password) -> dict:
|
|
57
102
|
"""
|
|
@@ -73,8 +118,37 @@ class Api:
|
|
|
73
118
|
"redirect": "/",
|
|
74
119
|
},
|
|
75
120
|
)
|
|
121
|
+
|
|
122
|
+
return Api._handle_response(response=response)
|
|
123
|
+
|
|
124
|
+
def signin_for_pub(self, publication):
|
|
125
|
+
"""
|
|
126
|
+
Complete the signin process
|
|
127
|
+
"""
|
|
128
|
+
response = self._session.get(
|
|
129
|
+
f"https://substack.com/sign-in?redirect=%2F&for_pub={publication['subdomain']}",
|
|
130
|
+
)
|
|
76
131
|
return Api._handle_response(response=response)
|
|
77
132
|
|
|
133
|
+
def change_publication(self, publication):
|
|
134
|
+
"""
|
|
135
|
+
Change the publication URL
|
|
136
|
+
"""
|
|
137
|
+
self.publication_url = urljoin(publication["publication_url"], "api/v1")
|
|
138
|
+
|
|
139
|
+
# sign-in to the publication
|
|
140
|
+
self.signin_for_pub(publication)
|
|
141
|
+
|
|
142
|
+
def export_cookies(self, path: str = "cookies.json"):
|
|
143
|
+
"""
|
|
144
|
+
Export cookies to a json file.
|
|
145
|
+
Args:
|
|
146
|
+
path: path to the json file
|
|
147
|
+
"""
|
|
148
|
+
cookies = self._session.cookies.get_dict()
|
|
149
|
+
with open(path, "w") as f:
|
|
150
|
+
json.dump(cookies, f)
|
|
151
|
+
|
|
78
152
|
@staticmethod
|
|
79
153
|
def _handle_response(response: requests.Response):
|
|
80
154
|
"""
|
|
@@ -92,6 +166,82 @@ class Api:
|
|
|
92
166
|
except ValueError:
|
|
93
167
|
raise SubstackRequestException("Invalid Response: %s" % response.text)
|
|
94
168
|
|
|
169
|
+
def get_user_id(self):
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
profile = self.get_user_profile()
|
|
176
|
+
user_id = profile["id"]
|
|
177
|
+
|
|
178
|
+
return user_id
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def get_publication_url(publication: dict) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Gets the publication url
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
publication:
|
|
187
|
+
"""
|
|
188
|
+
custom_domain = publication["custom_domain"]
|
|
189
|
+
if not custom_domain:
|
|
190
|
+
publication_url = f"https://{publication['subdomain']}.substack.com"
|
|
191
|
+
else:
|
|
192
|
+
publication_url = f"https://{custom_domain}"
|
|
193
|
+
|
|
194
|
+
return publication_url
|
|
195
|
+
|
|
196
|
+
def get_user_primary_publication(self):
|
|
197
|
+
"""
|
|
198
|
+
Gets the users primary publication
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
profile = self.get_user_profile()
|
|
202
|
+
primary_publication = profile["primaryPublication"]
|
|
203
|
+
primary_publication["publication_url"] = self.get_publication_url(
|
|
204
|
+
primary_publication
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return primary_publication
|
|
208
|
+
|
|
209
|
+
def get_user_publications(self):
|
|
210
|
+
"""
|
|
211
|
+
Gets the users publications
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
profile = self.get_user_profile()
|
|
215
|
+
|
|
216
|
+
# Loop through users "publicationUsers" list, and return a list
|
|
217
|
+
# of dictionaries of "name", and "subdomain", and "id"
|
|
218
|
+
user_publications = []
|
|
219
|
+
for publication in profile["publicationUsers"]:
|
|
220
|
+
pub = publication["publication"]
|
|
221
|
+
pub["publication_url"] = self.get_publication_url(pub)
|
|
222
|
+
user_publications.append(pub)
|
|
223
|
+
|
|
224
|
+
return user_publications
|
|
225
|
+
|
|
226
|
+
def get_user_profile(self):
|
|
227
|
+
"""
|
|
228
|
+
Gets the users profile
|
|
229
|
+
"""
|
|
230
|
+
response = self._session.get(f"{self.base_url}/user/profile/self")
|
|
231
|
+
|
|
232
|
+
return Api._handle_response(response=response)
|
|
233
|
+
|
|
234
|
+
def get_user_settings(self):
|
|
235
|
+
"""
|
|
236
|
+
Get list of users.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
response = self._session.get(f"{self.base_url}/settings")
|
|
242
|
+
|
|
243
|
+
return Api._handle_response(response=response)
|
|
244
|
+
|
|
95
245
|
def get_publication_users(self):
|
|
96
246
|
"""
|
|
97
247
|
Get list of users.
|
|
@@ -111,9 +261,29 @@ class Api:
|
|
|
111
261
|
Returns:
|
|
112
262
|
|
|
113
263
|
"""
|
|
114
|
-
response = self._session.get(
|
|
264
|
+
response = self._session.get(
|
|
265
|
+
f"{self.publication_url}/publication_launch_checklist"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return Api._handle_response(response=response)["subscriberCount"]
|
|
269
|
+
|
|
270
|
+
def get_published_posts(
|
|
271
|
+
self, offset=0, limit=25, order_by="post_date", order_direction="desc"
|
|
272
|
+
):
|
|
273
|
+
"""
|
|
274
|
+
Get list of published posts for the publication.
|
|
275
|
+
"""
|
|
276
|
+
response = self._session.get(
|
|
277
|
+
f"{self.publication_url}/post_management/published",
|
|
278
|
+
params={
|
|
279
|
+
"offset": offset,
|
|
280
|
+
"limit": limit,
|
|
281
|
+
"order_by": order_by,
|
|
282
|
+
"order_direction": order_direction,
|
|
283
|
+
},
|
|
284
|
+
)
|
|
115
285
|
|
|
116
|
-
return Api._handle_response(response=response)
|
|
286
|
+
return Api._handle_response(response=response)
|
|
117
287
|
|
|
118
288
|
def get_posts(self) -> dict:
|
|
119
289
|
"""
|
|
@@ -142,6 +312,14 @@ class Api:
|
|
|
142
312
|
)
|
|
143
313
|
return Api._handle_response(response=response)
|
|
144
314
|
|
|
315
|
+
def get_draft(self, draft_id):
|
|
316
|
+
"""
|
|
317
|
+
Gets a draft given it's id.
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
response = self._session.get(f"{self.publication_url}/drafts/{draft_id}")
|
|
321
|
+
return Api._handle_response(response=response)
|
|
322
|
+
|
|
145
323
|
def delete_draft(self, draft_id):
|
|
146
324
|
"""
|
|
147
325
|
|
|
@@ -166,35 +344,19 @@ class Api:
|
|
|
166
344
|
response = self._session.post(f"{self.publication_url}/drafts", json=body)
|
|
167
345
|
return Api._handle_response(response=response)
|
|
168
346
|
|
|
169
|
-
def put_draft(
|
|
170
|
-
self,
|
|
171
|
-
draft,
|
|
172
|
-
title=None,
|
|
173
|
-
subtitle=None,
|
|
174
|
-
body=None,
|
|
175
|
-
cover_image=None,
|
|
176
|
-
) -> dict:
|
|
347
|
+
def put_draft(self, draft, **kwargs) -> dict:
|
|
177
348
|
"""
|
|
178
349
|
|
|
179
350
|
Args:
|
|
180
|
-
draft:
|
|
181
|
-
|
|
182
|
-
subtitle:
|
|
183
|
-
body:
|
|
184
|
-
cover_image:
|
|
351
|
+
draft:
|
|
352
|
+
**kwargs:
|
|
185
353
|
|
|
186
354
|
Returns:
|
|
187
355
|
|
|
188
356
|
"""
|
|
189
|
-
|
|
190
357
|
response = self._session.put(
|
|
191
358
|
f"{self.publication_url}/drafts/{draft}",
|
|
192
|
-
json=
|
|
193
|
-
"draft_title": title,
|
|
194
|
-
"draft_subtitle": subtitle,
|
|
195
|
-
"draft_body": body,
|
|
196
|
-
"cover_image": cover_image,
|
|
197
|
-
},
|
|
359
|
+
json=kwargs,
|
|
198
360
|
)
|
|
199
361
|
return Api._handle_response(response=response)
|
|
200
362
|
|
|
@@ -214,7 +376,7 @@ class Api:
|
|
|
214
376
|
return Api._handle_response(response=response)
|
|
215
377
|
|
|
216
378
|
def publish_draft(
|
|
217
|
-
|
|
379
|
+
self, draft, send: bool = True, share_automatically: bool = False
|
|
218
380
|
) -> dict:
|
|
219
381
|
"""
|
|
220
382
|
|
|
@@ -332,7 +494,7 @@ class Api:
|
|
|
332
494
|
page_output = self.get_category(category_id, category_type, page)
|
|
333
495
|
publications.extend(page_output.get("publications", []))
|
|
334
496
|
if (
|
|
335
|
-
|
|
497
|
+
limit is not None and limit <= len(publications)
|
|
336
498
|
) or not page_output.get("more", False):
|
|
337
499
|
publications = publications[:limit]
|
|
338
500
|
break
|
|
@@ -357,3 +519,22 @@ class Api:
|
|
|
357
519
|
for draft in drafts:
|
|
358
520
|
response = self.delete_draft(draft.get("id"))
|
|
359
521
|
return response
|
|
522
|
+
|
|
523
|
+
def get_sections(self):
|
|
524
|
+
"""
|
|
525
|
+
Get a list of the sections of your publication.
|
|
526
|
+
|
|
527
|
+
TODO: this is hacky but I cannot find another place where to get the sections.
|
|
528
|
+
Returns:
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
response = self._session.get(
|
|
532
|
+
f"{self.publication_url}/subscriptions",
|
|
533
|
+
)
|
|
534
|
+
content = Api._handle_response(response=response)
|
|
535
|
+
sections = [
|
|
536
|
+
p.get("sections")
|
|
537
|
+
for p in content.get("publications")
|
|
538
|
+
if p.get("hostname") in self.publication_url
|
|
539
|
+
]
|
|
540
|
+
return sections[0]
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Post Utilities
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import json
|
|
2
8
|
from typing import Dict
|
|
3
9
|
|
|
4
10
|
__all__ = ["Post"]
|
|
5
11
|
|
|
12
|
+
from substack.exceptions import SectionNotExistsException
|
|
13
|
+
|
|
6
14
|
|
|
7
15
|
class Post:
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
Post utility class
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
8
22
|
def __init__(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
self,
|
|
24
|
+
title: str,
|
|
25
|
+
subtitle: str,
|
|
26
|
+
user_id,
|
|
27
|
+
audience: str = None,
|
|
28
|
+
write_comment_permissions: str = None,
|
|
15
29
|
):
|
|
16
30
|
"""
|
|
17
31
|
|
|
@@ -27,6 +41,8 @@ class Post:
|
|
|
27
41
|
self.draft_body = {"type": "doc", "content": []}
|
|
28
42
|
self.draft_bylines = [{"id": int(user_id), "is_guest": False}]
|
|
29
43
|
self.audience = audience if audience is not None else "everyone"
|
|
44
|
+
self.draft_section_id = None
|
|
45
|
+
self.section_chosen = True
|
|
30
46
|
|
|
31
47
|
# TODO better understand the possible values and combinations with audience
|
|
32
48
|
if write_comment_permissions is not None:
|
|
@@ -34,6 +50,22 @@ class Post:
|
|
|
34
50
|
else:
|
|
35
51
|
self.write_comment_permissions = self.audience
|
|
36
52
|
|
|
53
|
+
def set_section(self, name: str, sections: list):
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name:
|
|
58
|
+
sections:
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
section = [s for s in sections if s.get("name") == name]
|
|
64
|
+
if len(section) != 1:
|
|
65
|
+
raise SectionNotExistsException(name)
|
|
66
|
+
section = section[0]
|
|
67
|
+
self.draft_section_id = section.get("id")
|
|
68
|
+
|
|
37
69
|
def add(self, item: Dict):
|
|
38
70
|
"""
|
|
39
71
|
|
|
@@ -123,20 +155,20 @@ class Post:
|
|
|
123
155
|
return self
|
|
124
156
|
|
|
125
157
|
def captioned_image(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
self,
|
|
159
|
+
src: str,
|
|
160
|
+
fullscreen: bool = False,
|
|
161
|
+
imageSize: str = "normal",
|
|
162
|
+
height: int = 819,
|
|
163
|
+
width: int = 1456,
|
|
164
|
+
resizeWidth: int = 728,
|
|
165
|
+
bytes: str = None,
|
|
166
|
+
alt: str = None,
|
|
167
|
+
title: str = None,
|
|
168
|
+
type: str = None,
|
|
169
|
+
href: str = None,
|
|
170
|
+
belowTheFold: bool = False,
|
|
171
|
+
internalRedirect: str = None,
|
|
140
172
|
):
|
|
141
173
|
"""
|
|
142
174
|
|
|
@@ -232,7 +264,7 @@ class Post:
|
|
|
232
264
|
return self
|
|
233
265
|
|
|
234
266
|
def remove_last_paragraph(self):
|
|
235
|
-
""" """
|
|
267
|
+
"""Remove last paragraph"""
|
|
236
268
|
del self.draft_body.get("content")[-1]
|
|
237
269
|
|
|
238
270
|
def get_draft(self):
|
|
@@ -262,7 +294,11 @@ class Post:
|
|
|
262
294
|
Subscribe for free to receive new posts and support my work."""
|
|
263
295
|
|
|
264
296
|
subscribe = self.draft_body["content"][-1]
|
|
265
|
-
subscribe["attrs"] = {
|
|
297
|
+
subscribe["attrs"] = {
|
|
298
|
+
"url": "%%checkout_url%%",
|
|
299
|
+
"text": "Subscribe",
|
|
300
|
+
"language": "en",
|
|
301
|
+
}
|
|
266
302
|
subscribe["content"] = [
|
|
267
303
|
{
|
|
268
304
|
"type": "ctaCaption",
|
python_substack-0.1.12/README.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Python Substack
|
|
2
|
-
|
|
3
|
-
This is an unofficial library providing a Python interface for [Substack](https://substack.com/).
|
|
4
|
-
I am in no way affiliated with Substack.
|
|
5
|
-
|
|
6
|
-
[](https://www.python.org/downloads/)
|
|
7
|
-
[](https://pepy.tech/project/python-substack)
|
|
8
|
-

|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Installation
|
|
12
|
-
|
|
13
|
-
You can install python-substack using:
|
|
14
|
-
|
|
15
|
-
$ pip install python-substack
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
# Setup
|
|
20
|
-
|
|
21
|
-
Set the following environment variables by creating a **.env** file:
|
|
22
|
-
|
|
23
|
-
PUBLICATION_URL=https://ma2za.substack.com
|
|
24
|
-
EMAIL=
|
|
25
|
-
PASSWORD=
|
|
26
|
-
USER_ID=
|
|
27
|
-
|
|
28
|
-
To discover the USER_ID go to your public profile page,
|
|
29
|
-
in the URL bar of the browser you find the substack address
|
|
30
|
-
followed by your USER_ID and your username:
|
|
31
|
-
https://substack.com/profile/[USER_ID]-[username]
|
|
32
|
-
|
|
33
|
-
The .env file will be ignored by git but always be careful.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
# Usage
|
|
38
|
-
|
|
39
|
-
Check out the examples folder for some examples 😃 🚀
|
|
40
|
-
|
|
41
|
-
```python
|
|
42
|
-
import os
|
|
43
|
-
|
|
44
|
-
from substack import Api
|
|
45
|
-
from substack.post import Post
|
|
46
|
-
|
|
47
|
-
api = Api(
|
|
48
|
-
email=os.getenv("EMAIL"),
|
|
49
|
-
password=os.getenv("PASSWORD"),
|
|
50
|
-
publication_url=os.getenv("PUBLICATION_URL"),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
post = Post(
|
|
54
|
-
title="How to publish a Substack post using the Python API",
|
|
55
|
-
subtitle="This post was published using the Python API",
|
|
56
|
-
user_id=os.getenv("USER_ID")
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
|
|
60
|
-
|
|
61
|
-
#bolden text
|
|
62
|
-
post.add({'type': "paragraph",'content':[{'content': "This is how you "},{'content': "bolden ",'marks':[{'type': "strong"}]},{'content': "a word."}]})
|
|
63
|
-
|
|
64
|
-
#add hyperlink to text
|
|
65
|
-
post.add({'type':'paragraph','content':[{'content': "View Link",'marks':[{'type': "link",'href':'https://whoraised.substack.com/'}]}]})
|
|
66
|
-
|
|
67
|
-
#add image
|
|
68
|
-
post.add({'type':'captionedImage','src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
69
|
-
|
|
70
|
-
draft = api.post_draft(post.get_draft())
|
|
71
|
-
|
|
72
|
-
api.prepublish_draft(draft.get("id"))
|
|
73
|
-
|
|
74
|
-
api.publish_draft(draft.get("id"))
|
|
75
|
-
```
|
|
76
|
-
|
|
File without changes
|
|
File without changes
|