-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathapi_wrapper.py
More file actions
357 lines (303 loc) · 14.2 KB
/
Copy pathapi_wrapper.py
File metadata and controls
357 lines (303 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# ABOUTME: API wrapper to handle python-substack string error responses
# ABOUTME: Provides consistent error handling for all API calls
import logging
from typing import Any, Dict, List
logger = logging.getLogger(__name__)
class SubstackAPIError(Exception):
"""Custom exception for Substack API errors"""
pass
class APIWrapper:
"""Wrapper for python-substack API client to handle string errors"""
def __init__(self, client):
"""Initialize wrapper with the underlying client
Args:
client: The python-substack API client
"""
self.client = client
self.publication_url = client.publication_url
# Debug logging
logger.debug(f"APIWrapper initialized with client type: {type(client)}")
logger.debug(f"Client has get_draft method: {hasattr(client, 'get_draft')}")
def _handle_response(self, response: Any, method_name: str) -> Any:
"""Handle API response and convert errors to exceptions
Args:
response: The API response
method_name: Name of the method called (for error messages)
Returns:
The response if valid
Raises:
SubstackAPIError: If response is an error
"""
# Check for None responses
if response is None:
raise SubstackAPIError(f"{method_name} returned None")
# Check for string responses (always errors)
if isinstance(response, str):
# Log the string error
logger.error(f"{method_name} returned string: {response}")
# Parse common error patterns
if "not found" in response.lower():
raise SubstackAPIError("Post not found")
elif (
"unauthorized" in response.lower()
or "authentication" in response.lower()
):
raise SubstackAPIError(
"Authentication failed - please check your credentials"
)
elif "rate limit" in response.lower():
raise SubstackAPIError("Rate limit exceeded - please try again later")
else:
# Generic error for any other string response
raise SubstackAPIError(f"API error: {response}")
# Check for error objects (dict with 'error' key)
if isinstance(response, dict) and "error" in response:
error_msg = response.get("error", "Unknown error")
logger.error(f"{method_name} returned error object: {error_msg}")
# Parse the error message
if isinstance(error_msg, str):
if "not found" in error_msg.lower():
raise SubstackAPIError("Post not found")
elif "unauthorized" in error_msg.lower():
raise SubstackAPIError("Authentication failed")
else:
raise SubstackAPIError(f"API error: {error_msg}")
else:
raise SubstackAPIError(f"API error: {response}")
return response
def get_user_id(self) -> str:
"""Get user ID with error handling"""
try:
result = self.client.get_user_id()
# User ID is expected to be a string, so don't use _handle_response
if result is None:
raise SubstackAPIError("get_user_id returned None")
return str(result)
except AttributeError:
# Method might not exist
raise SubstackAPIError("get_user_id method not available")
def get_draft(self, post_id: str) -> Dict[str, Any]:
"""Get a draft with error handling"""
try:
logger.debug(f"APIWrapper.get_draft called with post_id: {post_id}")
logger.debug(
f"About to call self.client.get_draft, client type: {type(self.client)}"
)
result = self.client.get_draft(post_id)
# Log what we got back
logger.debug(f"get_draft({post_id}) returned type: {type(result)}")
if isinstance(result, str):
logger.debug(f"get_draft returned string: {result}")
# Handle the response
checked_result = self._handle_response(result, "get_draft")
# Additional validation for draft structure
if not isinstance(checked_result, dict):
raise SubstackAPIError(
f"Invalid draft response - expected dict, got {type(checked_result)}"
)
# Ensure it has at least some expected fields
# Don't require all fields as draft structure may vary
if not any(
key in checked_result
for key in ["id", "draft_title", "title", "body", "draft_body"]
):
logger.warning(
f"Draft response missing expected fields. Keys: {list(checked_result.keys())[:10]}"
)
return checked_result
except SubstackAPIError:
# Let our own errors bubble up
raise
except KeyError as e:
# Handle KeyError from python-substack
key_name = str(e).strip("'")
raise SubstackAPIError(
f"Missing required field in API response: {key_name}"
)
except AttributeError as e:
# Handle AttributeError (e.g., 'str' object has no attribute 'get')
logger.error(f"AttributeError in APIWrapper.get_draft: {str(e)}")
logger.error(f"Full exception details: {repr(e)}")
raise SubstackAPIError(f"Invalid API response format: {str(e)}")
except Exception as e:
logger.error(
f"Unexpected exception in get_draft: {type(e).__name__}: {str(e)}"
)
raise SubstackAPIError(f"Failed to get post {post_id}: {str(e)}")
def get_published_posts(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get published posts with error handling"""
try:
result = self.client.get_published_posts(limit=limit)
# get_published_posts returns {'posts': [...]} not a bare list
if isinstance(result, dict) and "posts" in result:
items = result["posts"]
elif isinstance(result, list):
items = result
else:
logger.warning(
f"Unexpected get_published_posts response type: {type(result)}"
)
return []
posts = []
for item in items:
checked = self._handle_response(item, "get_published_posts[item]")
if isinstance(checked, dict):
posts.append(checked)
return posts
except Exception as e:
logger.error(f"get_published_posts error: {type(e).__name__}: {str(e)}")
return []
def get_drafts(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get drafts with error handling"""
try:
logger.info(f"APIWrapper.get_drafts called with limit={limit}")
logger.info(f"Client type: {type(self.client)}")
logger.info(f"Client has get_drafts: {hasattr(self.client, 'get_drafts')}")
result = self.client.get_drafts(limit=limit)
logger.info(f"get_drafts returned type: {type(result)}")
# Convert generator to list and check each item
drafts = []
for i, draft in enumerate(result):
logger.debug(f"Processing draft {i+1}")
checked_draft = self._handle_response(draft, "get_drafts[item]")
if isinstance(checked_draft, dict):
drafts.append(checked_draft)
logger.info(f"APIWrapper.get_drafts returning {len(drafts)} drafts")
return drafts
except Exception as e:
logger.error(f"get_drafts error: {type(e).__name__}: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return []
def post_draft(self, draft_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a draft with error handling"""
try:
result = self.client.post_draft(draft_data)
return self._handle_response(result, "post_draft")
except Exception as e:
raise SubstackAPIError(f"Failed to create draft: {str(e)}")
def put_draft(self, post_id: str, **kwargs) -> Dict[str, Any]:
"""Update a draft with error handling"""
try:
result = self.client.put_draft(post_id, **kwargs)
return self._handle_response(result, "put_draft")
except Exception as e:
raise SubstackAPIError(f"Failed to update draft: {str(e)}")
def publish_draft(self, post_id: str) -> Dict[str, Any]:
"""Publish a draft with error handling"""
try:
result = self.client.publish_draft(post_id)
return self._handle_response(result, "publish_draft")
except Exception as e:
raise SubstackAPIError(f"Failed to publish draft: {str(e)}")
def delete_draft(self, post_id: str) -> bool:
"""Delete a draft with error handling"""
try:
result = self.client.delete_draft(post_id)
if isinstance(result, str):
if "deleted" in result.lower() or "success" in result.lower():
return True
else:
raise SubstackAPIError(f"Delete failed: {result}")
return True
except Exception as e:
raise SubstackAPIError(f"Failed to delete draft: {str(e)}")
def prepublish_draft(self, post_id: str) -> Dict[str, Any]:
"""Prepublish a draft with error handling"""
try:
result = self.client.prepublish_draft(post_id)
return self._handle_response(result, "prepublish_draft")
except Exception as e:
# This method might not exist or might fail silently
logger.warning(f"prepublish_draft failed: {str(e)}")
return {}
def get_sections(self) -> List[Dict[str, Any]]:
"""Get sections with error handling"""
try:
result = self.client.get_sections()
if result is None:
return []
# Convert generator to list
sections = []
for section in result:
checked_section = self._handle_response(section, "get_sections[item]")
if isinstance(checked_section, dict):
sections.append(checked_section)
return sections
except Exception as e:
logger.error(f"get_sections error: {str(e)}")
return []
def get_publication_subscriber_count(self) -> int:
"""Get subscriber count with error handling"""
try:
# The python-substack method directly accesses ["subscriberCount"]
# which will raise KeyError if the key doesn't exist
result = self.client.get_publication_subscriber_count()
# If we get here, the library successfully extracted the count
if isinstance(result, (int, float)):
return int(result)
else:
raise SubstackAPIError(
f"Unexpected subscriber count type: {type(result)}"
)
except KeyError as e:
# This happens when the API response doesn't have 'subscriberCount' key
logger.warning(f"subscriberCount key not found in API response: {e}")
# Try alternative via sections
try:
sections = self.get_sections()
if sections:
# Sum up subscriber counts from sections
total = 0
for section in sections:
# Check multiple possible field names
count = section.get("subscriber_count", 0)
if count == 0:
# Try alternative field names
count = section.get(
"free_subscriber_count", 0
) + section.get("paid_subscriber_count", 0)
total += count
# Log what fields we found
logger.debug(
f"Section {section.get('name', 'unknown')}: subscriber_count={section.get('subscriber_count')}, "
f"free={section.get('free_subscriber_count')}, paid={section.get('paid_subscriber_count')}"
)
if total > 0:
logger.info(f"Got subscriber count from sections: {total}")
return total
# If no sections or no counts, raise error
raise SubstackAPIError(
"Unable to get subscriber count - no data available"
)
except Exception as e2:
logger.error(f"Failed to get subscriber count from sections: {e2}")
raise SubstackAPIError(
"Unable to get subscriber count from publication or sections"
)
except AttributeError as e:
# Method might not exist or client might be None
raise SubstackAPIError(f"API client error: {str(e)}")
except Exception as e:
# Any other unexpected error
logger.error(
f"Unexpected error getting subscriber count: {type(e).__name__}: {str(e)}"
)
raise SubstackAPIError(f"Failed to get subscriber count: {str(e)}")
def get_image(self, image_path: str) -> Dict[str, Any]:
"""Upload an image to Substack CDN with error handling
Args:
image_path: Path to the image file or URL
Returns:
Dict with image metadata including URL
Raises:
SubstackAPIError: If upload fails
"""
try:
result = self.client.get_image(image_path)
return self._handle_response(result, "get_image")
except FileNotFoundError:
raise SubstackAPIError(f"Image file not found: {image_path}")
except Exception as e:
logger.error(f"get_image error: {type(e).__name__}: {str(e)}")
raise SubstackAPIError(f"Failed to upload image: {str(e)}")