python-substack 0.1.13__tar.gz → 0.1.15__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.13 → python_substack-0.1.15}/PKG-INFO +44 -9
- {python_substack-0.1.13 → python_substack-0.1.15}/README.md +43 -8
- {python_substack-0.1.13 → python_substack-0.1.15}/pyproject.toml +2 -2
- {python_substack-0.1.13 → python_substack-0.1.15}/substack/api.py +229 -18
- {python_substack-0.1.13 → python_substack-0.1.15}/substack/post.py +40 -22
- {python_substack-0.1.13 → python_substack-0.1.15}/LICENSE +0 -0
- {python_substack-0.1.13 → python_substack-0.1.15}/substack/__init__.py +0 -0
- {python_substack-0.1.13 → python_substack-0.1.15}/substack/exceptions.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.15
|
|
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,13 +75,24 @@ 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!'})
|
|
@@ -91,6 +106,9 @@ post.add({'type': "paragraph",
|
|
|
91
106
|
post.add({'type': 'paragraph', 'content': [
|
|
92
107
|
{'content': "View Link", 'marks': [{'type': "link", 'href': 'https://whoraised.substack.com/'}]}]})
|
|
93
108
|
|
|
109
|
+
# set paywall boundary
|
|
110
|
+
post.add({'type': 'paywall'})
|
|
111
|
+
|
|
94
112
|
# add image
|
|
95
113
|
post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
96
114
|
|
|
@@ -98,6 +116,10 @@ post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsA
|
|
|
98
116
|
image = api.get_image('image.png')
|
|
99
117
|
post.add({"type": "captionedImage", "src": image.get("url")})
|
|
100
118
|
|
|
119
|
+
# embed publication
|
|
120
|
+
embedded = api.publication_embed("https://jackio.substack.com/")
|
|
121
|
+
post.add({"type": "embeddedPublication", "url": embedded})
|
|
122
|
+
|
|
101
123
|
draft = api.post_draft(post.get_draft())
|
|
102
124
|
|
|
103
125
|
# set section (THIS CAN BE DONE ONLY AFTER HAVING FIRST POSTED THE DRAFT)
|
|
@@ -109,4 +131,17 @@ api.prepublish_draft(draft.get("id"))
|
|
|
109
131
|
api.publish_draft(draft.get("id"))
|
|
110
132
|
```
|
|
111
133
|
|
|
134
|
+
# Contributing
|
|
135
|
+
|
|
136
|
+
Install pre-commit:
|
|
137
|
+
|
|
138
|
+
```shell
|
|
139
|
+
pip install pre-commit
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Set up pre-commit
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
pre-commit install
|
|
146
|
+
```
|
|
112
147
|
|
|
@@ -20,15 +20,19 @@ You can install python-substack using:
|
|
|
20
20
|
|
|
21
21
|
Set the following environment variables by creating a **.env** file:
|
|
22
22
|
|
|
23
|
-
PUBLICATION_URL=https://ma2za.substack.com
|
|
24
23
|
EMAIL=
|
|
25
24
|
PASSWORD=
|
|
26
|
-
USER_ID=
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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"
|
|
32
36
|
|
|
33
37
|
The .env file will be ignored by git but always be careful.
|
|
34
38
|
|
|
@@ -47,13 +51,24 @@ from substack.post import Post
|
|
|
47
51
|
api = Api(
|
|
48
52
|
email=os.getenv("EMAIL"),
|
|
49
53
|
password=os.getenv("PASSWORD"),
|
|
50
|
-
publication_url=os.getenv("PUBLICATION_URL"),
|
|
51
54
|
)
|
|
52
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
|
+
|
|
53
68
|
post = Post(
|
|
54
69
|
title="How to publish a Substack post using the Python API",
|
|
55
70
|
subtitle="This post was published using the Python API",
|
|
56
|
-
user_id=
|
|
71
|
+
user_id=user_id
|
|
57
72
|
)
|
|
58
73
|
|
|
59
74
|
post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
|
|
@@ -67,6 +82,9 @@ post.add({'type': "paragraph",
|
|
|
67
82
|
post.add({'type': 'paragraph', 'content': [
|
|
68
83
|
{'content': "View Link", 'marks': [{'type': "link", 'href': 'https://whoraised.substack.com/'}]}]})
|
|
69
84
|
|
|
85
|
+
# set paywall boundary
|
|
86
|
+
post.add({'type': 'paywall'})
|
|
87
|
+
|
|
70
88
|
# add image
|
|
71
89
|
post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif"})
|
|
72
90
|
|
|
@@ -74,6 +92,10 @@ post.add({'type': 'captionedImage', 'src': "https://media.tenor.com/7B4jMa-a7bsA
|
|
|
74
92
|
image = api.get_image('image.png')
|
|
75
93
|
post.add({"type": "captionedImage", "src": image.get("url")})
|
|
76
94
|
|
|
95
|
+
# embed publication
|
|
96
|
+
embedded = api.publication_embed("https://jackio.substack.com/")
|
|
97
|
+
post.add({"type": "embeddedPublication", "url": embedded})
|
|
98
|
+
|
|
77
99
|
draft = api.post_draft(post.get_draft())
|
|
78
100
|
|
|
79
101
|
# set section (THIS CAN BE DONE ONLY AFTER HAVING FIRST POSTED THE DRAFT)
|
|
@@ -85,3 +107,16 @@ api.prepublish_draft(draft.get("id"))
|
|
|
85
107
|
api.publish_draft(draft.get("id"))
|
|
86
108
|
```
|
|
87
109
|
|
|
110
|
+
# Contributing
|
|
111
|
+
|
|
112
|
+
Install pre-commit:
|
|
113
|
+
|
|
114
|
+
```shell
|
|
115
|
+
pip install pre-commit
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Set up pre-commit
|
|
119
|
+
|
|
120
|
+
```shell
|
|
121
|
+
pre-commit install
|
|
122
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-substack"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.15"
|
|
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,41 @@ class Api:
|
|
|
73
118
|
"redirect": "/",
|
|
74
119
|
},
|
|
75
120
|
)
|
|
121
|
+
|
|
76
122
|
return Api._handle_response(response=response)
|
|
77
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
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
output = Api._handle_response(response=response)
|
|
133
|
+
except SubstackRequestException as ex:
|
|
134
|
+
output = {}
|
|
135
|
+
return output
|
|
136
|
+
|
|
137
|
+
def change_publication(self, publication):
|
|
138
|
+
"""
|
|
139
|
+
Change the publication URL
|
|
140
|
+
"""
|
|
141
|
+
self.publication_url = urljoin(publication["publication_url"], "api/v1")
|
|
142
|
+
|
|
143
|
+
# sign-in to the publication
|
|
144
|
+
self.signin_for_pub(publication)
|
|
145
|
+
|
|
146
|
+
def export_cookies(self, path: str = "cookies.json"):
|
|
147
|
+
"""
|
|
148
|
+
Export cookies to a json file.
|
|
149
|
+
Args:
|
|
150
|
+
path: path to the json file
|
|
151
|
+
"""
|
|
152
|
+
cookies = self._session.cookies.get_dict()
|
|
153
|
+
with open(path, "w") as f:
|
|
154
|
+
json.dump(cookies, f)
|
|
155
|
+
|
|
78
156
|
@staticmethod
|
|
79
157
|
def _handle_response(response: requests.Response):
|
|
80
158
|
"""
|
|
@@ -92,6 +170,82 @@ class Api:
|
|
|
92
170
|
except ValueError:
|
|
93
171
|
raise SubstackRequestException("Invalid Response: %s" % response.text)
|
|
94
172
|
|
|
173
|
+
def get_user_id(self):
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
profile = self.get_user_profile()
|
|
180
|
+
user_id = profile["id"]
|
|
181
|
+
|
|
182
|
+
return user_id
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def get_publication_url(publication: dict) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Gets the publication url
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
publication:
|
|
191
|
+
"""
|
|
192
|
+
custom_domain = publication["custom_domain"]
|
|
193
|
+
if not custom_domain:
|
|
194
|
+
publication_url = f"https://{publication['subdomain']}.substack.com"
|
|
195
|
+
else:
|
|
196
|
+
publication_url = f"https://{custom_domain}"
|
|
197
|
+
|
|
198
|
+
return publication_url
|
|
199
|
+
|
|
200
|
+
def get_user_primary_publication(self):
|
|
201
|
+
"""
|
|
202
|
+
Gets the users primary publication
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
profile = self.get_user_profile()
|
|
206
|
+
primary_publication = profile["primaryPublication"]
|
|
207
|
+
primary_publication["publication_url"] = self.get_publication_url(
|
|
208
|
+
primary_publication
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return primary_publication
|
|
212
|
+
|
|
213
|
+
def get_user_publications(self):
|
|
214
|
+
"""
|
|
215
|
+
Gets the users publications
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
profile = self.get_user_profile()
|
|
219
|
+
|
|
220
|
+
# Loop through users "publicationUsers" list, and return a list
|
|
221
|
+
# of dictionaries of "name", and "subdomain", and "id"
|
|
222
|
+
user_publications = []
|
|
223
|
+
for publication in profile["publicationUsers"]:
|
|
224
|
+
pub = publication["publication"]
|
|
225
|
+
pub["publication_url"] = self.get_publication_url(pub)
|
|
226
|
+
user_publications.append(pub)
|
|
227
|
+
|
|
228
|
+
return user_publications
|
|
229
|
+
|
|
230
|
+
def get_user_profile(self):
|
|
231
|
+
"""
|
|
232
|
+
Gets the users profile
|
|
233
|
+
"""
|
|
234
|
+
response = self._session.get(f"{self.base_url}/user/profile/self")
|
|
235
|
+
|
|
236
|
+
return Api._handle_response(response=response)
|
|
237
|
+
|
|
238
|
+
def get_user_settings(self):
|
|
239
|
+
"""
|
|
240
|
+
Get list of users.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
response = self._session.get(f"{self.base_url}/settings")
|
|
246
|
+
|
|
247
|
+
return Api._handle_response(response=response)
|
|
248
|
+
|
|
95
249
|
def get_publication_users(self):
|
|
96
250
|
"""
|
|
97
251
|
Get list of users.
|
|
@@ -111,9 +265,29 @@ class Api:
|
|
|
111
265
|
Returns:
|
|
112
266
|
|
|
113
267
|
"""
|
|
114
|
-
response = self._session.get(
|
|
268
|
+
response = self._session.get(
|
|
269
|
+
f"{self.publication_url}/publication_launch_checklist"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return Api._handle_response(response=response)["subscriberCount"]
|
|
273
|
+
|
|
274
|
+
def get_published_posts(
|
|
275
|
+
self, offset=0, limit=25, order_by="post_date", order_direction="desc"
|
|
276
|
+
):
|
|
277
|
+
"""
|
|
278
|
+
Get list of published posts for the publication.
|
|
279
|
+
"""
|
|
280
|
+
response = self._session.get(
|
|
281
|
+
f"{self.publication_url}/post_management/published",
|
|
282
|
+
params={
|
|
283
|
+
"offset": offset,
|
|
284
|
+
"limit": limit,
|
|
285
|
+
"order_by": order_by,
|
|
286
|
+
"order_direction": order_direction,
|
|
287
|
+
},
|
|
288
|
+
)
|
|
115
289
|
|
|
116
|
-
return Api._handle_response(response=response)
|
|
290
|
+
return Api._handle_response(response=response)
|
|
117
291
|
|
|
118
292
|
def get_posts(self) -> dict:
|
|
119
293
|
"""
|
|
@@ -142,6 +316,14 @@ class Api:
|
|
|
142
316
|
)
|
|
143
317
|
return Api._handle_response(response=response)
|
|
144
318
|
|
|
319
|
+
def get_draft(self, draft_id):
|
|
320
|
+
"""
|
|
321
|
+
Gets a draft given it's id.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
response = self._session.get(f"{self.publication_url}/drafts/{draft_id}")
|
|
325
|
+
return Api._handle_response(response=response)
|
|
326
|
+
|
|
145
327
|
def delete_draft(self, draft_id):
|
|
146
328
|
"""
|
|
147
329
|
|
|
@@ -166,11 +348,7 @@ class Api:
|
|
|
166
348
|
response = self._session.post(f"{self.publication_url}/drafts", json=body)
|
|
167
349
|
return Api._handle_response(response=response)
|
|
168
350
|
|
|
169
|
-
def put_draft(
|
|
170
|
-
self,
|
|
171
|
-
draft,
|
|
172
|
-
**kwargs
|
|
173
|
-
) -> dict:
|
|
351
|
+
def put_draft(self, draft, **kwargs) -> dict:
|
|
174
352
|
"""
|
|
175
353
|
|
|
176
354
|
Args:
|
|
@@ -202,7 +380,7 @@ class Api:
|
|
|
202
380
|
return Api._handle_response(response=response)
|
|
203
381
|
|
|
204
382
|
def publish_draft(
|
|
205
|
-
|
|
383
|
+
self, draft, send: bool = True, share_automatically: bool = False
|
|
206
384
|
) -> dict:
|
|
207
385
|
"""
|
|
208
386
|
|
|
@@ -320,7 +498,7 @@ class Api:
|
|
|
320
498
|
page_output = self.get_category(category_id, category_type, page)
|
|
321
499
|
publications.extend(page_output.get("publications", []))
|
|
322
500
|
if (
|
|
323
|
-
|
|
501
|
+
limit is not None and limit <= len(publications)
|
|
324
502
|
) or not page_output.get("more", False):
|
|
325
503
|
publications = publications[:limit]
|
|
326
504
|
break
|
|
@@ -358,5 +536,38 @@ class Api:
|
|
|
358
536
|
f"{self.publication_url}/subscriptions",
|
|
359
537
|
)
|
|
360
538
|
content = Api._handle_response(response=response)
|
|
361
|
-
sections = [
|
|
539
|
+
sections = [
|
|
540
|
+
p.get("sections")
|
|
541
|
+
for p in content.get("publications")
|
|
542
|
+
if p.get("hostname") in self.publication_url
|
|
543
|
+
]
|
|
362
544
|
return sections[0]
|
|
545
|
+
|
|
546
|
+
def publication_embed(self, url):
|
|
547
|
+
"""
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
url:
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
return self.call("/publication/embed", "GET", url=url)
|
|
556
|
+
|
|
557
|
+
def call(self, endpoint, method, **params):
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
endpoint:
|
|
562
|
+
method:
|
|
563
|
+
**params:
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
|
|
567
|
+
"""
|
|
568
|
+
response = self._session.request(
|
|
569
|
+
method=method,
|
|
570
|
+
url=f"{self.publication_url}/{endpoint}",
|
|
571
|
+
params=params,
|
|
572
|
+
)
|
|
573
|
+
return Api._handle_response(response=response)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Post Utilities
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import json
|
|
2
8
|
from typing import Dict
|
|
3
9
|
|
|
@@ -7,13 +13,19 @@ from substack.exceptions import SectionNotExistsException
|
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
class Post:
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
Post utility class
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
10
22
|
def __init__(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
self,
|
|
24
|
+
title: str,
|
|
25
|
+
subtitle: str,
|
|
26
|
+
user_id,
|
|
27
|
+
audience: str = None,
|
|
28
|
+
write_comment_permissions: str = None,
|
|
17
29
|
):
|
|
18
30
|
"""
|
|
19
31
|
|
|
@@ -72,6 +84,8 @@ class Post:
|
|
|
72
84
|
content = item.get("content")
|
|
73
85
|
if item.get("type") == "captionedImage":
|
|
74
86
|
self.captioned_image(**item)
|
|
87
|
+
elif item.get("type") == "embeddedPublication":
|
|
88
|
+
self.draft_body["content"][-1]["attrs"] = item.get("url")
|
|
75
89
|
elif item.get("type") == "youtube2":
|
|
76
90
|
self.youtube(item.get("src"))
|
|
77
91
|
elif item.get("type") == "subscribeWidget":
|
|
@@ -143,20 +157,20 @@ class Post:
|
|
|
143
157
|
return self
|
|
144
158
|
|
|
145
159
|
def captioned_image(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
self,
|
|
161
|
+
src: str,
|
|
162
|
+
fullscreen: bool = False,
|
|
163
|
+
imageSize: str = "normal",
|
|
164
|
+
height: int = 819,
|
|
165
|
+
width: int = 1456,
|
|
166
|
+
resizeWidth: int = 728,
|
|
167
|
+
bytes: str = None,
|
|
168
|
+
alt: str = None,
|
|
169
|
+
title: str = None,
|
|
170
|
+
type: str = None,
|
|
171
|
+
href: str = None,
|
|
172
|
+
belowTheFold: bool = False,
|
|
173
|
+
internalRedirect: str = None,
|
|
160
174
|
):
|
|
161
175
|
"""
|
|
162
176
|
|
|
@@ -252,7 +266,7 @@ class Post:
|
|
|
252
266
|
return self
|
|
253
267
|
|
|
254
268
|
def remove_last_paragraph(self):
|
|
255
|
-
""" """
|
|
269
|
+
"""Remove last paragraph"""
|
|
256
270
|
del self.draft_body.get("content")[-1]
|
|
257
271
|
|
|
258
272
|
def get_draft(self):
|
|
@@ -282,7 +296,11 @@ class Post:
|
|
|
282
296
|
Subscribe for free to receive new posts and support my work."""
|
|
283
297
|
|
|
284
298
|
subscribe = self.draft_body["content"][-1]
|
|
285
|
-
subscribe["attrs"] = {
|
|
299
|
+
subscribe["attrs"] = {
|
|
300
|
+
"url": "%%checkout_url%%",
|
|
301
|
+
"text": "Subscribe",
|
|
302
|
+
"language": "en",
|
|
303
|
+
}
|
|
286
304
|
subscribe["content"] = [
|
|
287
305
|
{
|
|
288
306
|
"type": "ctaCaption",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|