scholarinboxcli 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1 +1 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.2"
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  import os
7
6
  import time
8
7
  from urllib.parse import urlparse, parse_qs
@@ -12,7 +11,8 @@ from typing import Any
12
11
 
13
12
  import httpx
14
13
 
15
- from scholarinboxcli.config import Config, load_config, save_config
14
+ from scholarinboxcli.config import load_config, save_config
15
+ from scholarinboxcli.api import endpoints as ep
16
16
 
17
17
 
18
18
  @dataclass
@@ -81,6 +81,34 @@ def _is_paper_list(data: Any) -> bool:
81
81
  return False
82
82
 
83
83
 
84
+ def _extract_collections(data: Any) -> list[dict[str, Any]]:
85
+ if isinstance(data, dict):
86
+ for key in ("collections", "expanded_collections"):
87
+ val = data.get(key)
88
+ if isinstance(val, list):
89
+ return [item for item in val if isinstance(item, dict)]
90
+ if isinstance(data, list):
91
+ return [item for item in data if isinstance(item, dict)]
92
+ return []
93
+
94
+
95
+ def _find_collection_id(data: Any, name: str) -> str | None:
96
+ target = name.strip().lower()
97
+ for item in _extract_collections(data):
98
+ cname = str(item.get("name") or item.get("collection_name") or "").strip().lower()
99
+ if cname == target:
100
+ cid = item.get("id") or item.get("collection_id")
101
+ if cid is not None:
102
+ return str(cid)
103
+ if isinstance(data, dict):
104
+ mapping = data.get("collection_names_to_ids_dict")
105
+ if isinstance(mapping, dict):
106
+ for key, value in mapping.items():
107
+ if str(key).strip().lower() == target and value is not None:
108
+ return str(value)
109
+ return None
110
+
111
+
84
112
  class ScholarInboxClient:
85
113
  def __init__(self, api_base: str | None = None, no_retry: bool = False):
86
114
  self.no_retry = no_retry
@@ -106,7 +134,7 @@ class ScholarInboxClient:
106
134
  sha_key = None
107
135
 
108
136
  if sha_key:
109
- resp = self.client.get(f"/api/login/{sha_key}/")
137
+ resp = self.client.get(ep.LOGIN_WITH_SHA_TEMPLATE.format(sha_key=sha_key))
110
138
  if resp.status_code >= 400:
111
139
  raise ApiError("Login failed", resp.status_code, resp.text)
112
140
  self.save_cookies()
@@ -141,13 +169,13 @@ class ScholarInboxClient:
141
169
 
142
170
  def _post_first(self, endpoints: list[str], payload: dict[str, Any]) -> Any:
143
171
  last_error: ApiError | None = None
144
- for ep in endpoints:
172
+ for endpoint in endpoints:
145
173
  try:
146
- return self._request("POST", ep, json=payload)
174
+ return self._request("POST", endpoint, json=payload)
147
175
  except ApiError as e:
148
176
  last_error = e
149
177
  try:
150
- return self._request("POST", ep, data=payload)
178
+ return self._request("POST", endpoint, data=payload)
151
179
  except ApiError as e:
152
180
  last_error = e
153
181
  if last_error:
@@ -155,18 +183,18 @@ class ScholarInboxClient:
155
183
  raise ApiError("No endpoints tried")
156
184
 
157
185
  def session_info(self) -> Any:
158
- return self._request("GET", "/api/session_info")
186
+ return self._request("GET", ep.SESSION_INFO)
159
187
 
160
188
  def get_digest(self, date: str | None = None) -> Any:
161
189
  if date:
162
- return self._request("GET", f"/api/?date={date}")
163
- return self._request("GET", "/api/")
190
+ return self._request("GET", f"{ep.DIGEST}?date={date}")
191
+ return self._request("GET", ep.DIGEST)
164
192
 
165
193
  def get_trending(self, category: str = "ALL", days: int = 7, sort: str = "hype", asc: bool = False) -> Any:
166
194
  asc_val = "1" if asc else "0"
167
195
  return self._request(
168
196
  "GET",
169
- f"/api/trending?column={sort}&category={category}&ascending={asc_val}&dates={days}",
197
+ f"{ep.TRENDING}?column={sort}&category={category}&ascending={asc_val}&dates={days}",
170
198
  )
171
199
 
172
200
  def search(self, query: str, sort: str | None = None, limit: int | None = None, offset: int | None = None) -> Any:
@@ -178,7 +206,7 @@ class ScholarInboxClient:
178
206
  }
179
207
  if sort:
180
208
  payload["orderBy"] = sort
181
- return self._request("POST", "/api/get_search_results/", json=payload)
209
+ return self._request("POST", ep.SEARCH, json=payload)
182
210
 
183
211
  def semantic_search(self, text: str, limit: int | None = None, offset: int | None = None) -> Any:
184
212
  payload: dict[str, Any] = {
@@ -188,52 +216,70 @@ class ScholarInboxClient:
188
216
  }
189
217
  if limit is not None:
190
218
  payload["n_results"] = limit
191
- return self._request("POST", "/api/semantic-search", json=payload)
219
+ return self._request("POST", ep.SEMANTIC_SEARCH, json=payload)
192
220
 
193
221
  def interactions(self, type_: str = "all", sort: str = "ranking_score", asc: bool = False) -> Any:
194
222
  asc_val = "1" if asc else "0"
195
223
  return self._request(
196
224
  "GET",
197
- f"/api/interactions?column={sort}&type={type_}&ascending={asc_val}",
225
+ f"{ep.INTERACTIONS}?column={sort}&type={type_}&ascending={asc_val}",
198
226
  )
199
227
 
200
228
  def bookmarks(self) -> Any:
201
- return self._request("GET", "/api/bookmarks")
229
+ data = self.collections_list()
230
+ cid = _find_collection_id(data, "Bookmarks")
231
+ if not cid:
232
+ try:
233
+ data = self.collections_expanded()
234
+ cid = _find_collection_id(data, "Bookmarks")
235
+ except ApiError:
236
+ cid = None
237
+ if not cid:
238
+ try:
239
+ data = self.collections_map()
240
+ cid = _find_collection_id(data, "Bookmarks")
241
+ except ApiError:
242
+ cid = None
243
+ if not cid:
244
+ raise ApiError("Bookmarks collection not found")
245
+ return self.collections_get([cid])
202
246
 
203
247
  def bookmark_add(self, paper_id: str) -> Any:
204
248
  payload = {"bookmarked": True, "id": paper_id}
205
249
  try:
206
- return self._request("POST", "/api/bookmark_paper/", json=payload)
250
+ return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
207
251
  except ApiError:
208
- return self._request("POST", "/api/bookmark_paper/", data=payload)
252
+ return self._request("POST", ep.BOOKMARK_PAPER, data=payload)
209
253
 
210
254
  def bookmark_remove(self, paper_id: str) -> Any:
211
255
  payload = {"bookmarked": False, "id": paper_id}
212
256
  try:
213
- return self._request("POST", "/api/bookmark_paper/", json=payload)
257
+ return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
214
258
  except ApiError:
215
- return self._request("POST", "/api/bookmark_paper/", data=payload)
259
+ return self._request("POST", ep.BOOKMARK_PAPER, data=payload)
216
260
 
217
261
  def collections_list(self) -> Any:
218
262
  try:
219
- return self._request("GET", "/api/get_all_user_collections")
263
+ return self._request("GET", ep.COLLECTIONS_PRIMARY)
220
264
  except ApiError:
221
- return self._request("GET", "/api/collections")
265
+ return self._request("GET", ep.COLLECTIONS_FALLBACK)
222
266
 
223
267
  def collections_expanded(self) -> Any:
224
- return self._request("GET", "/api/get_expanded_collections")
268
+ return self._request("GET", ep.COLLECTIONS_EXPANDED)
225
269
 
226
270
  def collections_map(self) -> Any:
227
- return self._request("GET", "/api/collections")
271
+ return self._request("GET", ep.COLLECTIONS_FALLBACK)
272
+
273
+ def collections_get(self, collection_ids: list[str]) -> Any:
274
+ payload = {"collection_ids": collection_ids}
275
+ try:
276
+ return self._request("POST", ep.COLLECTIONS_GET, json=payload)
277
+ except ApiError:
278
+ return self._request("POST", ep.COLLECTIONS_GET, data=payload)
228
279
 
229
280
  def collection_create(self, name: str) -> Any:
230
281
  payload = {"name": name, "collection_name": name}
231
- endpoints = [
232
- "/api/create_collection/",
233
- "/api/collections",
234
- "/api/collection-create/",
235
- ]
236
- return self._post_first(endpoints, payload)
282
+ return self._post_first(list(ep.COLLECTION_CREATE_CANDIDATES), payload)
237
283
 
238
284
  def collection_rename(self, collection_id: str, new_name: str) -> Any:
239
285
  payload = {
@@ -242,51 +288,34 @@ class ScholarInboxClient:
242
288
  "name": new_name,
243
289
  "new_name": new_name,
244
290
  }
245
- endpoints = [
246
- "/api/rename_collection/",
247
- "/api/collection-rename/",
248
- "/api/collections/rename",
249
- ]
250
- return self._post_first(endpoints, payload)
291
+ return self._post_first(list(ep.COLLECTION_RENAME_CANDIDATES), payload)
251
292
 
252
293
  def collection_delete(self, collection_id: str) -> Any:
253
294
  payload = {"collection_id": collection_id, "id": collection_id}
254
- endpoints = [
255
- "/api/delete_collection/",
256
- "/api/collection-delete/",
257
- "/api/collections/delete",
258
- ]
259
- return self._post_first(endpoints, payload)
295
+ return self._post_first(list(ep.COLLECTION_DELETE_CANDIDATES), payload)
260
296
 
261
297
  def collection_add_paper(self, collection_id: str, paper_id: str) -> Any:
262
298
  payload = {"collection_id": collection_id, "paper_id": paper_id}
263
- endpoints = [
264
- "/api/add_paper_to_collection/",
265
- "/api/collection-add-paper/",
266
- "/api/add_to_collection/",
267
- ]
268
- return self._post_first(endpoints, payload)
299
+ return self._post_first(list(ep.COLLECTION_ADD_PAPER_CANDIDATES), payload)
269
300
 
270
301
  def collection_remove_paper(self, collection_id: str, paper_id: str) -> Any:
271
302
  payload = {"collection_id": collection_id, "paper_id": paper_id}
272
- endpoints = [
273
- "/api/remove_paper_from_collection/",
274
- "/api/collection-remove-paper/",
275
- "/api/remove_from_collection/",
276
- ]
277
- return self._post_first(endpoints, payload)
303
+ return self._post_first(list(ep.COLLECTION_REMOVE_PAPER_CANDIDATES), payload)
278
304
 
279
305
  def collection_papers(self, collection_id: str, limit: int | None = None, offset: int | None = None) -> Any:
280
- params: dict[str, Any] = {"collection_id": collection_id}
281
- if limit is not None:
282
- params["limit"] = limit
283
- if offset is not None:
284
- params["offset"] = offset
285
306
  try:
286
- return self._request("GET", "/api/collection-papers", params=params)
307
+ return self.collections_get([collection_id])
287
308
  except ApiError:
288
- # fallback without paging
289
- return self._request("GET", "/api/collection-papers", params={"collection_id": collection_id})
309
+ params: dict[str, Any] = {"collection_id": collection_id}
310
+ if limit is not None:
311
+ params["limit"] = limit
312
+ if offset is not None:
313
+ params["offset"] = offset
314
+ try:
315
+ return self._request("GET", ep.COLLECTION_PAPERS, params=params)
316
+ except ApiError:
317
+ # fallback without paging
318
+ return self._request("GET", ep.COLLECTION_PAPERS, params={"collection_id": collection_id})
290
319
 
291
320
  def collections_similar(self, collection_ids: list[str], limit: int | None = None, offset: int | None = None) -> Any:
292
321
  schemas = [
@@ -317,39 +346,39 @@ class ScholarInboxClient:
317
346
  payload: dict[str, Any] = {"collectionIds": collection_ids, "p": offset if offset is not None else 0}
318
347
  if limit is not None:
319
348
  payload["n_results"] = limit
320
- return self._request("POST", "/api/get_collections_similar_papers/", json=payload)
349
+ return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
321
350
  if schema == "json_collection_ids":
322
351
  payload: dict[str, Any] = {"collection_ids": collection_ids}
323
352
  if limit is not None:
324
353
  payload["limit"] = limit
325
354
  if offset is not None:
326
355
  payload["offset"] = offset
327
- return self._request("POST", "/api/get_collections_similar_papers/", json=payload)
356
+ return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
328
357
  if schema == "json_collection_id" and len(collection_ids) == 1:
329
358
  payload = {"collection_id": collection_ids[0]}
330
359
  if limit is not None:
331
360
  payload["limit"] = limit
332
361
  if offset is not None:
333
362
  payload["offset"] = offset
334
- return self._request("POST", "/api/get_collections_similar_papers/", json=payload)
363
+ return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
335
364
  if schema == "form_collection_ids":
336
365
  payload = {"collection_ids": ",".join(collection_ids)}
337
366
  if limit is not None:
338
367
  payload["limit"] = limit
339
368
  if offset is not None:
340
369
  payload["offset"] = offset
341
- return self._request("POST", "/api/get_collections_similar_papers/", data=payload)
370
+ return self._request("POST", ep.COLLECTIONS_SIMILAR, data=payload)
342
371
  if schema == "get_params":
343
372
  params = {"collection_id": ",".join(collection_ids)}
344
373
  if limit is not None:
345
374
  params["limit"] = limit
346
375
  if offset is not None:
347
376
  params["offset"] = offset
348
- return self._request("GET", "/api/get_collections_similar_papers/", params=params)
377
+ return self._request("GET", ep.COLLECTIONS_SIMILAR, params=params)
349
378
  raise ApiError("Unknown schema")
350
379
 
351
380
  def conference_list(self) -> Any:
352
- return self._request("GET", "/api/conference_list")
381
+ return self._request("GET", ep.CONFERENCE_LIST)
353
382
 
354
383
  def conference_explorer(self) -> Any:
355
- return self._request("GET", "/api/conference-explorer")
384
+ return self._request("GET", ep.CONFERENCE_EXPLORER)
@@ -0,0 +1,54 @@
1
+ """API endpoint constants used by the Scholar Inbox client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Auth/session
6
+ SESSION_INFO = "/api/session_info"
7
+ LOGIN_WITH_SHA_TEMPLATE = "/api/login/{sha_key}/"
8
+
9
+ # Feed/search
10
+ DIGEST = "/api/"
11
+ TRENDING = "/api/trending"
12
+ SEARCH = "/api/get_search_results/"
13
+ SEMANTIC_SEARCH = "/api/semantic-search"
14
+ INTERACTIONS = "/api/interactions"
15
+
16
+ # Bookmarks
17
+ BOOKMARK_PAPER = "/api/bookmark_paper/"
18
+
19
+ # Collections
20
+ COLLECTIONS_PRIMARY = "/api/get_all_user_collections"
21
+ COLLECTIONS_FALLBACK = "/api/collections"
22
+ COLLECTIONS_EXPANDED = "/api/get_expanded_collections"
23
+ COLLECTIONS_GET = "/api/get_collections"
24
+ COLLECTION_CREATE_CANDIDATES = (
25
+ "/api/create_collection/",
26
+ "/api/collections",
27
+ "/api/collection-create/",
28
+ )
29
+ COLLECTION_RENAME_CANDIDATES = (
30
+ "/api/rename_collection/",
31
+ "/api/collection-rename/",
32
+ "/api/collections/rename",
33
+ )
34
+ COLLECTION_DELETE_CANDIDATES = (
35
+ "/api/delete_collection/",
36
+ "/api/collection-delete/",
37
+ "/api/collections/delete",
38
+ )
39
+ COLLECTION_ADD_PAPER_CANDIDATES = (
40
+ "/api/add_paper_to_collection/",
41
+ "/api/collection-add-paper/",
42
+ "/api/add_to_collection/",
43
+ )
44
+ COLLECTION_REMOVE_PAPER_CANDIDATES = (
45
+ "/api/remove_paper_from_collection/",
46
+ "/api/collection-remove-paper/",
47
+ "/api/remove_from_collection/",
48
+ )
49
+ COLLECTION_PAPERS = "/api/collection-papers"
50
+ COLLECTIONS_SIMILAR = "/api/get_collections_similar_papers/"
51
+
52
+ # Conferences
53
+ CONFERENCE_LIST = "/api/conference_list"
54
+ CONFERENCE_EXPLORER = "/api/conference-explorer"