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/cli.py
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
"""Scholar Inbox CLI."""
|
|
1
|
+
"""Scholar Inbox CLI app composition."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
5
|
import typer
|
|
9
6
|
|
|
10
|
-
from scholarinboxcli.
|
|
11
|
-
from scholarinboxcli.
|
|
12
|
-
|
|
7
|
+
from scholarinboxcli.commands import auth, bookmarks, collections, conferences, papers
|
|
8
|
+
from scholarinboxcli.services.collections import resolve_collection_id as _resolve_collection_id # noqa: F401
|
|
9
|
+
|
|
13
10
|
|
|
14
11
|
app = typer.Typer(
|
|
15
12
|
help=(
|
|
@@ -23,502 +20,11 @@ app = typer.Typer(
|
|
|
23
20
|
)
|
|
24
21
|
)
|
|
25
22
|
|
|
23
|
+
# Top-level feed/search commands
|
|
24
|
+
papers.register(app)
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
app.add_typer(auth_app, name="auth")
|
|
33
|
-
app.add_typer(collection_app, name="collection")
|
|
34
|
-
app.add_typer(bookmark_app, name="bookmark")
|
|
35
|
-
app.add_typer(conference_app, name="conference")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _print_output(data, use_json: bool, title: str | None = None) -> None:
|
|
39
|
-
if use_json or not sys.stdout.isatty():
|
|
40
|
-
typer.echo(format_json(data))
|
|
41
|
-
return
|
|
42
|
-
table = format_table(data, title=title)
|
|
43
|
-
if table == "(no results)":
|
|
44
|
-
typer.echo(table)
|
|
45
|
-
return
|
|
46
|
-
typer.echo(table)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _handle_error(err: ApiError) -> None:
|
|
50
|
-
if not sys.stdout.isatty():
|
|
51
|
-
typer.echo(format_json({"error": err.message, "status_code": err.status_code, "detail": err.detail}))
|
|
52
|
-
else:
|
|
53
|
-
typer.echo(f"Error: {err.message}", err=True)
|
|
54
|
-
if err.status_code:
|
|
55
|
-
typer.echo(f"Status: {err.status_code}", err=True)
|
|
56
|
-
raise typer.Exit(1)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _normalize_name(name: str) -> str:
|
|
60
|
-
return name.strip().lower()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _resolve_collection_id(client: ScholarInboxClient, identifier: str) -> str:
|
|
64
|
-
if identifier.isdigit():
|
|
65
|
-
return identifier
|
|
66
|
-
data = client.collections_list()
|
|
67
|
-
items = _collection_items_from_response(data)
|
|
68
|
-
candidates = _collection_candidates(items)
|
|
69
|
-
if not _candidates_have_ids(candidates):
|
|
70
|
-
try:
|
|
71
|
-
data = client.collections_expanded()
|
|
72
|
-
items = _collection_items_from_response(data)
|
|
73
|
-
candidates = _collection_candidates(items)
|
|
74
|
-
except ApiError:
|
|
75
|
-
pass
|
|
76
|
-
if not _candidates_have_ids(candidates):
|
|
77
|
-
try:
|
|
78
|
-
data = client.collections_map()
|
|
79
|
-
mapped = _collection_candidates_from_map(data)
|
|
80
|
-
if mapped:
|
|
81
|
-
candidates = mapped
|
|
82
|
-
except ApiError:
|
|
83
|
-
pass
|
|
84
|
-
if not _candidates_have_ids(candidates):
|
|
85
|
-
matched = _match_collection_name(candidates, identifier)
|
|
86
|
-
if matched:
|
|
87
|
-
# Only names are available; fall back to name as identifier.
|
|
88
|
-
return matched
|
|
89
|
-
raise ApiError("Unable to resolve collection name (no IDs available)")
|
|
90
|
-
candidates = [(name, cid) for name, cid in candidates if cid]
|
|
91
|
-
target = _normalize_name(identifier)
|
|
92
|
-
for name, cid in candidates:
|
|
93
|
-
if _normalize_name(name) == target:
|
|
94
|
-
return cid
|
|
95
|
-
# prefix match
|
|
96
|
-
prefix = [c for c in candidates if _normalize_name(c[0]).startswith(target)]
|
|
97
|
-
if len(prefix) == 1:
|
|
98
|
-
return prefix[0][1]
|
|
99
|
-
if len(prefix) > 1:
|
|
100
|
-
names = ", ".join([f"{n}({cid})" for n, cid in prefix[:10]])
|
|
101
|
-
raise ApiError(f"Ambiguous collection name. Matches: {names}")
|
|
102
|
-
# contains match
|
|
103
|
-
contains = [c for c in candidates if target in _normalize_name(c[0])]
|
|
104
|
-
if len(contains) == 1:
|
|
105
|
-
return contains[0][1]
|
|
106
|
-
if len(contains) > 1:
|
|
107
|
-
names = ", ".join([f"{n}({cid})" for n, cid in contains[:10]])
|
|
108
|
-
raise ApiError(f"Ambiguous collection name. Matches: {names}")
|
|
109
|
-
raise ApiError("Collection name not found")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _collection_candidates(items: object) -> list[tuple[str, str]]:
|
|
113
|
-
if not isinstance(items, list):
|
|
114
|
-
return []
|
|
115
|
-
candidates: list[tuple[str, str]] = []
|
|
116
|
-
for item in items:
|
|
117
|
-
if isinstance(item, dict):
|
|
118
|
-
name = item.get("name") or item.get("collection_name") or ""
|
|
119
|
-
cid = str(item.get("id") or item.get("collection_id") or "")
|
|
120
|
-
elif isinstance(item, str):
|
|
121
|
-
name = item
|
|
122
|
-
cid = ""
|
|
123
|
-
else:
|
|
124
|
-
continue
|
|
125
|
-
if name:
|
|
126
|
-
candidates.append((name, cid))
|
|
127
|
-
return candidates
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _collection_items_from_response(data: object) -> object:
|
|
131
|
-
if isinstance(data, dict):
|
|
132
|
-
for key in ("collections", "expanded_collections", "collection_names"):
|
|
133
|
-
if key in data:
|
|
134
|
-
return data.get(key)
|
|
135
|
-
return data
|
|
136
|
-
return data
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _collection_candidates_from_map(data: object) -> list[tuple[str, str]]:
|
|
140
|
-
if not isinstance(data, dict):
|
|
141
|
-
return []
|
|
142
|
-
mapping = data.get("collection_names_to_ids_dict")
|
|
143
|
-
if not isinstance(mapping, dict):
|
|
144
|
-
return []
|
|
145
|
-
candidates: list[tuple[str, str]] = []
|
|
146
|
-
for name, cid in mapping.items():
|
|
147
|
-
if name and cid is not None:
|
|
148
|
-
candidates.append((str(name), str(cid)))
|
|
149
|
-
return candidates
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _candidates_have_ids(candidates: list[tuple[str, str]]) -> bool:
|
|
153
|
-
for _, cid in candidates:
|
|
154
|
-
if cid:
|
|
155
|
-
return True
|
|
156
|
-
return False
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _match_collection_name(candidates: list[tuple[str, str]], identifier: str) -> str | None:
|
|
160
|
-
target = _normalize_name(identifier)
|
|
161
|
-
names = [(name, cid) for name, cid in candidates if name]
|
|
162
|
-
for name, _ in names:
|
|
163
|
-
if _normalize_name(name) == target:
|
|
164
|
-
return name
|
|
165
|
-
prefix = [c for c in names if _normalize_name(c[0]).startswith(target)]
|
|
166
|
-
if len(prefix) == 1:
|
|
167
|
-
return prefix[0][0]
|
|
168
|
-
if len(prefix) > 1:
|
|
169
|
-
names_str = ", ".join([n for n, _ in prefix[:10]])
|
|
170
|
-
raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
|
|
171
|
-
contains = [c for c in names if target in _normalize_name(c[0])]
|
|
172
|
-
if len(contains) == 1:
|
|
173
|
-
return contains[0][0]
|
|
174
|
-
if len(contains) > 1:
|
|
175
|
-
names_str = ", ".join([n for n, _ in contains[:10]])
|
|
176
|
-
raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
|
|
177
|
-
return None
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
@auth_app.command("login")
|
|
181
|
-
def auth_login(
|
|
182
|
-
url: str = typer.Option(..., "--url", help="Magic login URL with sha_key"),
|
|
183
|
-
):
|
|
184
|
-
client = ScholarInboxClient()
|
|
185
|
-
try:
|
|
186
|
-
client.login_with_magic_link(url)
|
|
187
|
-
typer.echo("Login successful")
|
|
188
|
-
except ApiError as e:
|
|
189
|
-
_handle_error(e)
|
|
190
|
-
finally:
|
|
191
|
-
client.close()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@auth_app.command("status")
|
|
195
|
-
def auth_status(json_output: bool = typer.Option(False, "--json", help="Output as JSON")):
|
|
196
|
-
client = ScholarInboxClient()
|
|
197
|
-
try:
|
|
198
|
-
data = client.session_info()
|
|
199
|
-
_print_output(data, json_output, title="Session")
|
|
200
|
-
except ApiError as e:
|
|
201
|
-
_handle_error(e)
|
|
202
|
-
finally:
|
|
203
|
-
client.close()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@auth_app.command("logout")
|
|
207
|
-
def auth_logout():
|
|
208
|
-
from scholarinboxcli.config import save_config, Config
|
|
209
|
-
|
|
210
|
-
save_config(Config())
|
|
211
|
-
typer.echo("Logged out")
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@app.command("digest")
|
|
215
|
-
def digest(
|
|
216
|
-
date: Optional[str] = typer.Option(None, "--date", help="Digest date (MM-DD-YYYY)"),
|
|
217
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
218
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
219
|
-
):
|
|
220
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
221
|
-
try:
|
|
222
|
-
data = client.get_digest(date)
|
|
223
|
-
_print_output(data, json_output, title="Digest")
|
|
224
|
-
except ApiError as e:
|
|
225
|
-
_handle_error(e)
|
|
226
|
-
finally:
|
|
227
|
-
client.close()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@app.command("trending")
|
|
231
|
-
def trending(
|
|
232
|
-
category: str = typer.Option("ALL", "--category", help="Category filter"),
|
|
233
|
-
days: int = typer.Option(7, "--days", help="Lookback window in days"),
|
|
234
|
-
sort: str = typer.Option("hype", "--sort", help="Sort column"),
|
|
235
|
-
asc: bool = typer.Option(False, "--asc", help="Sort ascending"),
|
|
236
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
237
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
238
|
-
):
|
|
239
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
240
|
-
try:
|
|
241
|
-
data = client.get_trending(category=category, days=days, sort=sort, asc=asc)
|
|
242
|
-
_print_output(data, json_output, title="Trending")
|
|
243
|
-
except ApiError as e:
|
|
244
|
-
_handle_error(e)
|
|
245
|
-
finally:
|
|
246
|
-
client.close()
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
@app.command("search")
|
|
250
|
-
def search(
|
|
251
|
-
query: str = typer.Argument(..., help="Search query"),
|
|
252
|
-
sort: Optional[str] = typer.Option(None, "--sort", help="Sort option"),
|
|
253
|
-
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
254
|
-
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
255
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
256
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
257
|
-
):
|
|
258
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
259
|
-
try:
|
|
260
|
-
data = client.search(query=query, sort=sort, limit=limit, offset=offset)
|
|
261
|
-
_print_output(data, json_output, title="Search")
|
|
262
|
-
except ApiError as e:
|
|
263
|
-
_handle_error(e)
|
|
264
|
-
finally:
|
|
265
|
-
client.close()
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
@app.command("semantic")
|
|
269
|
-
def semantic_search(
|
|
270
|
-
text: Optional[str] = typer.Argument(None, help="Semantic search text"),
|
|
271
|
-
file: Optional[str] = typer.Option(None, "--file", help="Read query text from file"),
|
|
272
|
-
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
273
|
-
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
274
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
275
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
276
|
-
):
|
|
277
|
-
if not text and not file:
|
|
278
|
-
typer.echo("Provide text or --file", err=True)
|
|
279
|
-
raise typer.Exit(1)
|
|
280
|
-
if file:
|
|
281
|
-
text = open(file, "r", encoding="utf-8").read()
|
|
282
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
283
|
-
try:
|
|
284
|
-
data = client.semantic_search(text=text or "", limit=limit, offset=offset)
|
|
285
|
-
_print_output(data, json_output, title="Semantic Search")
|
|
286
|
-
except ApiError as e:
|
|
287
|
-
_handle_error(e)
|
|
288
|
-
finally:
|
|
289
|
-
client.close()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
@app.command("interactions")
|
|
293
|
-
def interactions(
|
|
294
|
-
type_: str = typer.Option("all", "--type", help="Interaction type (all/up/down)"),
|
|
295
|
-
sort: str = typer.Option("ranking_score", "--sort", help="Sort column"),
|
|
296
|
-
asc: bool = typer.Option(False, "--asc", help="Sort ascending"),
|
|
297
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
298
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
299
|
-
):
|
|
300
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
301
|
-
try:
|
|
302
|
-
data = client.interactions(type_=type_, sort=sort, asc=asc)
|
|
303
|
-
_print_output(data, json_output, title="Interactions")
|
|
304
|
-
except ApiError as e:
|
|
305
|
-
_handle_error(e)
|
|
306
|
-
finally:
|
|
307
|
-
client.close()
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
@bookmark_app.command("list")
|
|
311
|
-
def bookmark_list(
|
|
312
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
313
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
314
|
-
):
|
|
315
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
316
|
-
try:
|
|
317
|
-
data = client.bookmarks()
|
|
318
|
-
_print_output(data, json_output, title="Bookmarks")
|
|
319
|
-
except ApiError as e:
|
|
320
|
-
_handle_error(e)
|
|
321
|
-
finally:
|
|
322
|
-
client.close()
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
@bookmark_app.command("add")
|
|
326
|
-
def bookmark_add(
|
|
327
|
-
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
328
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
329
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
330
|
-
):
|
|
331
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
332
|
-
try:
|
|
333
|
-
data = client.bookmark_add(paper_id)
|
|
334
|
-
_print_output(data, json_output, title="Bookmark added")
|
|
335
|
-
except ApiError as e:
|
|
336
|
-
_handle_error(e)
|
|
337
|
-
finally:
|
|
338
|
-
client.close()
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
@bookmark_app.command("remove")
|
|
342
|
-
def bookmark_remove(
|
|
343
|
-
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
344
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
345
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
346
|
-
):
|
|
347
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
348
|
-
try:
|
|
349
|
-
data = client.bookmark_remove(paper_id)
|
|
350
|
-
_print_output(data, json_output, title="Bookmark removed")
|
|
351
|
-
except ApiError as e:
|
|
352
|
-
_handle_error(e)
|
|
353
|
-
finally:
|
|
354
|
-
client.close()
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
@collection_app.command("list")
|
|
358
|
-
def collection_list(
|
|
359
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
360
|
-
expanded: bool = typer.Option(False, "--expanded", help="Use expanded collection metadata"),
|
|
361
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
362
|
-
):
|
|
363
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
364
|
-
try:
|
|
365
|
-
data = client.collections_expanded() if expanded else client.collections_list()
|
|
366
|
-
_print_output(data, json_output, title="Collections")
|
|
367
|
-
except ApiError as e:
|
|
368
|
-
_handle_error(e)
|
|
369
|
-
finally:
|
|
370
|
-
client.close()
|
|
371
|
-
|
|
372
|
-
@collection_app.command("create")
|
|
373
|
-
def collection_create(
|
|
374
|
-
name: str = typer.Argument(..., help="Collection name"),
|
|
375
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
376
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
377
|
-
):
|
|
378
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
379
|
-
try:
|
|
380
|
-
data = client.collection_create(name)
|
|
381
|
-
_print_output(data, json_output, title="Collection created")
|
|
382
|
-
except ApiError as e:
|
|
383
|
-
_handle_error(e)
|
|
384
|
-
finally:
|
|
385
|
-
client.close()
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
@collection_app.command("rename")
|
|
389
|
-
def collection_rename(
|
|
390
|
-
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
391
|
-
new_name: str = typer.Argument(..., help="New collection name"),
|
|
392
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
393
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
394
|
-
):
|
|
395
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
396
|
-
try:
|
|
397
|
-
cid = _resolve_collection_id(client, collection_id)
|
|
398
|
-
data = client.collection_rename(cid, new_name)
|
|
399
|
-
_print_output(data, json_output, title="Collection renamed")
|
|
400
|
-
except ApiError as e:
|
|
401
|
-
_handle_error(e)
|
|
402
|
-
finally:
|
|
403
|
-
client.close()
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
@collection_app.command("delete")
|
|
407
|
-
def collection_delete(
|
|
408
|
-
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
409
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
410
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
411
|
-
):
|
|
412
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
413
|
-
try:
|
|
414
|
-
cid = _resolve_collection_id(client, collection_id)
|
|
415
|
-
data = client.collection_delete(cid)
|
|
416
|
-
_print_output(data, json_output, title="Collection deleted")
|
|
417
|
-
except ApiError as e:
|
|
418
|
-
_handle_error(e)
|
|
419
|
-
finally:
|
|
420
|
-
client.close()
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
@collection_app.command("add")
|
|
424
|
-
def collection_add(
|
|
425
|
-
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
426
|
-
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
427
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
428
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
429
|
-
):
|
|
430
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
431
|
-
try:
|
|
432
|
-
cid = _resolve_collection_id(client, collection_id)
|
|
433
|
-
data = client.collection_add_paper(cid, paper_id)
|
|
434
|
-
_print_output(data, json_output, title="Collection add paper")
|
|
435
|
-
except ApiError as e:
|
|
436
|
-
_handle_error(e)
|
|
437
|
-
finally:
|
|
438
|
-
client.close()
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
@collection_app.command("remove")
|
|
442
|
-
def collection_remove(
|
|
443
|
-
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
444
|
-
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
445
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
446
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
447
|
-
):
|
|
448
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
449
|
-
try:
|
|
450
|
-
cid = _resolve_collection_id(client, collection_id)
|
|
451
|
-
data = client.collection_remove_paper(cid, paper_id)
|
|
452
|
-
_print_output(data, json_output, title="Collection remove paper")
|
|
453
|
-
except ApiError as e:
|
|
454
|
-
_handle_error(e)
|
|
455
|
-
finally:
|
|
456
|
-
client.close()
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
@collection_app.command("papers")
|
|
460
|
-
def collection_papers(
|
|
461
|
-
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
462
|
-
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
463
|
-
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
464
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
465
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
466
|
-
):
|
|
467
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
468
|
-
try:
|
|
469
|
-
cid = _resolve_collection_id(client, collection_id)
|
|
470
|
-
data = client.collection_papers(cid, limit=limit, offset=offset)
|
|
471
|
-
_print_output(data, json_output, title=f"Collection {cid}")
|
|
472
|
-
except ApiError as e:
|
|
473
|
-
_handle_error(e)
|
|
474
|
-
finally:
|
|
475
|
-
client.close()
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
@collection_app.command("similar")
|
|
479
|
-
def collection_similar(
|
|
480
|
-
collection_ids: list[str] = typer.Argument(..., help="Collection ID(s) or names"),
|
|
481
|
-
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
482
|
-
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
483
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
484
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
485
|
-
):
|
|
486
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
487
|
-
try:
|
|
488
|
-
resolved = [_resolve_collection_id(client, cid) for cid in collection_ids]
|
|
489
|
-
data = client.collections_similar(resolved, limit=limit, offset=offset)
|
|
490
|
-
_print_output(data, json_output, title="Similar Papers")
|
|
491
|
-
except ApiError as e:
|
|
492
|
-
_handle_error(e)
|
|
493
|
-
finally:
|
|
494
|
-
client.close()
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
@conference_app.command("list")
|
|
498
|
-
def conference_list(
|
|
499
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
500
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
501
|
-
):
|
|
502
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
503
|
-
try:
|
|
504
|
-
data = client.conference_list()
|
|
505
|
-
_print_output(data, json_output, title="Conferences")
|
|
506
|
-
except ApiError as e:
|
|
507
|
-
_handle_error(e)
|
|
508
|
-
finally:
|
|
509
|
-
client.close()
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@conference_app.command("explore")
|
|
513
|
-
def conference_explore(
|
|
514
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
515
|
-
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
516
|
-
):
|
|
517
|
-
client = ScholarInboxClient(no_retry=no_retry)
|
|
518
|
-
try:
|
|
519
|
-
data = client.conference_explorer()
|
|
520
|
-
_print_output(data, json_output, title="Conference Explorer")
|
|
521
|
-
except ApiError as e:
|
|
522
|
-
_handle_error(e)
|
|
523
|
-
finally:
|
|
524
|
-
client.close()
|
|
26
|
+
# Grouped commands
|
|
27
|
+
app.add_typer(auth.app, name="auth")
|
|
28
|
+
app.add_typer(collections.app, name="collection")
|
|
29
|
+
app.add_typer(bookmarks.app, name="bookmark")
|
|
30
|
+
app.add_typer(conferences.app, name="conference")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command groups for scholarinboxcli."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Authentication command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_auth_status
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Authentication commands", no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("login")
|
|
15
|
+
def auth_login(
|
|
16
|
+
url: str = typer.Option(..., "--url", help="Magic login URL with sha_key"),
|
|
17
|
+
):
|
|
18
|
+
def action(client):
|
|
19
|
+
client.login_with_magic_link(url)
|
|
20
|
+
typer.echo("Login successful")
|
|
21
|
+
|
|
22
|
+
with_client(False, action)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("status")
|
|
26
|
+
def auth_status(json_output: bool = typer.Option(False, "--json", help="Output as JSON")):
|
|
27
|
+
def action(client):
|
|
28
|
+
data = client.session_info()
|
|
29
|
+
print_output(data, json_output, title="Session", table_formatter=format_auth_status)
|
|
30
|
+
|
|
31
|
+
with_client(False, action)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("logout")
|
|
35
|
+
def auth_logout():
|
|
36
|
+
from scholarinboxcli.config import Config, save_config
|
|
37
|
+
|
|
38
|
+
save_config(Config())
|
|
39
|
+
typer.echo("Logged out")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Bookmark command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_collection_papers
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Bookmark commands", no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("list")
|
|
15
|
+
def bookmark_list(
|
|
16
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
17
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
18
|
+
):
|
|
19
|
+
def action(client):
|
|
20
|
+
data = client.bookmarks()
|
|
21
|
+
print_output(data, json_output, title="Bookmarks", table_formatter=format_collection_papers)
|
|
22
|
+
|
|
23
|
+
with_client(no_retry, action)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("add")
|
|
27
|
+
def bookmark_add(
|
|
28
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
29
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
30
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
31
|
+
):
|
|
32
|
+
def action(client):
|
|
33
|
+
data = client.bookmark_add(paper_id)
|
|
34
|
+
print_output(data, json_output, title="Bookmark added")
|
|
35
|
+
|
|
36
|
+
with_client(no_retry, action)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("remove")
|
|
40
|
+
def bookmark_remove(
|
|
41
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
42
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
43
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
44
|
+
):
|
|
45
|
+
def action(client):
|
|
46
|
+
data = client.bookmark_remove(paper_id)
|
|
47
|
+
print_output(data, json_output, title="Bookmark removed")
|
|
48
|
+
|
|
49
|
+
with_client(no_retry, action)
|