Skip to content

Commit

Permalink
🏰 upkeep
Browse files Browse the repository at this point in the history
  • Loading branch information
ma2za committed Dec 7, 2023
1 parent e0743f9 commit 5708d5d
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ dmypy.json
# Pyre type checker
.pyre/

.idea/
.idea/
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ Set the following environment variables by creating a **.env** file:
PASSWORD=

## 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.

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"

- 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 Down Expand Up @@ -100,3 +103,16 @@ api.prepublish_draft(draft.get("id"))
api.publish_draft(draft.get("id"))
```

# Contributing

Install pre-commit:

```shell
pip install pre-commit
```

Set up pre-commit

```shell
pre-commit install
```
2 changes: 1 addition & 1 deletion examples/draft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ body:
src: "EnDg65ISswg"
9:
type: "subscribeWidget"
message: "Hello Everyone!!!"
message: "Hello Everyone!!!"
14 changes: 7 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-substack"
version = "0.1.13"
version = "0.1.14"
description = "A Python wrapper around the Substack API."
authors = ["Paolo Mazza <[email protected]>"]
license = "MIT"
Expand Down Expand Up @@ -28,4 +28,4 @@ PyYAML = "^6.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
build-backend = "poetry.core.masonry.api"
108 changes: 70 additions & 38 deletions substack/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""
API Wrapper
"""

import base64
import json
import logging
import os
from datetime import datetime
from urllib.parse import urljoin
import json

import requests

Expand All @@ -22,13 +28,13 @@ class Api:
"""

def __init__(
self,
email=None,
password=None,
cookies_path=None,
base_url=None,
publication_url=None,
debug=False,
self,
email=None,
password=None,
cookies_path=None,
base_url=None,
publication_url=None,
debug=False,
):
"""
Expand All @@ -40,7 +46,8 @@ def __init__(
email:
password:
cookies_path
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.
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.
Make sure to re-save your cookies, as they do update over time.
base_url:
The base URL to use to contact the Substack API.
Expand All @@ -57,27 +64,30 @@ def __init__(
# Load cookies from file if provided
# Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud
if cookies_path is not None:
with open(cookies_path, "r") as f:
with open(cookies_path) as f:
cookies = json.load(f)
self._session.cookies.update(cookies)

elif email is not None and password is not None:
self.login(email, password)
else:
raise ValueError("Must provide email and password or cookies_path to authenticate.")
raise ValueError(
"Must provide email and password or cookies_path to authenticate."
)

user_publication = None
# 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:
if publication["subdomain"] == subdomain:
# set the current publication to the users publication
user_publication = publication
break
Expand Down Expand Up @@ -110,20 +120,21 @@ def login(self, email, password) -> dict:
)

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']}",
)
return Api._handle_response(response=response)

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

# sign-in to the publication
self.signin_for_pub(publication)
Expand Down Expand Up @@ -156,16 +167,25 @@ def _handle_response(response: requests.Response):
raise SubstackRequestException("Invalid Response: %s" % response.text)

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

return user_id

def get_publication_url(self, publication):

@staticmethod
def get_publication_url(publication: dict) -> str:
"""
Gets the publication url
Args:
publication:
"""
custom_domain = publication['custom_domain']
custom_domain = publication["custom_domain"]
if not custom_domain:
publication_url = f"https://{publication['subdomain']}.substack.com"
else:
Expand All @@ -179,8 +199,10 @@ def get_user_primary_publication(self):
"""

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

return primary_publication

Expand All @@ -191,11 +213,12 @@ def get_user_publications(self):

profile = self.get_user_profile()

# Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id"
# 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)
for publication in profile["publicationUsers"]:
pub = publication["publication"]
pub["publication_url"] = self.get_publication_url(pub)
user_publications.append(pub)

return user_publications
Expand All @@ -218,7 +241,7 @@ def get_user_settings(self):
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 @@ -238,17 +261,26 @@ def get_publication_subscriber_count(self):
Returns:
"""
response = self._session.get(f"{self.publication_url}/publication_launch_checklist")
response = self._session.get(
f"{self.publication_url}/publication_launch_checklist"
)

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

def get_published_posts(self, offset=0, limit=25, order_by="post_date", order_direction="desc"):
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},
params={
"offset": offset,
"limit": limit,
"order_by": order_by,
"order_direction": order_direction,
},
)

return Api._handle_response(response=response)
Expand Down Expand Up @@ -312,11 +344,7 @@ def post_draft(self, body) -> dict:
response = self._session.post(f"{self.publication_url}/drafts", json=body)
return Api._handle_response(response=response)

def put_draft(
self,
draft,
**kwargs
) -> dict:
def put_draft(self, draft, **kwargs) -> dict:
"""
Args:
Expand Down Expand Up @@ -348,7 +376,7 @@ def prepublish_draft(self, draft) -> dict:
return Api._handle_response(response=response)

def publish_draft(
self, draft, send: bool = True, share_automatically: bool = False
self, draft, send: bool = True, share_automatically: bool = False
) -> dict:
"""
Expand Down Expand Up @@ -466,7 +494,7 @@ def get_single_category(self, category_id, category_type, page=None, limit=None)
page_output = self.get_category(category_id, category_type, page)
publications.extend(page_output.get("publications", []))
if (
limit is not None and limit <= len(publications)
limit is not None and limit <= len(publications)
) or not page_output.get("more", False):
publications = publications[:limit]
break
Expand Down Expand Up @@ -504,5 +532,9 @@ def get_sections(self):
f"{self.publication_url}/subscriptions",
)
content = Api._handle_response(response=response)
sections = [p.get("sections") for p in content.get("publications") if p.get("hostname") in self.publication_url]
sections = [
p.get("sections")
for p in content.get("publications")
if p.get("hostname") in self.publication_url
]
return sections[0]
Loading

0 comments on commit 5708d5d

Please sign in to comment.