substack-api 1.1.2__py3-none-any.whl → 1.1.4.dev0__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,9 +1,17 @@
1
+ from importlib import metadata as _metadata
2
+
1
3
  from .auth import SubstackAuth
2
4
  from .category import Category, list_all_categories
3
5
  from .newsletter import Newsletter
4
6
  from .post import Post
5
7
  from .user import User, resolve_handle_redirect
6
8
 
9
+ try:
10
+ # Use distribution metadata so tag-based builds report the correct version.
11
+ __version__ = _metadata.version("substack-api")
12
+ except _metadata.PackageNotFoundError: # pragma: no cover - occurs for editable installs
13
+ __version__ = "0.0.0"
14
+
7
15
  __all__ = [
8
16
  "User",
9
17
  "Post",
@@ -12,4 +20,5 @@ __all__ = [
12
20
  "SubstackAuth",
13
21
  "resolve_handle_redirect",
14
22
  "list_all_categories",
23
+ "__version__",
15
24
  ]
@@ -1,3 +1,5 @@
1
+ import re
2
+ import urllib.parse
1
3
  from time import sleep
2
4
  from typing import Any, Dict, List, Optional
3
5
 
@@ -10,6 +12,43 @@ HEADERS = {
10
12
  }
11
13
 
12
14
 
15
+ SEARCH_URL = "https://substack.com/api/v1/publication/search"
16
+
17
+ DISCOVERY_HEADERS = {
18
+ "User-Agent": HEADERS["User-Agent"],
19
+ "Accept": "application/json",
20
+ "Origin": "https://substack.com",
21
+ "Referer": "https://substack.com/discover",
22
+ }
23
+
24
+
25
+ def _host_from_url(url: str) -> str:
26
+ host = urllib.parse.urlparse(
27
+ url if "://" in url else f"https://{url}"
28
+ ).netloc.lower()
29
+ return host
30
+
31
+
32
+ def _match_publication(search_results: dict, host: str) -> Optional[dict]:
33
+ # Try exact custom domain, then subdomain match
34
+ for item in search_results.get("publications", []):
35
+ if (
36
+ item.get("custom_domain") and _host_from_url(item["custom_domain"]) == host
37
+ ) or (
38
+ item.get("subdomain")
39
+ and f"{item['subdomain'].lower()}.substack.com" == host
40
+ ):
41
+ return item
42
+ # Fallback: loose match on subdomain token
43
+ m = re.match(r"^([a-z0-9-]+)\.substack\.com$", host)
44
+ if m:
45
+ sub = m.group(1)
46
+ for item in search_results.get("publications", []):
47
+ if item.get("subdomain", "").lower() == sub:
48
+ return item
49
+ return None
50
+
51
+
13
52
  class Newsletter:
14
53
  """
15
54
  Newsletter class for interacting with Substack newsletters
@@ -183,49 +222,73 @@ class Newsletter:
183
222
  post_data = self._fetch_paginated_posts(params, limit)
184
223
  return [Post(item["canonical_url"], auth=self.auth) for item in post_data]
185
224
 
186
- def get_recommendations(self) -> List["Newsletter"]:
225
+ def _resolve_publication_id(self) -> Optional[int]:
187
226
  """
188
- Get recommended publications for this newsletter
227
+ Resolve publication_id via Substack discovery search—no posts needed.
228
+
229
+ Parameters
230
+ ----------
231
+ None
189
232
 
190
233
  Returns
191
234
  -------
192
- List[Newsletter]
193
- List of recommended Newsletter objects
235
+ Optional[int]
236
+ The publication ID if found, otherwise None.
237
+
238
+ Raises
239
+ ------
240
+ requests.HTTPError
241
+ If the HTTP request to Substack fails.
194
242
  """
195
- # First get any post to extract the publication ID
196
- posts = self.get_posts(limit=1)
197
- if not posts:
198
- return []
243
+ host = _host_from_url(self.url)
244
+ q = host.split(":")[0] # strip port if present
245
+ params = {
246
+ "query": q,
247
+ "page": 0,
248
+ "limit": 25,
249
+ "skipExplanation": "true",
250
+ "sort": "relevance",
251
+ }
252
+ r = requests.get(
253
+ SEARCH_URL, headers=DISCOVERY_HEADERS, params=params, timeout=30
254
+ )
255
+ r.raise_for_status()
256
+ match = _match_publication(r.json(), host)
257
+ return match.get("id") if match else None
199
258
 
200
- publication_id = posts[0].get_metadata()["publication_id"]
259
+ def get_recommendations(self) -> List["Newsletter"]:
260
+ """
261
+ Get recommended publications without relying on the latest post.
262
+ """
263
+ publication_id = self._resolve_publication_id()
264
+ if not publication_id:
265
+ # graceful fallback to your existing (post-derived) path
266
+ try:
267
+ posts = self.get_posts(limit=1)
268
+ publication_id = (
269
+ posts[0].get_metadata()["publication_id"] if posts else None
270
+ )
271
+ except Exception:
272
+ publication_id = None
273
+ if not publication_id:
274
+ return []
201
275
 
202
- # Now get the recommendations
203
276
  endpoint = f"{self.url}/api/v1/recommendations/from/{publication_id}"
204
277
  response = self._make_request(endpoint, timeout=30)
205
278
  response.raise_for_status()
279
+ recommendations = response.json() or []
206
280
 
207
- recommendations = response.json()
208
- if not recommendations:
209
- return []
210
-
211
- recommended_newsletter_urls = []
281
+ urls = []
212
282
  for rec in recommendations:
213
- recpub = rec["recommendedPublication"]
214
- if "custom_domain" in recpub and recpub["custom_domain"]:
215
- recommended_newsletter_urls.append(recpub["custom_domain"])
216
- else:
217
- recommended_newsletter_urls.append(
218
- f"{recpub['subdomain']}.substack.com"
219
- )
220
-
221
- # Avoid circular import
222
- from .newsletter import Newsletter
283
+ pub = rec.get("recommendedPublication", {})
284
+ if pub.get("custom_domain"):
285
+ urls.append(pub["custom_domain"])
286
+ elif pub.get("subdomain"):
287
+ urls.append(f"{pub['subdomain']}.substack.com")
223
288
 
224
- result = [
225
- Newsletter(url, auth=self.auth) for url in recommended_newsletter_urls
226
- ]
289
+ from .newsletter import Newsletter # avoid circular import
227
290
 
228
- return result
291
+ return [Newsletter(u, auth=self.auth) for u in urls]
229
292
 
230
293
  def get_authors(self) -> List:
231
294
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: substack-api
3
- Version: 1.1.2
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,11 @@
1
+ substack_api/__init__.py,sha256=PIVnDjujILU1uVJoSr7XckDNHYHaGYkX7InO7dZZcdw,658
2
+ substack_api/auth.py,sha256=V5pU1jKZdEdsEcaL02wGO3frdxmtj-zJbafNXzH1AKI,2775
3
+ substack_api/category.py,sha256=Xzc8KOIHg_eqloiWy44Lfm-T2EWC1nbt8sd4OII_eAQ,5167
4
+ substack_api/newsletter.py,sha256=XmpX8-QIPD38-akUHzB0LMWj0-9FoYs75npeSWbvOzM,9676
5
+ substack_api/post.py,sha256=k75iXnxAECKFvIzyAPT3fjbel6dM2YJysss82ZsLsLo,3633
6
+ substack_api/user.py,sha256=aCQPdTVz0CYwCjIBkVeGi-6gz5o2pD1SbiyXD_Cj73w,8211
7
+ substack_api-1.1.4.dev0.dist-info/licenses/LICENSE,sha256=yBOIxrO0jO_AaAkiYbdydb7kg5ZO_qy4mBEYzjxIQn8,1066
8
+ substack_api-1.1.4.dev0.dist-info/METADATA,sha256=_RhPL7q0sZbJyGFkuEmBjU8v3pAFBJEE5ONpLNHjKds,5977
9
+ substack_api-1.1.4.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ substack_api-1.1.4.dev0.dist-info/top_level.txt,sha256=0CQgkJ3y5JH19TzaDyg_rjPkU6zKxEffCGksxw7qyIg,13
11
+ substack_api-1.1.4.dev0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- substack_api/__init__.py,sha256=zlsma-aBbXqRp6hoZ1e2ZZU4H5xZjJpr91EpJfAGd28,340
2
- substack_api/auth.py,sha256=V5pU1jKZdEdsEcaL02wGO3frdxmtj-zJbafNXzH1AKI,2775
3
- substack_api/category.py,sha256=Xzc8KOIHg_eqloiWy44Lfm-T2EWC1nbt8sd4OII_eAQ,5167
4
- substack_api/newsletter.py,sha256=ynvkH5iwI45pgar4lJxwAsaG_6FSlRpq0CyM85F8kIc,7664
5
- substack_api/post.py,sha256=k75iXnxAECKFvIzyAPT3fjbel6dM2YJysss82ZsLsLo,3633
6
- substack_api/user.py,sha256=aCQPdTVz0CYwCjIBkVeGi-6gz5o2pD1SbiyXD_Cj73w,8211
7
- substack_api-1.1.2.dist-info/licenses/LICENSE,sha256=yBOIxrO0jO_AaAkiYbdydb7kg5ZO_qy4mBEYzjxIQn8,1066
8
- substack_api-1.1.2.dist-info/METADATA,sha256=o8RRePSGLjiYZrS8OwEO4IB6jpT4Noor00p30JXkIrg,5972
9
- substack_api-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- substack_api-1.1.2.dist-info/top_level.txt,sha256=0CQgkJ3y5JH19TzaDyg_rjPkU6zKxEffCGksxw7qyIg,13
11
- substack_api-1.1.2.dist-info/RECORD,,