scholarinboxcli 0.1.0__tar.gz → 0.1.1__tar.gz
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-0.1.1/.github/workflows/ci.yml +17 -0
- scholarinboxcli-0.1.1/.github/workflows/publish.yml +19 -0
- scholarinboxcli-0.1.1/CHANGELOG.md +59 -0
- scholarinboxcli-0.1.1/LICENSE +21 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/PKG-INFO +8 -1
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/README.md +6 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/pyproject.toml +1 -1
- scholarinboxcli-0.1.1/src/scholarinboxcli/__init__.py +1 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/src/scholarinboxcli/api/client.py +32 -56
- scholarinboxcli-0.1.1/src/scholarinboxcli/api/endpoints.py +54 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/cli.py +30 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/__init__.py +1 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/auth.py +38 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/bookmarks.py +48 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/collections.py +130 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/common.py +53 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/conferences.py +34 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/commands/papers.py +88 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/services/__init__.py +1 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/services/collections.py +132 -0
- scholarinboxcli-0.1.1/tests/test_api_client_mocked.py +60 -0
- scholarinboxcli-0.1.1/tests/test_cli_smoke.py +51 -0
- scholarinboxcli-0.1.1/tests/test_collection_resolution.py +85 -0
- scholarinboxcli-0.1.0/src/scholarinboxcli/__init__.py +0 -1
- scholarinboxcli-0.1.0/src/scholarinboxcli/cli.py +0 -524
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/.gitignore +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/src/scholarinboxcli/config.py +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/src/scholarinboxcli/formatters/json_fmt.py +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/src/scholarinboxcli/formatters/table.py +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.1}/uv.lock +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
- run: uv python install 3.11
|
|
16
|
+
- run: uv run scholarinboxcli --help
|
|
17
|
+
- run: uv run --with pytest pytest -q
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv python install 3.11
|
|
18
|
+
- run: uv build
|
|
19
|
+
- run: uv publish --trusted-publishing=automatic --check-url https://pypi.org/simple/scholarinboxcli/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2026-02-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `LICENSE` (MIT) file.
|
|
15
|
+
- Added CI workflow (`.github/workflows/ci.yml`) for help smoke-check + pytest.
|
|
16
|
+
- Added offline smoke tests for CLI commands with mocked client behavior.
|
|
17
|
+
- Added logic-focused collection resolution unit tests (exact/prefix/contains/ambiguity/fallbacks).
|
|
18
|
+
- Added mocked API client tests for bookmark payload/fallback behavior and conference explorer calls.
|
|
19
|
+
- Added Keep a Changelog style `CHANGELOG.md`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Refactored CLI into focused command modules (`auth`, `papers`, `bookmarks`, `collections`, `conferences`).
|
|
24
|
+
- Moved collection name/ID resolution into a dedicated service module.
|
|
25
|
+
- Centralized API endpoint constants in `api/endpoints.py`.
|
|
26
|
+
- Reduced command boilerplate via shared `with_client()` helper for client lifecycle and error handling.
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-02-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Initial `scholarinboxcli` command-line interface with Typer + Rich and JSON output.
|
|
33
|
+
- Auth commands: `auth login`, `auth status`, `auth logout`.
|
|
34
|
+
- Research workflow commands: `digest`, `trending`, `search`, `semantic`, `interactions`.
|
|
35
|
+
- Bookmark commands: `bookmark list`, `bookmark add`, `bookmark remove`.
|
|
36
|
+
- Collection commands: `collection list/create/rename/delete/add/remove/papers/similar`.
|
|
37
|
+
- Conference commands: `conference list`, `conference explore`.
|
|
38
|
+
- Collection name resolution by ID or name, including fallback ID map lookup.
|
|
39
|
+
- README quickstart, command examples, tested-command matrix, and release instructions.
|
|
40
|
+
- GitHub Actions trusted-publishing workflow for tag-based PyPI release.
|
|
41
|
+
- Project tagline and CLI help examples for humans and agents.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Standardized JSON output to pretty-printed format for both `--json` and piped output.
|
|
46
|
+
- Simplified `conference explore` to list-style behavior by removing ineffective query/sort options.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- Bookmark add/remove payloads aligned with the web API endpoint behavior.
|
|
51
|
+
- Collection name-to-ID resolution improved for cases where list endpoints omit IDs.
|
|
52
|
+
|
|
53
|
+
### Removed
|
|
54
|
+
|
|
55
|
+
- Tracked Python bytecode artifacts (`.pyc`) removed from repository history and ignored via `.gitignore`.
|
|
56
|
+
|
|
57
|
+
[Unreleased]: https://github.com/mrshu/scholarinboxcli/compare/v0.1.1...HEAD
|
|
58
|
+
[0.1.1]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.1
|
|
59
|
+
[0.1.0]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marek Suppa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarinboxcli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: CLI for Scholar Inbox (authenticated web API)
|
|
5
5
|
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: bibliography,cli,research,scholar
|
|
7
8
|
Classifier: Development Status :: 3 - Alpha
|
|
8
9
|
Classifier: Environment :: Console
|
|
@@ -234,3 +235,9 @@ If using an API token:
|
|
|
234
235
|
export TWINE_USERNAME=__token__
|
|
235
236
|
export TWINE_PASSWORD=<your-pypi-token>
|
|
236
237
|
```
|
|
238
|
+
|
|
239
|
+
Automated publish is also configured via GitHub Actions:
|
|
240
|
+
|
|
241
|
+
- Workflow: `.github/workflows/publish.yml`
|
|
242
|
+
- Trigger: push a tag matching `v*` (for example `v0.1.1`)
|
|
243
|
+
- Auth: PyPI Trusted Publishing (OIDC)
|
|
@@ -213,3 +213,9 @@ If using an API token:
|
|
|
213
213
|
export TWINE_USERNAME=__token__
|
|
214
214
|
export TWINE_PASSWORD=<your-pypi-token>
|
|
215
215
|
```
|
|
216
|
+
|
|
217
|
+
Automated publish is also configured via GitHub Actions:
|
|
218
|
+
|
|
219
|
+
- Workflow: `.github/workflows/publish.yml`
|
|
220
|
+
- Trigger: push a tag matching `v*` (for example `v0.1.1`)
|
|
221
|
+
- Auth: PyPI Trusted Publishing (OIDC)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
15
|
from scholarinboxcli.config import Config, load_config, save_config
|
|
16
|
+
from scholarinboxcli.api import endpoints as ep
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass
|
|
@@ -106,7 +107,7 @@ class ScholarInboxClient:
|
|
|
106
107
|
sha_key = None
|
|
107
108
|
|
|
108
109
|
if sha_key:
|
|
109
|
-
resp = self.client.get(
|
|
110
|
+
resp = self.client.get(ep.LOGIN_WITH_SHA_TEMPLATE.format(sha_key=sha_key))
|
|
110
111
|
if resp.status_code >= 400:
|
|
111
112
|
raise ApiError("Login failed", resp.status_code, resp.text)
|
|
112
113
|
self.save_cookies()
|
|
@@ -155,18 +156,18 @@ class ScholarInboxClient:
|
|
|
155
156
|
raise ApiError("No endpoints tried")
|
|
156
157
|
|
|
157
158
|
def session_info(self) -> Any:
|
|
158
|
-
return self._request("GET",
|
|
159
|
+
return self._request("GET", ep.SESSION_INFO)
|
|
159
160
|
|
|
160
161
|
def get_digest(self, date: str | None = None) -> Any:
|
|
161
162
|
if date:
|
|
162
|
-
return self._request("GET", f"
|
|
163
|
-
return self._request("GET",
|
|
163
|
+
return self._request("GET", f"{ep.DIGEST}?date={date}")
|
|
164
|
+
return self._request("GET", ep.DIGEST)
|
|
164
165
|
|
|
165
166
|
def get_trending(self, category: str = "ALL", days: int = 7, sort: str = "hype", asc: bool = False) -> Any:
|
|
166
167
|
asc_val = "1" if asc else "0"
|
|
167
168
|
return self._request(
|
|
168
169
|
"GET",
|
|
169
|
-
f"
|
|
170
|
+
f"{ep.TRENDING}?column={sort}&category={category}&ascending={asc_val}&dates={days}",
|
|
170
171
|
)
|
|
171
172
|
|
|
172
173
|
def search(self, query: str, sort: str | None = None, limit: int | None = None, offset: int | None = None) -> Any:
|
|
@@ -178,7 +179,7 @@ class ScholarInboxClient:
|
|
|
178
179
|
}
|
|
179
180
|
if sort:
|
|
180
181
|
payload["orderBy"] = sort
|
|
181
|
-
return self._request("POST",
|
|
182
|
+
return self._request("POST", ep.SEARCH, json=payload)
|
|
182
183
|
|
|
183
184
|
def semantic_search(self, text: str, limit: int | None = None, offset: int | None = None) -> Any:
|
|
184
185
|
payload: dict[str, Any] = {
|
|
@@ -188,52 +189,47 @@ class ScholarInboxClient:
|
|
|
188
189
|
}
|
|
189
190
|
if limit is not None:
|
|
190
191
|
payload["n_results"] = limit
|
|
191
|
-
return self._request("POST",
|
|
192
|
+
return self._request("POST", ep.SEMANTIC_SEARCH, json=payload)
|
|
192
193
|
|
|
193
194
|
def interactions(self, type_: str = "all", sort: str = "ranking_score", asc: bool = False) -> Any:
|
|
194
195
|
asc_val = "1" if asc else "0"
|
|
195
196
|
return self._request(
|
|
196
197
|
"GET",
|
|
197
|
-
f"
|
|
198
|
+
f"{ep.INTERACTIONS}?column={sort}&type={type_}&ascending={asc_val}",
|
|
198
199
|
)
|
|
199
200
|
|
|
200
201
|
def bookmarks(self) -> Any:
|
|
201
|
-
return self._request("GET",
|
|
202
|
+
return self._request("GET", ep.BOOKMARKS)
|
|
202
203
|
|
|
203
204
|
def bookmark_add(self, paper_id: str) -> Any:
|
|
204
205
|
payload = {"bookmarked": True, "id": paper_id}
|
|
205
206
|
try:
|
|
206
|
-
return self._request("POST",
|
|
207
|
+
return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
|
|
207
208
|
except ApiError:
|
|
208
|
-
return self._request("POST",
|
|
209
|
+
return self._request("POST", ep.BOOKMARK_PAPER, data=payload)
|
|
209
210
|
|
|
210
211
|
def bookmark_remove(self, paper_id: str) -> Any:
|
|
211
212
|
payload = {"bookmarked": False, "id": paper_id}
|
|
212
213
|
try:
|
|
213
|
-
return self._request("POST",
|
|
214
|
+
return self._request("POST", ep.BOOKMARK_PAPER, json=payload)
|
|
214
215
|
except ApiError:
|
|
215
|
-
return self._request("POST",
|
|
216
|
+
return self._request("POST", ep.BOOKMARK_PAPER, data=payload)
|
|
216
217
|
|
|
217
218
|
def collections_list(self) -> Any:
|
|
218
219
|
try:
|
|
219
|
-
return self._request("GET",
|
|
220
|
+
return self._request("GET", ep.COLLECTIONS_PRIMARY)
|
|
220
221
|
except ApiError:
|
|
221
|
-
return self._request("GET",
|
|
222
|
+
return self._request("GET", ep.COLLECTIONS_FALLBACK)
|
|
222
223
|
|
|
223
224
|
def collections_expanded(self) -> Any:
|
|
224
|
-
return self._request("GET",
|
|
225
|
+
return self._request("GET", ep.COLLECTIONS_EXPANDED)
|
|
225
226
|
|
|
226
227
|
def collections_map(self) -> Any:
|
|
227
|
-
return self._request("GET",
|
|
228
|
+
return self._request("GET", ep.COLLECTIONS_FALLBACK)
|
|
228
229
|
|
|
229
230
|
def collection_create(self, name: str) -> Any:
|
|
230
231
|
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)
|
|
232
|
+
return self._post_first(list(ep.COLLECTION_CREATE_CANDIDATES), payload)
|
|
237
233
|
|
|
238
234
|
def collection_rename(self, collection_id: str, new_name: str) -> Any:
|
|
239
235
|
payload = {
|
|
@@ -242,39 +238,19 @@ class ScholarInboxClient:
|
|
|
242
238
|
"name": new_name,
|
|
243
239
|
"new_name": new_name,
|
|
244
240
|
}
|
|
245
|
-
|
|
246
|
-
"/api/rename_collection/",
|
|
247
|
-
"/api/collection-rename/",
|
|
248
|
-
"/api/collections/rename",
|
|
249
|
-
]
|
|
250
|
-
return self._post_first(endpoints, payload)
|
|
241
|
+
return self._post_first(list(ep.COLLECTION_RENAME_CANDIDATES), payload)
|
|
251
242
|
|
|
252
243
|
def collection_delete(self, collection_id: str) -> Any:
|
|
253
244
|
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)
|
|
245
|
+
return self._post_first(list(ep.COLLECTION_DELETE_CANDIDATES), payload)
|
|
260
246
|
|
|
261
247
|
def collection_add_paper(self, collection_id: str, paper_id: str) -> Any:
|
|
262
248
|
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)
|
|
249
|
+
return self._post_first(list(ep.COLLECTION_ADD_PAPER_CANDIDATES), payload)
|
|
269
250
|
|
|
270
251
|
def collection_remove_paper(self, collection_id: str, paper_id: str) -> Any:
|
|
271
252
|
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)
|
|
253
|
+
return self._post_first(list(ep.COLLECTION_REMOVE_PAPER_CANDIDATES), payload)
|
|
278
254
|
|
|
279
255
|
def collection_papers(self, collection_id: str, limit: int | None = None, offset: int | None = None) -> Any:
|
|
280
256
|
params: dict[str, Any] = {"collection_id": collection_id}
|
|
@@ -283,10 +259,10 @@ class ScholarInboxClient:
|
|
|
283
259
|
if offset is not None:
|
|
284
260
|
params["offset"] = offset
|
|
285
261
|
try:
|
|
286
|
-
return self._request("GET",
|
|
262
|
+
return self._request("GET", ep.COLLECTION_PAPERS, params=params)
|
|
287
263
|
except ApiError:
|
|
288
264
|
# fallback without paging
|
|
289
|
-
return self._request("GET",
|
|
265
|
+
return self._request("GET", ep.COLLECTION_PAPERS, params={"collection_id": collection_id})
|
|
290
266
|
|
|
291
267
|
def collections_similar(self, collection_ids: list[str], limit: int | None = None, offset: int | None = None) -> Any:
|
|
292
268
|
schemas = [
|
|
@@ -317,39 +293,39 @@ class ScholarInboxClient:
|
|
|
317
293
|
payload: dict[str, Any] = {"collectionIds": collection_ids, "p": offset if offset is not None else 0}
|
|
318
294
|
if limit is not None:
|
|
319
295
|
payload["n_results"] = limit
|
|
320
|
-
return self._request("POST",
|
|
296
|
+
return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
|
|
321
297
|
if schema == "json_collection_ids":
|
|
322
298
|
payload: dict[str, Any] = {"collection_ids": collection_ids}
|
|
323
299
|
if limit is not None:
|
|
324
300
|
payload["limit"] = limit
|
|
325
301
|
if offset is not None:
|
|
326
302
|
payload["offset"] = offset
|
|
327
|
-
return self._request("POST",
|
|
303
|
+
return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
|
|
328
304
|
if schema == "json_collection_id" and len(collection_ids) == 1:
|
|
329
305
|
payload = {"collection_id": collection_ids[0]}
|
|
330
306
|
if limit is not None:
|
|
331
307
|
payload["limit"] = limit
|
|
332
308
|
if offset is not None:
|
|
333
309
|
payload["offset"] = offset
|
|
334
|
-
return self._request("POST",
|
|
310
|
+
return self._request("POST", ep.COLLECTIONS_SIMILAR, json=payload)
|
|
335
311
|
if schema == "form_collection_ids":
|
|
336
312
|
payload = {"collection_ids": ",".join(collection_ids)}
|
|
337
313
|
if limit is not None:
|
|
338
314
|
payload["limit"] = limit
|
|
339
315
|
if offset is not None:
|
|
340
316
|
payload["offset"] = offset
|
|
341
|
-
return self._request("POST",
|
|
317
|
+
return self._request("POST", ep.COLLECTIONS_SIMILAR, data=payload)
|
|
342
318
|
if schema == "get_params":
|
|
343
319
|
params = {"collection_id": ",".join(collection_ids)}
|
|
344
320
|
if limit is not None:
|
|
345
321
|
params["limit"] = limit
|
|
346
322
|
if offset is not None:
|
|
347
323
|
params["offset"] = offset
|
|
348
|
-
return self._request("GET",
|
|
324
|
+
return self._request("GET", ep.COLLECTIONS_SIMILAR, params=params)
|
|
349
325
|
raise ApiError("Unknown schema")
|
|
350
326
|
|
|
351
327
|
def conference_list(self) -> Any:
|
|
352
|
-
return self._request("GET",
|
|
328
|
+
return self._request("GET", ep.CONFERENCE_LIST)
|
|
353
329
|
|
|
354
330
|
def conference_explorer(self) -> Any:
|
|
355
|
-
return self._request("GET",
|
|
331
|
+
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
|
+
BOOKMARKS = "/api/bookmarks"
|
|
18
|
+
BOOKMARK_PAPER = "/api/bookmark_paper/"
|
|
19
|
+
|
|
20
|
+
# Collections
|
|
21
|
+
COLLECTIONS_PRIMARY = "/api/get_all_user_collections"
|
|
22
|
+
COLLECTIONS_FALLBACK = "/api/collections"
|
|
23
|
+
COLLECTIONS_EXPANDED = "/api/get_expanded_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"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Scholar Inbox CLI app composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from scholarinboxcli.commands import auth, bookmarks, collections, conferences, papers
|
|
8
|
+
from scholarinboxcli.services.collections import resolve_collection_id as _resolve_collection_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
help=(
|
|
13
|
+
"Scholar Inbox CLI.\n\n"
|
|
14
|
+
"Examples:\n"
|
|
15
|
+
" scholarinboxcli auth login --url \"https://www.scholar-inbox.com/login?sha_key=...&date=MM-DD-YYYY\"\n"
|
|
16
|
+
" scholarinboxcli digest --date 01-30-2026 --json\n"
|
|
17
|
+
" scholarinboxcli search \"transformers\" --limit 5 --json\n"
|
|
18
|
+
" scholarinboxcli collection papers \"AIAgents\" --json\n"
|
|
19
|
+
" scholarinboxcli conference explore --json\n"
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Top-level feed/search commands
|
|
24
|
+
papers.register(app)
|
|
25
|
+
|
|
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,38 @@
|
|
|
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
|
+
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Authentication commands", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("login")
|
|
14
|
+
def auth_login(
|
|
15
|
+
url: str = typer.Option(..., "--url", help="Magic login URL with sha_key"),
|
|
16
|
+
):
|
|
17
|
+
def action(client):
|
|
18
|
+
client.login_with_magic_link(url)
|
|
19
|
+
typer.echo("Login successful")
|
|
20
|
+
|
|
21
|
+
with_client(False, action)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("status")
|
|
25
|
+
def auth_status(json_output: bool = typer.Option(False, "--json", help="Output as JSON")):
|
|
26
|
+
def action(client):
|
|
27
|
+
data = client.session_info()
|
|
28
|
+
print_output(data, json_output, title="Session")
|
|
29
|
+
|
|
30
|
+
with_client(False, action)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("logout")
|
|
34
|
+
def auth_logout():
|
|
35
|
+
from scholarinboxcli.config import Config, save_config
|
|
36
|
+
|
|
37
|
+
save_config(Config())
|
|
38
|
+
typer.echo("Logged out")
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Bookmark commands", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("list")
|
|
14
|
+
def bookmark_list(
|
|
15
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
16
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
17
|
+
):
|
|
18
|
+
def action(client):
|
|
19
|
+
data = client.bookmarks()
|
|
20
|
+
print_output(data, json_output, title="Bookmarks")
|
|
21
|
+
|
|
22
|
+
with_client(no_retry, action)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("add")
|
|
26
|
+
def bookmark_add(
|
|
27
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
28
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
29
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
30
|
+
):
|
|
31
|
+
def action(client):
|
|
32
|
+
data = client.bookmark_add(paper_id)
|
|
33
|
+
print_output(data, json_output, title="Bookmark added")
|
|
34
|
+
|
|
35
|
+
with_client(no_retry, action)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("remove")
|
|
39
|
+
def bookmark_remove(
|
|
40
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
41
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
42
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
43
|
+
):
|
|
44
|
+
def action(client):
|
|
45
|
+
data = client.bookmark_remove(paper_id)
|
|
46
|
+
print_output(data, json_output, title="Bookmark removed")
|
|
47
|
+
|
|
48
|
+
with_client(no_retry, action)
|