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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-substack
3
- Version: 0.1.12
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
- To discover the USER_ID go to your public profile page,
53
- in the URL bar of the browser you find the substack address
54
- followed by your USER_ID and your username:
55
- https://substack.com/profile/[USER_ID]-[username]
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=os.getenv("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",'content':[{'content': "This is how you "},{'content': "bolden ",'marks':[{'type': "strong"}]},{'content': "a word."}]})
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
- #add hyperlink to text
89
- post.add({'type':'paragraph','content':[{'content': "View Link",'marks':[{'type': "link",'href':'https://whoraised.substack.com/'}]}]})
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
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058)](https://www.python.org/downloads/)
7
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
8
+ ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
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.12"
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
- self,
25
- email=None,
26
- password=None,
27
- base_url=None,
28
- publication_url=None,
29
- 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,
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
- if email is not None and password is not None:
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(f"{self.publication_url}/publication_launch_checklist")
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)['subscriberCount']
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: draft id
181
- title:
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
- self, draft, send: bool = True, share_automatically: bool = False
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
- limit is not None and limit <= len(publications)
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]
@@ -26,3 +26,7 @@ class SubstackRequestException(Exception):
26
26
 
27
27
  def __str__(self):
28
28
  return f"SubstackRequestException: {self.message}"
29
+
30
+
31
+ class SectionNotExistsException(SubstackRequestException):
32
+ pass
@@ -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
- self,
10
- title: str,
11
- subtitle: str,
12
- user_id,
13
- audience: str = None,
14
- write_comment_permissions: str = None,
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
- self,
127
- src: str,
128
- fullscreen: bool = False,
129
- imageSize: str = "normal",
130
- height: int = 819,
131
- width: int = 1456,
132
- resizeWidth: int = 728,
133
- bytes: str = None,
134
- alt: str = None,
135
- title: str = None,
136
- type: str = None,
137
- href: str = None,
138
- belowTheFold: bool = False,
139
- internalRedirect: str = None,
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"] = {"url": "%%checkout_url%%", "text": "Subscribe", "language": "en"}
297
+ subscribe["attrs"] = {
298
+ "url": "%%checkout_url%%",
299
+ "text": "Subscribe",
300
+ "language": "en",
301
+ }
266
302
  subscribe["content"] = [
267
303
  {
268
304
  "type": "ctaCaption",
@@ -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
- [![Python](https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058)](https://www.python.org/downloads/)
7
- [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
8
- ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
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
-