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
5
12
from urllib .parse import urljoin
6
- import json
7
13
8
14
import requests
9
15
@@ -22,13 +28,13 @@ class Api:
22
28
"""
23
29
24
30
def __init__ (
25
- self ,
26
- email = None ,
27
- password = None ,
28
- cookies_path = None ,
29
- base_url = None ,
30
- publication_url = None ,
31
- debug = False ,
31
+ self ,
32
+ email = None ,
33
+ password = None ,
34
+ cookies_path = None ,
35
+ base_url = None ,
36
+ publication_url = None ,
37
+ debug = False ,
32
38
):
33
39
"""
34
40
@@ -40,7 +46,8 @@ def __init__(
40
46
email:
41
47
password:
42
48
cookies_path
43
- To re-use your session without logging in each time, you can save your cookies to a json file and then load them in the next session.
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.
44
51
Make sure to re-save your cookies, as they do update over time.
45
52
base_url:
46
53
The base URL to use to contact the Substack API.
@@ -57,27 +64,30 @@ def __init__(
57
64
# Load cookies from file if provided
58
65
# Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud
59
66
if cookies_path is not None :
60
- with open (cookies_path , "r" ) as f :
67
+ with open (cookies_path ) as f :
61
68
cookies = json .load (f )
62
69
self ._session .cookies .update (cookies )
63
70
64
71
elif email is not None and password is not None :
65
72
self .login (email , password )
66
73
else :
67
- raise ValueError ("Must provide email and password or cookies_path to authenticate." )
74
+ raise ValueError (
75
+ "Must provide email and password or cookies_path to authenticate."
76
+ )
68
77
78
+ user_publication = None
69
79
# if the user provided a publication url, then use that
70
80
if publication_url :
71
81
import re
72
82
73
83
# Regular expression to extract subdomain name
74
84
match = re .search (r"https://(.*).substack.com" , publication_url .lower ())
75
85
subdomain = match .group (1 ) if match else None
76
-
86
+
77
87
user_publications = self .get_user_publications ()
78
88
# search through publications to find the publication with the matching subdomain
79
89
for publication in user_publications :
80
- if publication [' subdomain' ] == subdomain :
90
+ if publication [" subdomain" ] == subdomain :
81
91
# set the current publication to the users publication
82
92
user_publication = publication
83
93
break
@@ -110,20 +120,21 @@ def login(self, email, password) -> dict:
110
120
)
111
121
112
122
return Api ._handle_response (response = response )
113
-
123
+
114
124
def signin_for_pub (self , publication ):
115
125
"""
116
126
Complete the signin process
117
127
"""
118
128
response = self ._session .get (
119
129
f"https://substack.com/sign-in?redirect=%2F&for_pub={ publication ['subdomain' ]} " ,
120
130
)
131
+ return Api ._handle_response (response = response )
121
132
122
133
def change_publication (self , publication ):
123
134
"""
124
135
Change the publication URL
125
136
"""
126
- self .publication_url = urljoin (publication [' publication_url' ], "api/v1" )
137
+ self .publication_url = urljoin (publication [" publication_url" ], "api/v1" )
127
138
128
139
# sign-in to the publication
129
140
self .signin_for_pub (publication )
@@ -156,16 +167,25 @@ def _handle_response(response: requests.Response):
156
167
raise SubstackRequestException ("Invalid Response: %s" % response .text )
157
168
158
169
def get_user_id (self ):
170
+ """
171
+
172
+ Returns:
173
+
174
+ """
159
175
profile = self .get_user_profile ()
160
- user_id = profile ['id' ]
176
+ user_id = profile ["id" ]
161
177
162
178
return user_id
163
-
164
- def get_publication_url (self , publication ):
179
+
180
+ @staticmethod
181
+ def get_publication_url (publication : dict ) -> str :
165
182
"""
166
183
Gets the publication url
184
+
185
+ Args:
186
+ publication:
167
187
"""
168
- custom_domain = publication [' custom_domain' ]
188
+ custom_domain = publication [" custom_domain" ]
169
189
if not custom_domain :
170
190
publication_url = f"https://{ publication ['subdomain' ]} .substack.com"
171
191
else :
@@ -179,8 +199,10 @@ def get_user_primary_publication(self):
179
199
"""
180
200
181
201
profile = self .get_user_profile ()
182
- primary_publication = profile ['primaryPublication' ]
183
- primary_publication ['publication_url' ] = self .get_publication_url (primary_publication )
202
+ primary_publication = profile ["primaryPublication" ]
203
+ primary_publication ["publication_url" ] = self .get_publication_url (
204
+ primary_publication
205
+ )
184
206
185
207
return primary_publication
186
208
@@ -191,11 +213,12 @@ def get_user_publications(self):
191
213
192
214
profile = self .get_user_profile ()
193
215
194
- # Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id"
216
+ # Loop through users "publicationUsers" list, and return a list
217
+ # of dictionaries of "name", and "subdomain", and "id"
195
218
user_publications = []
196
- for publication in profile [' publicationUsers' ]:
197
- pub = publication [' publication' ]
198
- pub [' publication_url' ] = self .get_publication_url (pub )
219
+ for publication in profile [" publicationUsers" ]:
220
+ pub = publication [" publication" ]
221
+ pub [" publication_url" ] = self .get_publication_url (pub )
199
222
user_publications .append (pub )
200
223
201
224
return user_publications
@@ -218,7 +241,7 @@ def get_user_settings(self):
218
241
response = self ._session .get (f"{ self .base_url } /settings" )
219
242
220
243
return Api ._handle_response (response = response )
221
-
244
+
222
245
def get_publication_users (self ):
223
246
"""
224
247
Get list of users.
@@ -238,17 +261,26 @@ def get_publication_subscriber_count(self):
238
261
Returns:
239
262
240
263
"""
241
- response = self ._session .get (f"{ self .publication_url } /publication_launch_checklist" )
264
+ response = self ._session .get (
265
+ f"{ self .publication_url } /publication_launch_checklist"
266
+ )
242
267
243
- return Api ._handle_response (response = response )[' subscriberCount' ]
268
+ return Api ._handle_response (response = response )[" subscriberCount" ]
244
269
245
- def get_published_posts (self , offset = 0 , limit = 25 , order_by = "post_date" , order_direction = "desc" ):
270
+ def get_published_posts (
271
+ self , offset = 0 , limit = 25 , order_by = "post_date" , order_direction = "desc"
272
+ ):
246
273
"""
247
274
Get list of published posts for the publication.
248
275
"""
249
276
response = self ._session .get (
250
277
f"{ self .publication_url } /post_management/published" ,
251
- params = {"offset" : offset , "limit" : limit , "order_by" : order_by , "order_direction" : order_direction },
278
+ params = {
279
+ "offset" : offset ,
280
+ "limit" : limit ,
281
+ "order_by" : order_by ,
282
+ "order_direction" : order_direction ,
283
+ },
252
284
)
253
285
254
286
return Api ._handle_response (response = response )
@@ -312,11 +344,7 @@ def post_draft(self, body) -> dict:
312
344
response = self ._session .post (f"{ self .publication_url } /drafts" , json = body )
313
345
return Api ._handle_response (response = response )
314
346
315
- def put_draft (
316
- self ,
317
- draft ,
318
- ** kwargs
319
- ) -> dict :
347
+ def put_draft (self , draft , ** kwargs ) -> dict :
320
348
"""
321
349
322
350
Args:
@@ -348,7 +376,7 @@ def prepublish_draft(self, draft) -> dict:
348
376
return Api ._handle_response (response = response )
349
377
350
378
def publish_draft (
351
- self , draft , send : bool = True , share_automatically : bool = False
379
+ self , draft , send : bool = True , share_automatically : bool = False
352
380
) -> dict :
353
381
"""
354
382
@@ -466,7 +494,7 @@ def get_single_category(self, category_id, category_type, page=None, limit=None)
466
494
page_output = self .get_category (category_id , category_type , page )
467
495
publications .extend (page_output .get ("publications" , []))
468
496
if (
469
- limit is not None and limit <= len (publications )
497
+ limit is not None and limit <= len (publications )
470
498
) or not page_output .get ("more" , False ):
471
499
publications = publications [:limit ]
472
500
break
@@ -504,5 +532,9 @@ def get_sections(self):
504
532
f"{ self .publication_url } /subscriptions" ,
505
533
)
506
534
content = Api ._handle_response (response = response )
507
- sections = [p .get ("sections" ) for p in content .get ("publications" ) if p .get ("hostname" ) in self .publication_url ]
535
+ sections = [
536
+ p .get ("sections" )
537
+ for p in content .get ("publications" )
538
+ if p .get ("hostname" ) in self .publication_url
539
+ ]
508
540
return sections [0 ]
0 commit comments