substack-api 1.1.2__tar.gz → 1.1.4.dev0__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.
- substack_api-1.1.4.dev0/.github/workflows/docs.yml +54 -0
- substack_api-1.1.4.dev0/.github/workflows/pull_request.yml +42 -0
- substack_api-1.1.4.dev0/.github/workflows/release.yml +42 -0
- substack_api-1.1.4.dev0/.gitignore +9 -0
- substack_api-1.1.4.dev0/.python-version +1 -0
- {substack_api-1.1.2/substack_api.egg-info → substack_api-1.1.4.dev0}/PKG-INFO +1 -1
- substack_api-1.1.4.dev0/docs/api-reference/auth.md +163 -0
- substack_api-1.1.4.dev0/docs/api-reference/category.md +118 -0
- substack_api-1.1.4.dev0/docs/api-reference/index.md +37 -0
- substack_api-1.1.4.dev0/docs/api-reference/newsletter.md +130 -0
- substack_api-1.1.4.dev0/docs/api-reference/post.md +93 -0
- substack_api-1.1.4.dev0/docs/api-reference/user.md +131 -0
- substack_api-1.1.4.dev0/docs/authentication.md +172 -0
- substack_api-1.1.4.dev0/docs/css/extra.css +16 -0
- substack_api-1.1.4.dev0/docs/index.md +52 -0
- substack_api-1.1.4.dev0/docs/installation.md +32 -0
- substack_api-1.1.4.dev0/docs/user-guide.md +191 -0
- substack_api-1.1.4.dev0/examples/usage_walkthrough.ipynb +4447 -0
- substack_api-1.1.4.dev0/mkdocs.yml +77 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/pyproject.toml +10 -1
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/__init__.py +9 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/newsletter.py +92 -29
- {substack_api-1.1.2 → substack_api-1.1.4.dev0/substack_api.egg-info}/PKG-INFO +1 -1
- substack_api-1.1.4.dev0/substack_api.egg-info/SOURCES.txt +41 -0
- substack_api-1.1.4.dev0/tests/__init__.py +0 -0
- substack_api-1.1.4.dev0/tests/conftest.py +55 -0
- substack_api-1.1.4.dev0/tests/test_newsletter.py +611 -0
- substack_api-1.1.4.dev0/uv.lock +1118 -0
- substack_api-1.1.2/substack_api.egg-info/SOURCES.txt +0 -20
- substack_api-1.1.2/tests/test_newsletter.py +0 -300
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/LICENSE +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/README.md +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/setup.cfg +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/auth.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/category.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/post.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api/user.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api.egg-info/dependency_links.txt +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api.egg-info/requires.txt +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/substack_api.egg-info/top_level.txt +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/tests/test_auth.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/tests/test_category.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/tests/test_post.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/tests/test_user.py +0 -0
- {substack_api-1.1.2 → substack_api-1.1.4.dev0}/tests/test_user_redirects.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Deploy Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- master
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout repository
|
|
16
|
+
uses: actions/checkout@v3
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v4
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.12'
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
run: |
|
|
27
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
28
|
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
29
|
+
|
|
30
|
+
- name: Setup uv cache
|
|
31
|
+
uses: actions/cache@v3
|
|
32
|
+
with:
|
|
33
|
+
path: ~/.cache/uv
|
|
34
|
+
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
|
|
35
|
+
restore-keys: |
|
|
36
|
+
${{ runner.os }}-uv-
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: |
|
|
40
|
+
uv sync
|
|
41
|
+
|
|
42
|
+
- name: Configure Git user
|
|
43
|
+
run: |
|
|
44
|
+
git config --global user.name "github-actions[bot]"
|
|
45
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
46
|
+
|
|
47
|
+
- name: Deploy documentation
|
|
48
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
49
|
+
run: |
|
|
50
|
+
uv run mkdocs gh-deploy --force
|
|
51
|
+
|
|
52
|
+
- name: Build documentation (PR)
|
|
53
|
+
if: github.event_name == 'pull_request'
|
|
54
|
+
run: uv run mkdocs build
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Pull Request
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v3
|
|
13
|
+
|
|
14
|
+
- name: Set up Python
|
|
15
|
+
uses: actions/setup-python@v4
|
|
16
|
+
with:
|
|
17
|
+
python-version: '3.12'
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
run: |
|
|
21
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
22
|
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
23
|
+
|
|
24
|
+
- name: Setup uv cache
|
|
25
|
+
uses: actions/cache@v3
|
|
26
|
+
with:
|
|
27
|
+
path: ~/.cache/uv
|
|
28
|
+
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
|
|
29
|
+
restore-keys: |
|
|
30
|
+
${{ runner.os }}-uv-
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
uv sync
|
|
35
|
+
|
|
36
|
+
- name: Lint with ruff
|
|
37
|
+
run: |
|
|
38
|
+
uv run ruff check .
|
|
39
|
+
|
|
40
|
+
- name: Test with pytest
|
|
41
|
+
run: |
|
|
42
|
+
uv run pytest
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Python package
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*.*.*"
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- name: Checkout repository
|
|
11
|
+
uses: actions/checkout@v3
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: actions/setup-python@v4
|
|
15
|
+
with:
|
|
16
|
+
python-version: '3.12'
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
run: |
|
|
20
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
21
|
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
22
|
+
|
|
23
|
+
- name: Setup uv cache
|
|
24
|
+
uses: actions/cache@v3
|
|
25
|
+
with:
|
|
26
|
+
path: ~/.cache/uv
|
|
27
|
+
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
|
|
28
|
+
restore-keys: |
|
|
29
|
+
${{ runner.os }}-uv-
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: |
|
|
33
|
+
uv sync
|
|
34
|
+
uv pip install build twine
|
|
35
|
+
|
|
36
|
+
- name: Build and publish
|
|
37
|
+
env:
|
|
38
|
+
TWINE_USERNAME: __token__
|
|
39
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }}
|
|
40
|
+
run: |
|
|
41
|
+
uv run python -m build
|
|
42
|
+
uv run twine upload dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: substack-api
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4.dev0
|
|
4
4
|
Summary: Unofficial wrapper for the Substack API
|
|
5
5
|
Project-URL: Homepage, https://github.com/nhagar/substack_api
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/nhagar/substack_api/issues
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# SubstackAuth
|
|
2
|
+
|
|
3
|
+
The `SubstackAuth` class handles authentication for accessing paywalled Substack content.
|
|
4
|
+
|
|
5
|
+
## Class Definition
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
SubstackAuth(cookies_path: str)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Parameters
|
|
12
|
+
|
|
13
|
+
- `cookies_path` (str): Path to the JSON file containing session cookies
|
|
14
|
+
|
|
15
|
+
## Properties
|
|
16
|
+
|
|
17
|
+
### `authenticated` (bool)
|
|
18
|
+
Whether the authentication was successful and cookies were loaded.
|
|
19
|
+
|
|
20
|
+
### `cookies_path` (str)
|
|
21
|
+
Path to the cookies file.
|
|
22
|
+
|
|
23
|
+
### `session` (requests.Session)
|
|
24
|
+
The authenticated requests session object.
|
|
25
|
+
|
|
26
|
+
## Methods
|
|
27
|
+
|
|
28
|
+
### `load_cookies() -> bool`
|
|
29
|
+
|
|
30
|
+
Load cookies from the specified file.
|
|
31
|
+
|
|
32
|
+
#### Returns
|
|
33
|
+
|
|
34
|
+
- `bool`: True if cookies were loaded successfully, False otherwise
|
|
35
|
+
|
|
36
|
+
### `get(url: str, **kwargs) -> requests.Response`
|
|
37
|
+
|
|
38
|
+
Make an authenticated GET request.
|
|
39
|
+
|
|
40
|
+
#### Parameters
|
|
41
|
+
|
|
42
|
+
- `url` (str): The URL to request
|
|
43
|
+
- `**kwargs`: Additional arguments passed to requests.get
|
|
44
|
+
|
|
45
|
+
#### Returns
|
|
46
|
+
|
|
47
|
+
- `requests.Response`: The response object
|
|
48
|
+
|
|
49
|
+
### `post(url: str, **kwargs) -> requests.Response`
|
|
50
|
+
|
|
51
|
+
Make an authenticated POST request.
|
|
52
|
+
|
|
53
|
+
#### Parameters
|
|
54
|
+
|
|
55
|
+
- `url` (str): The URL to request
|
|
56
|
+
- `**kwargs`: Additional arguments passed to requests.post
|
|
57
|
+
|
|
58
|
+
#### Returns
|
|
59
|
+
|
|
60
|
+
- `requests.Response`: The response object
|
|
61
|
+
|
|
62
|
+
## Example Usage
|
|
63
|
+
|
|
64
|
+
### Basic Authentication Setup
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from substack_api import SubstackAuth
|
|
68
|
+
|
|
69
|
+
# Initialize with cookies file
|
|
70
|
+
auth = SubstackAuth(cookies_path="my_cookies.json")
|
|
71
|
+
|
|
72
|
+
# Check if authentication succeeded
|
|
73
|
+
if auth.authenticated:
|
|
74
|
+
print("Successfully authenticated!")
|
|
75
|
+
else:
|
|
76
|
+
print("Authentication failed")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Using with Newsletter and Post Classes
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from substack_api import Newsletter, Post, SubstackAuth
|
|
83
|
+
|
|
84
|
+
# Set up authentication
|
|
85
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
86
|
+
|
|
87
|
+
# Use with Newsletter
|
|
88
|
+
newsletter = Newsletter("https://example.substack.com", auth=auth)
|
|
89
|
+
posts = newsletter.get_posts(limit=5)
|
|
90
|
+
|
|
91
|
+
# Use with Post
|
|
92
|
+
post = Post("https://example.substack.com/p/paywalled-post", auth=auth)
|
|
93
|
+
content = post.get_content()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Manual Authenticated Requests
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from substack_api import SubstackAuth
|
|
100
|
+
|
|
101
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
102
|
+
|
|
103
|
+
# Make authenticated GET request
|
|
104
|
+
response = auth.get("https://example.substack.com/api/v1/posts/123")
|
|
105
|
+
data = response.json()
|
|
106
|
+
|
|
107
|
+
# Make authenticated POST request
|
|
108
|
+
response = auth.post(
|
|
109
|
+
"https://example.substack.com/api/v1/some-endpoint",
|
|
110
|
+
json={"key": "value"}
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Cookie File Format
|
|
115
|
+
|
|
116
|
+
The cookies file should be in JSON format with the following structure:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
"name": "substack.sid",
|
|
122
|
+
"value": "your_session_id",
|
|
123
|
+
"domain": ".substack.com",
|
|
124
|
+
"path": "/",
|
|
125
|
+
"secure": true
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "substack.lli",
|
|
129
|
+
"value": "your_lli_value",
|
|
130
|
+
"domain": ".substack.com",
|
|
131
|
+
"path": "/",
|
|
132
|
+
"secure": true
|
|
133
|
+
},
|
|
134
|
+
...
|
|
135
|
+
]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Error Handling
|
|
139
|
+
|
|
140
|
+
The `SubstackAuth` class handles several error conditions:
|
|
141
|
+
|
|
142
|
+
- **File not found**: If the cookies file doesn't exist, `authenticated` will be `False`
|
|
143
|
+
- **Invalid JSON**: If the cookies file contains invalid JSON, `load_cookies()` returns `False`
|
|
144
|
+
- **Missing cookies**: If required cookies are missing, authentication may fail silently
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from substack_api import SubstackAuth
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
151
|
+
if not auth.authenticated:
|
|
152
|
+
print("Authentication failed - check your cookies file")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"Error setting up authentication: {e}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Security Notes
|
|
158
|
+
|
|
159
|
+
- Keep your cookies file secure and private
|
|
160
|
+
- Don't commit cookies files to version control
|
|
161
|
+
- Only use your own session cookies
|
|
162
|
+
- Cookies may expire and need to be refreshed periodically
|
|
163
|
+
- Respect Substack's Terms of Service when using authentication
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Category
|
|
2
|
+
|
|
3
|
+
The `Category` class provides access to Substack newsletter categories.
|
|
4
|
+
|
|
5
|
+
## Module Functions
|
|
6
|
+
|
|
7
|
+
### `list_all_categories() -> List[Tuple[str, int]]`
|
|
8
|
+
|
|
9
|
+
Get name/id representations of all newsletter categories.
|
|
10
|
+
|
|
11
|
+
#### Returns
|
|
12
|
+
|
|
13
|
+
- `List[Tuple[str, int]]`: List of tuples containing (category_name, category_id)
|
|
14
|
+
|
|
15
|
+
## Class Definition
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
Category(name: Optional[str] = None, id: Optional[int] = None)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Parameters
|
|
22
|
+
|
|
23
|
+
- `name` (Optional[str]): The name of the category
|
|
24
|
+
- `id` (Optional[int]): The ID of the category
|
|
25
|
+
|
|
26
|
+
### Raises
|
|
27
|
+
|
|
28
|
+
- `ValueError`: If neither name nor id is provided, or if the provided name/id is not found
|
|
29
|
+
|
|
30
|
+
## Methods
|
|
31
|
+
|
|
32
|
+
### `_get_id_from_name() -> None`
|
|
33
|
+
|
|
34
|
+
Lookup category ID based on name.
|
|
35
|
+
|
|
36
|
+
#### Raises
|
|
37
|
+
|
|
38
|
+
- `ValueError`: If the category name is not found
|
|
39
|
+
|
|
40
|
+
### `_get_name_from_id() -> None`
|
|
41
|
+
|
|
42
|
+
Lookup category name based on ID.
|
|
43
|
+
|
|
44
|
+
#### Raises
|
|
45
|
+
|
|
46
|
+
- `ValueError`: If the category ID is not found
|
|
47
|
+
|
|
48
|
+
### `_fetch_newsletters_data(force_refresh: bool = False) -> List[Dict[str, Any]]`
|
|
49
|
+
|
|
50
|
+
Fetch the raw newsletter data from the API and cache it.
|
|
51
|
+
|
|
52
|
+
#### Parameters
|
|
53
|
+
|
|
54
|
+
- `force_refresh` (bool): Whether to force a refresh of the data, ignoring the cache
|
|
55
|
+
|
|
56
|
+
#### Returns
|
|
57
|
+
|
|
58
|
+
- `List[Dict[str, Any]]`: Full newsletter metadata
|
|
59
|
+
|
|
60
|
+
### `get_newsletter_urls() -> List[str]`
|
|
61
|
+
|
|
62
|
+
Get only the URLs of newsletters in this category.
|
|
63
|
+
|
|
64
|
+
#### Returns
|
|
65
|
+
|
|
66
|
+
- `List[str]`: List of newsletter URLs
|
|
67
|
+
|
|
68
|
+
### `get_newsletters() -> List[Newsletter]`
|
|
69
|
+
|
|
70
|
+
Get Newsletter objects for all newsletters in this category.
|
|
71
|
+
|
|
72
|
+
#### Returns
|
|
73
|
+
|
|
74
|
+
- `List[Newsletter]`: List of Newsletter objects
|
|
75
|
+
|
|
76
|
+
### `get_newsletter_metadata() -> List[Dict[str, Any]]`
|
|
77
|
+
|
|
78
|
+
Get full metadata for all newsletters in this category.
|
|
79
|
+
|
|
80
|
+
#### Returns
|
|
81
|
+
|
|
82
|
+
- `List[Dict[str, Any]]`: List of newsletter metadata dictionaries
|
|
83
|
+
|
|
84
|
+
### `refresh_data() -> None`
|
|
85
|
+
|
|
86
|
+
Force refresh of the newsletter data cache.
|
|
87
|
+
|
|
88
|
+
## Example Usage
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from substack_api import Category
|
|
92
|
+
from substack_api.category import list_all_categories
|
|
93
|
+
|
|
94
|
+
# List all available categories
|
|
95
|
+
categories = list_all_categories()
|
|
96
|
+
print("Available categories:")
|
|
97
|
+
for name, id in categories:
|
|
98
|
+
print(f"- {name} (ID: {id})")
|
|
99
|
+
|
|
100
|
+
# Create a category object by name
|
|
101
|
+
tech_category = Category(name="Technology")
|
|
102
|
+
print(f"Selected category: {tech_category}")
|
|
103
|
+
|
|
104
|
+
# Get newsletters in this category
|
|
105
|
+
newsletters = tech_category.get_newsletters()
|
|
106
|
+
print(f"Found {len(newsletters)} newsletters in {tech_category.name} category")
|
|
107
|
+
|
|
108
|
+
# Print the first 5 newsletters
|
|
109
|
+
for i, newsletter in enumerate(newsletters[:5]):
|
|
110
|
+
print(f"{i+1}. {newsletter.url}")
|
|
111
|
+
|
|
112
|
+
# Get detailed metadata
|
|
113
|
+
metadata = tech_category.get_newsletter_metadata()
|
|
114
|
+
for entry in metadata[:3]:
|
|
115
|
+
print(f"Newsletter ID: {entry['id']}")
|
|
116
|
+
print(f"Description: {entry.get('description', 'No description')[:100]}...")
|
|
117
|
+
print("-" * 40)
|
|
118
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
This section provides detailed documentation for all modules and classes in the Substack API library.
|
|
4
|
+
|
|
5
|
+
## Modules
|
|
6
|
+
|
|
7
|
+
- [User](user.md): Access to Substack user profiles and subscriptions
|
|
8
|
+
- [Newsletter](newsletter.md): Access to Substack publications, posts, and podcasts
|
|
9
|
+
- [Post](post.md): Access to individual Substack post content and metadata
|
|
10
|
+
- [Category](category.md): Discovery of newsletters by category
|
|
11
|
+
- [SubstackAuth](auth.md): Authentication for accessing paywalled content
|
|
12
|
+
|
|
13
|
+
Each module documentation includes:
|
|
14
|
+
|
|
15
|
+
- Class properties and methods
|
|
16
|
+
- Method parameters
|
|
17
|
+
- Return types
|
|
18
|
+
- Example usage
|
|
19
|
+
|
|
20
|
+
## Common Patterns
|
|
21
|
+
|
|
22
|
+
Most classes in the library follow these patterns:
|
|
23
|
+
|
|
24
|
+
1. **Initialization**: Create an object by providing an identifier (URL, username, etc.)
|
|
25
|
+
2. **Data Retrieval**: Methods that fetch data from the Substack API
|
|
26
|
+
3. **Caching**: Data is cached to avoid unnecessary API requests
|
|
27
|
+
4. **Force Refresh**: Most methods accept a `force_refresh` parameter to bypass the cache
|
|
28
|
+
|
|
29
|
+
## Error Handling
|
|
30
|
+
|
|
31
|
+
The library uses standard Python exceptions:
|
|
32
|
+
|
|
33
|
+
- `requests.exceptions.HTTPError`: Raised when an HTTP request fails
|
|
34
|
+
- `ValueError`: Raised when invalid parameters are provided
|
|
35
|
+
- `KeyError`: Raised when expected data is not found in the API response
|
|
36
|
+
|
|
37
|
+
You should wrap API calls in try/except blocks to handle these exceptions gracefully.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Newsletter
|
|
2
|
+
|
|
3
|
+
The `Newsletter` class provides access to Substack publications.
|
|
4
|
+
|
|
5
|
+
## Class Definition
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
Newsletter(url: str, auth: Optional[SubstackAuth] = None)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Parameters
|
|
12
|
+
|
|
13
|
+
- `url` (str): The URL of the Substack newsletter
|
|
14
|
+
- `auth` (Optional[SubstackAuth]): Authentication handler for accessing paywalled content
|
|
15
|
+
|
|
16
|
+
## Methods
|
|
17
|
+
|
|
18
|
+
### `_fetch_paginated_posts(params: Dict[str, str], limit: Optional[int] = None, page_size: int = 15) -> List[Dict[str, Any]]`
|
|
19
|
+
|
|
20
|
+
Helper method to fetch paginated posts with different query parameters.
|
|
21
|
+
|
|
22
|
+
#### Parameters
|
|
23
|
+
|
|
24
|
+
- `params` (Dict[str, str]): Dictionary of query parameters to include in the API request
|
|
25
|
+
- `limit` (Optional[int]): Maximum number of posts to return
|
|
26
|
+
- `page_size` (int): Number of posts to retrieve per page request
|
|
27
|
+
|
|
28
|
+
#### Returns
|
|
29
|
+
|
|
30
|
+
- `List[Dict[str, Any]]`: List of post data dictionaries
|
|
31
|
+
|
|
32
|
+
### `get_posts(sorting: str = "new", limit: Optional[int] = None) -> List[Post]`
|
|
33
|
+
|
|
34
|
+
Get posts from the newsletter with specified sorting.
|
|
35
|
+
|
|
36
|
+
#### Parameters
|
|
37
|
+
|
|
38
|
+
- `sorting` (str): Sorting order for the posts ("new", "top", "pinned", or "community")
|
|
39
|
+
- `limit` (Optional[int]): Maximum number of posts to return
|
|
40
|
+
|
|
41
|
+
#### Returns
|
|
42
|
+
|
|
43
|
+
- `List[Post]`: List of Post objects
|
|
44
|
+
|
|
45
|
+
### `search_posts(query: str, limit: Optional[int] = None) -> List[Post]`
|
|
46
|
+
|
|
47
|
+
Search posts in the newsletter with the given query.
|
|
48
|
+
|
|
49
|
+
#### Parameters
|
|
50
|
+
|
|
51
|
+
- `query` (str): Search query string
|
|
52
|
+
- `limit` (Optional[int]): Maximum number of posts to return
|
|
53
|
+
|
|
54
|
+
#### Returns
|
|
55
|
+
|
|
56
|
+
- `List[Post]`: List of Post objects matching the search query
|
|
57
|
+
|
|
58
|
+
### `get_podcasts(limit: Optional[int] = None) -> List[Post]`
|
|
59
|
+
|
|
60
|
+
Get podcast posts from the newsletter.
|
|
61
|
+
|
|
62
|
+
#### Parameters
|
|
63
|
+
|
|
64
|
+
- `limit` (Optional[int]): Maximum number of podcast posts to return
|
|
65
|
+
|
|
66
|
+
#### Returns
|
|
67
|
+
|
|
68
|
+
- `List[Post]`: List of Post objects representing podcast posts
|
|
69
|
+
|
|
70
|
+
### `get_recommendations() -> List[Newsletter]`
|
|
71
|
+
|
|
72
|
+
Get recommended publications for this newsletter.
|
|
73
|
+
|
|
74
|
+
#### Returns
|
|
75
|
+
|
|
76
|
+
- `List[Newsletter]`: List of recommended Newsletter objects
|
|
77
|
+
|
|
78
|
+
### `get_authors() -> List[User]`
|
|
79
|
+
|
|
80
|
+
Get authors of the newsletter.
|
|
81
|
+
|
|
82
|
+
#### Returns
|
|
83
|
+
|
|
84
|
+
- `List[User]`: List of User objects representing the authors
|
|
85
|
+
|
|
86
|
+
## Example Usage
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from substack_api import Newsletter, SubstackAuth
|
|
90
|
+
|
|
91
|
+
# Create a newsletter object
|
|
92
|
+
newsletter = Newsletter("https://example.substack.com")
|
|
93
|
+
|
|
94
|
+
# Get recent posts
|
|
95
|
+
recent_posts = newsletter.get_posts(limit=5)
|
|
96
|
+
for post in recent_posts:
|
|
97
|
+
metadata = post.get_metadata()
|
|
98
|
+
print(f"Post: {metadata['title']}")
|
|
99
|
+
|
|
100
|
+
# Search for posts on a specific topic
|
|
101
|
+
search_results = newsletter.search_posts("machine learning", limit=3)
|
|
102
|
+
for post in search_results:
|
|
103
|
+
metadata = post.get_metadata()
|
|
104
|
+
print(f"Found: {metadata['title']}")
|
|
105
|
+
|
|
106
|
+
# Get podcast episodes
|
|
107
|
+
podcasts = newsletter.get_podcasts(limit=2)
|
|
108
|
+
for podcast in podcasts:
|
|
109
|
+
metadata = podcast.get_metadata()
|
|
110
|
+
print(f"Podcast: {metadata['title']}")
|
|
111
|
+
|
|
112
|
+
# Get newsletter authors
|
|
113
|
+
authors = newsletter.get_authors()
|
|
114
|
+
for author in authors:
|
|
115
|
+
print(f"Author: {author.name}")
|
|
116
|
+
|
|
117
|
+
# Get recommended newsletters
|
|
118
|
+
recommendations = newsletter.get_recommendations()
|
|
119
|
+
for rec in recommendations:
|
|
120
|
+
print(f"Recommended: {rec.url}")
|
|
121
|
+
|
|
122
|
+
# Use with authentication for paywalled content
|
|
123
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
124
|
+
authenticated_newsletter = Newsletter("https://example.substack.com", auth=auth)
|
|
125
|
+
paywalled_posts = authenticated_newsletter.get_posts(limit=5)
|
|
126
|
+
for post in paywalled_posts:
|
|
127
|
+
if post.is_paywalled():
|
|
128
|
+
content = post.get_content() # Now accessible with auth
|
|
129
|
+
print(f"Paywalled content: {content[:100]}...")
|
|
130
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Post
|
|
2
|
+
|
|
3
|
+
The `Post` class provides access to individual Substack posts.
|
|
4
|
+
|
|
5
|
+
## Class Definition
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
Post(url: str, auth: Optional[SubstackAuth] = None)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Parameters
|
|
12
|
+
|
|
13
|
+
- `url` (str): The URL of the Substack post
|
|
14
|
+
- `auth` (Optional[SubstackAuth]): Authentication handler for accessing paywalled content
|
|
15
|
+
|
|
16
|
+
## Methods
|
|
17
|
+
|
|
18
|
+
### `_fetch_post_data(force_refresh: bool = False) -> Dict[str, Any]`
|
|
19
|
+
|
|
20
|
+
Fetch the raw post data from the API and cache it.
|
|
21
|
+
|
|
22
|
+
#### Parameters
|
|
23
|
+
|
|
24
|
+
- `force_refresh` (bool): Whether to force a refresh of the data, ignoring the cache
|
|
25
|
+
|
|
26
|
+
#### Returns
|
|
27
|
+
|
|
28
|
+
- `Dict[str, Any]`: Full post metadata
|
|
29
|
+
|
|
30
|
+
### `get_metadata(force_refresh: bool = False) -> Dict[str, Any]`
|
|
31
|
+
|
|
32
|
+
Get metadata for the post.
|
|
33
|
+
|
|
34
|
+
#### Parameters
|
|
35
|
+
|
|
36
|
+
- `force_refresh` (bool): Whether to force a refresh of the data, ignoring the cache
|
|
37
|
+
|
|
38
|
+
#### Returns
|
|
39
|
+
|
|
40
|
+
- `Dict[str, Any]`: Full post metadata
|
|
41
|
+
|
|
42
|
+
### `get_content(force_refresh: bool = False) -> Optional[str]`
|
|
43
|
+
|
|
44
|
+
Get the HTML content of the post.
|
|
45
|
+
|
|
46
|
+
#### Parameters
|
|
47
|
+
|
|
48
|
+
- `force_refresh` (bool): Whether to force a refresh of the data, ignoring the cache
|
|
49
|
+
|
|
50
|
+
#### Returns
|
|
51
|
+
|
|
52
|
+
- `Optional[str]`: HTML content of the post, or None if not available (e.g., for paywalled content without authentication)
|
|
53
|
+
|
|
54
|
+
### `is_paywalled() -> bool`
|
|
55
|
+
|
|
56
|
+
Check if the post is paywalled.
|
|
57
|
+
|
|
58
|
+
#### Returns
|
|
59
|
+
|
|
60
|
+
- `bool`: True if the post requires a subscription to access full content
|
|
61
|
+
|
|
62
|
+
## Example Usage
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from substack_api import Post, SubstackAuth
|
|
66
|
+
|
|
67
|
+
# Create a post object
|
|
68
|
+
post = Post("https://example.substack.com/p/post-slug")
|
|
69
|
+
|
|
70
|
+
# Get post metadata
|
|
71
|
+
metadata = post.get_metadata()
|
|
72
|
+
print(f"Title: {metadata['title']}")
|
|
73
|
+
print(f"Published: {metadata['post_date']}")
|
|
74
|
+
|
|
75
|
+
# Check if the post is paywalled
|
|
76
|
+
if post.is_paywalled():
|
|
77
|
+
print("This post is paywalled")
|
|
78
|
+
|
|
79
|
+
# Set up authentication to access paywalled content
|
|
80
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
81
|
+
authenticated_post = Post("https://example.substack.com/p/post-slug", auth=auth)
|
|
82
|
+
content = authenticated_post.get_content()
|
|
83
|
+
else:
|
|
84
|
+
# Public content - no authentication needed
|
|
85
|
+
content = post.get_content()
|
|
86
|
+
|
|
87
|
+
print(f"Content length: {len(content) if content else 0}")
|
|
88
|
+
|
|
89
|
+
# Alternative: Create post with authentication from the start
|
|
90
|
+
auth = SubstackAuth(cookies_path="cookies.json")
|
|
91
|
+
authenticated_post = Post("https://example.substack.com/p/paywalled-post", auth=auth)
|
|
92
|
+
content = authenticated_post.get_content() # Works for both public and paywalled content
|
|
93
|
+
```
|