Skip to content

Commit a618951

Browse files
amckennaclaude
andcommitted
Address PR review feedback: auth/errors, lazy-load, revert unrelated docs
- get_sub_replies(): add auth check before request, map 402→ChatPaymentRequired and 403→ChatAccessDenied to match get_threads()/get_messages() behavior - ChatThread.author/created_at/comment_count: fetch lazily on first access when _thread_data is None, consistent with body property - get_thread() docstring: accurately describes lazy-fetch behavior for all properties - Revert unrelated README changes: restore uv install example and CLI section - Revert unrelated mkdocs.yml change: restore CLI: cli.md nav entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c592d7e commit a618951

3 files changed

Lines changed: 56 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,38 @@ This library provides Python interfaces for interacting with Substack's unoffici
1919
# Using pip
2020
pip install substack-api
2121

22-
# Using poetry
23-
poetry add substack-api
22+
# Using uv
23+
uv add substack-api
2424
```
2525

26-
## Usage Examples
26+
## Command-Line Interface
27+
28+
The library includes a CLI for quick access from the terminal. All commands output JSON by default.
29+
30+
```bash
31+
# Get the 5 newest posts from a newsletter
32+
substack newsletter posts https://example.substack.com --limit 5
33+
34+
# Search for posts
35+
substack newsletter search https://example.substack.com "machine learning"
36+
37+
# Get metadata for a specific post
38+
substack post metadata https://example.substack.com/p/my-post --pretty
39+
40+
# Look up a user's subscriptions
41+
substack user subscriptions username
42+
43+
# Browse categories
44+
substack categories
45+
substack category newsletters --name Technology
46+
47+
# Run the quickstart guide for a full command reference
48+
substack quickstart
49+
```
50+
51+
Use `--pretty` for human-readable output and `--cookies <path>` for authenticated access to paywalled content.
52+
53+
## Python Usage Examples
2754

2855
### Working with Newsletters
2956

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ plugins:
5252
nav:
5353
- Home: index.md
5454
- Installation: installation.md
55+
- CLI: cli.md
5556
- User Guide: user-guide.md
5657
- Authentication: authentication.md
5758
- API Reference:

‎substack_api/chat.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,20 +329,29 @@ def author(self) -> Dict[str, Any]:
329329
330330
Contains keys like 'id', 'name', 'handle', 'photo_url'.
331331
"""
332+
if self._thread_data:
333+
return self._thread_data.get("user", {})
334+
self._fetch_thread_data()
332335
if self._thread_data:
333336
return self._thread_data.get("user", {})
334337
return {}
335338

336339
@property
337340
def created_at(self) -> str:
338341
"""The ISO timestamp when the thread was created."""
342+
if self._thread_data:
343+
return self._thread_data.get("communityPost", {}).get("created_at", "")
344+
self._fetch_thread_data()
339345
if self._thread_data:
340346
return self._thread_data.get("communityPost", {}).get("created_at", "")
341347
return ""
342348

343349
@property
344350
def comment_count(self) -> int:
345351
"""The number of replies/comments in this thread."""
352+
if self._thread_data:
353+
return self._thread_data.get("communityPost", {}).get("comment_count", 0)
354+
self._fetch_thread_data()
346355
if self._thread_data:
347356
return self._thread_data.get("communityPost", {}).get("comment_count", 0)
348357
return 0
@@ -409,6 +418,11 @@ def get_sub_replies(
409418
ThreadNotFound
410419
If the parent reply is not found.
411420
"""
421+
if not self.auth or not self.auth.authenticated:
422+
raise ChatAuthenticationRequired(
423+
"Authentication is required to access sub-replies."
424+
)
425+
412426
parent_id = message.id if isinstance(message, ChatMessage) else message
413427
url = f"{BASE_URL}/community/comments/{parent_id}/comments"
414428
params = {"order": "asc", "initial": "true"}
@@ -419,6 +433,14 @@ def get_sub_replies(
419433
raise ChatAuthenticationRequired(
420434
"Authentication is required to access sub-replies."
421435
)
436+
elif response.status_code == 402:
437+
raise ChatPaymentRequired(
438+
"Access to this chat requires a paid subscription or a higher subscription tier."
439+
)
440+
elif response.status_code == 403:
441+
raise ChatAccessDenied(
442+
"You do not have permission to access this thread."
443+
)
422444
elif response.status_code == 404:
423445
raise ThreadNotFound(
424446
f"Parent reply with ID '{parent_id}' was not found."
@@ -623,8 +645,9 @@ def get_thread(self, thread_id: str) -> ChatThread:
623645
624646
Notes
625647
-----
626-
This creates a ChatThread object without pre-fetched data.
627-
The thread data will be fetched when properties are accessed.
648+
This creates a ChatThread without pre-fetched data. All properties
649+
(``body``, ``author``, ``created_at``, ``comment_count``) fetch from
650+
the API on first access and cache the result.
628651
"""
629652
return ChatThread(
630653
publication_id=self._publication_id,

0 commit comments

Comments
 (0)