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.
- scholarinboxcli/__init__.py +1 -1
- scholarinboxcli/api/client.py +96 -67
- scholarinboxcli/api/endpoints.py +54 -0
- scholarinboxcli/cli.py +11 -505
- scholarinboxcli/commands/__init__.py +1 -0
- scholarinboxcli/commands/auth.py +39 -0
- scholarinboxcli/commands/bookmarks.py +49 -0
- scholarinboxcli/commands/collections.py +135 -0
- scholarinboxcli/commands/common.py +59 -0
- scholarinboxcli/commands/conferences.py +35 -0
- scholarinboxcli/commands/papers.py +88 -0
- scholarinboxcli/formatters/domain_tables.py +122 -0
- scholarinboxcli/formatters/table.py +93 -25
- scholarinboxcli/services/__init__.py +1 -0
- scholarinboxcli/services/collections.py +130 -0
- scholarinboxcli/services/paper_sort.py +54 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/METADATA +13 -46
- scholarinboxcli-0.1.2.dist-info/RECORD +23 -0
- scholarinboxcli-0.1.2.dist-info/licenses/LICENSE +21 -0
- scholarinboxcli-0.1.0.dist-info/RECORD +0 -10
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/WHEEL +0 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/entry_points.txt +0 -0
scholarinboxcli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.2"
|
scholarinboxcli/api/client.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
172
|
+
for endpoint in endpoints:
|
|
145
173
|
try:
|
|
146
|
-
return self._request("POST",
|
|
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",
|
|
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",
|
|
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"
|
|
163
|
-
return self._request("GET",
|
|
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"
|
|
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",
|
|
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",
|
|
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"
|
|
225
|
+
f"{ep.INTERACTIONS}?column={sort}&type={type_}&ascending={asc_val}",
|
|
198
226
|
)
|
|
199
227
|
|
|
200
228
|
def bookmarks(self) -> Any:
|
|
201
|
-
|
|
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",
|
|
250
|
+
return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
|
|
207
251
|
except ApiError:
|
|
208
|
-
return self._request("POST",
|
|
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",
|
|
257
|
+
return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
|
|
214
258
|
except ApiError:
|
|
215
|
-
return self._request("POST",
|
|
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",
|
|
263
|
+
return self._request("GET", ep.COLLECTIONS_PRIMARY)
|
|
220
264
|
except ApiError:
|
|
221
|
-
return self._request("GET",
|
|
265
|
+
return self._request("GET", ep.COLLECTIONS_FALLBACK)
|
|
222
266
|
|
|
223
267
|
def collections_expanded(self) -> Any:
|
|
224
|
-
return self._request("GET",
|
|
268
|
+
return self._request("GET", ep.COLLECTIONS_EXPANDED)
|
|
225
269
|
|
|
226
270
|
def collections_map(self) -> Any:
|
|
227
|
-
return self._request("GET",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
307
|
+
return self.collections_get([collection_id])
|
|
287
308
|
except ApiError:
|
|
288
|
-
|
|
289
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
381
|
+
return self._request("GET", ep.CONFERENCE_LIST)
|
|
353
382
|
|
|
354
383
|
def conference_explorer(self) -> Any:
|
|
355
|
-
return self._request("GET",
|
|
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"
|