Skip to content
Merged
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ You can install python-substack using:

Set the following environment variables by creating a **.env** file:

PUBLICATION_URL=https://ma2za.substack.com
EMAIL=
PASSWORD=
USER_ID=

To discover the USER_ID go to your public profile page,
in the URL bar of the browser you find the substack address
followed by your USER_ID and your username:
https://substack.com/profile/[USER_ID]-[username]
## If you don't have a password
Recently Substack has been setting up new accounts without a password. If you sign-out and sign back in it just uses your email address with a "magic" link.

Set a password:
- Sign-out of Substack
- At the sign-in page click, "Sign in with password" under the `Email` text box
- Then choose, "Set a new password"

The .env file will be ignored by git but always be careful.

Expand All @@ -47,13 +48,24 @@ from substack.post import Post
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)

user_id = api.get_user_id()

# Switch Publications - The library defaults to your users primary publication. You can retrieve all your publications and change which one you want to use.

# primary publication
user_publication = api.get_user_primary_publication()
# all publications
user_publications = api.get_user_publications()

# This step is only necessary if you are not using your primary publication
# api.change_publication(user_publication)

post = Post(
title="How to publish a Substack post using the Python API",
subtitle="This post was published using the Python API",
user_id=os.getenv("USER_ID")
user_id=user_id
)

post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})
Expand Down
124 changes: 123 additions & 1 deletion substack/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def __init__(
Defaults to https://substack.com/api/v1.
"""
self.base_url = base_url or "https://substack.com/api/v1"
self.publication_url = urljoin(publication_url, "api/v1")

if debug:
logging.basicConfig()
Expand All @@ -53,6 +52,28 @@ def __init__(
if email is not None and password is not None:
self.login(email, password)

# if the user provided a publication url, then use that
if publication_url:
import re

# Regular expression to extract subdomain name
match = re.search(r"https://(.*).substack.com", publication_url.lower())
subdomain = match.group(1) if match else None

user_publications = self.get_user_publications()
# search through publications to find the publication with the matching subdomain
for publication in user_publications:
if publication['subdomain'] == subdomain:
# set the current publication to the users publication
user_publication = publication
break
else:
# get the users primary publication
user_publication = self.get_user_primary_publication()

# set the current publication to the users primary publication
self.change_publication(user_publication)

def login(self, email, password) -> dict:
"""

Expand All @@ -73,7 +94,25 @@ def login(self, email, password) -> dict:
"redirect": "/",
},
)

return Api._handle_response(response=response)

def signin_for_pub(self, publication):
"""
Complete the signin process
"""
response = self._session.get(
f"https://substack.com/sign-in?redirect=%2F&for_pub={publication['subdomain']}",
)

def change_publication(self, publication):
"""
Change the publication URL
"""
self.publication_url = urljoin(publication['publication_url'], "api/v1")

# sign-in to the publication
self.signin_for_pub(publication)

@staticmethod
def _handle_response(response: requests.Response):
Expand All @@ -92,6 +131,70 @@ def _handle_response(response: requests.Response):
except ValueError:
raise SubstackRequestException("Invalid Response: %s" % response.text)

def get_user_id(self):
profile = self.get_user_profile()
user_id = profile['id']

return user_id

def get_publication_url(self, publication):
"""
Gets the publication url
"""
custom_domain = publication['custom_domain']
if not custom_domain:
publication_url = f"https://{publication['subdomain']}.substack.com"
else:
publication_url = f"https://{custom_domain}"

return publication_url

def get_user_primary_publication(self):
"""
Gets the users primary publication
"""

profile = self.get_user_profile()
primary_publication = profile['primaryPublication']
primary_publication['publication_url'] = self.get_publication_url(primary_publication)

return primary_publication

def get_user_publications(self):
"""
Gets the users publications
"""

profile = self.get_user_profile()

# Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id"
user_publications = []
for publication in profile['publicationUsers']:
pub = publication['publication']
pub['publication_url'] = self.get_publication_url(pub)
user_publications.append(pub)

return user_publications

def get_user_profile(self):
"""
Gets the users profile
"""
response = self._session.get(f"{self.base_url}/user/profile/self")

return Api._handle_response(response=response)

def get_user_settings(self):
"""
Get list of users.

Returns:

"""
response = self._session.get(f"{self.base_url}/settings")

return Api._handle_response(response=response)

def get_publication_users(self):
"""
Get list of users.
Expand All @@ -115,6 +218,17 @@ def get_publication_subscriber_count(self):

return Api._handle_response(response=response)['subscriberCount']

def get_published_posts(self, offset=0, limit=25, order_by="post_date", order_direction="desc"):
"""
Get list of published posts for the publication.
"""
response = self._session.get(
f"{self.publication_url}/post_management/published",
params={"offset": offset, "limit": limit, "order_by": order_by, "order_direction": order_direction},
)

return Api._handle_response(response=response)

def get_posts(self) -> dict:
"""

Expand Down Expand Up @@ -142,6 +256,14 @@ def get_drafts(self, filter=None, offset=None, limit=None):
)
return Api._handle_response(response=response)

def get_draft(self, draft_id):
"""
Gets a draft given it's id.

"""
response = self._session.get(f"{self.publication_url}/drafts/{draft_id}")
return Api._handle_response(response=response)

def delete_draft(self, draft_id):
"""

Expand Down
5 changes: 0 additions & 5 deletions tests/substack/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def test_login(self):
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
self.assertIsNotNone(api)

Expand All @@ -31,7 +30,6 @@ def test_get_drafts(self):
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
drafts = api.get_drafts()
self.assertIsNotNone(drafts)
Expand All @@ -40,7 +38,6 @@ def test_post_draft(self):
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
posted_draft = api.post_draft([{"id": os.getenv("USER_ID"), "is_guest": False}])
self.assertIsNotNone(posted_draft)
Expand All @@ -49,7 +46,6 @@ def test_publication_users(self):
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
users = api.get_publication_users()
self.assertIsNotNone(users)
Expand All @@ -58,7 +54,6 @@ def test_put_draft(self):
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
posted_draft = api.put_draft("")
self.assertIsNotNone(posted_draft)
Expand Down