scholarinboxcli 0.1.0__tar.gz → 0.1.2__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.2/.github/workflows/ci.yml +17 -0
- scholarinboxcli-0.1.2/.github/workflows/publish.yml +19 -0
- scholarinboxcli-0.1.2/CHANGELOG.md +74 -0
- scholarinboxcli-0.1.2/LICENSE +21 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/PKG-INFO +13 -46
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/README.md +11 -45
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/pyproject.toml +7 -1
- scholarinboxcli-0.1.2/src/scholarinboxcli/__init__.py +1 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/src/scholarinboxcli/api/client.py +96 -67
- scholarinboxcli-0.1.2/src/scholarinboxcli/api/endpoints.py +54 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/cli.py +30 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/__init__.py +1 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/auth.py +39 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/bookmarks.py +49 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/collections.py +135 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/common.py +59 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/conferences.py +35 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/commands/papers.py +88 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/formatters/domain_tables.py +122 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/formatters/table.py +134 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/services/__init__.py +1 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/services/collections.py +130 -0
- scholarinboxcli-0.1.2/src/scholarinboxcli/services/paper_sort.py +54 -0
- scholarinboxcli-0.1.2/tests/test_api_client_mocked.py +83 -0
- scholarinboxcli-0.1.2/tests/test_cli_smoke.py +52 -0
- scholarinboxcli-0.1.2/tests/test_collection_resolution.py +85 -0
- scholarinboxcli-0.1.2/tests/test_domain_tables.py +60 -0
- scholarinboxcli-0.1.2/tests/test_paper_sort.py +27 -0
- scholarinboxcli-0.1.2/tests/test_table_formatter.py +64 -0
- scholarinboxcli-0.1.2/uv.lock +334 -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/src/scholarinboxcli/formatters/table.py +0 -66
- scholarinboxcli-0.1.0/uv.lock +0 -198
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/.gitignore +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/src/scholarinboxcli/config.py +0 -0
- {scholarinboxcli-0.1.0 → scholarinboxcli-0.1.2}/src/scholarinboxcli/formatters/json_fmt.py +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,74 @@
|
|
|
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.2] - 2026-02-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added collection-aware paper formatting for bookmark and collection views.
|
|
15
|
+
- Added tests for bookmark collection lookup and collection paper formatting.
|
|
16
|
+
- Added dev tooling dependencies for pytest and ruff.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Bookmarks now resolve the "Bookmarks" collection and load papers via `/api/get_collections`.
|
|
21
|
+
- Collection paper retrieval prefers `/api/get_collections` with fallback to legacy endpoints.
|
|
22
|
+
- Cleaned up minor lint issues and restored `_resolve_collection_id` export for tests.
|
|
23
|
+
|
|
24
|
+
## [0.1.1] - 2026-02-01
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Added `LICENSE` (MIT) file.
|
|
29
|
+
- Added CI workflow (`.github/workflows/ci.yml`) for help smoke-check + pytest.
|
|
30
|
+
- Added offline smoke tests for CLI commands with mocked client behavior.
|
|
31
|
+
- Added logic-focused collection resolution unit tests (exact/prefix/contains/ambiguity/fallbacks).
|
|
32
|
+
- Added mocked API client tests for bookmark payload/fallback behavior and conference explorer calls.
|
|
33
|
+
- Added Keep a Changelog style `CHANGELOG.md`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Refactored CLI into focused command modules (`auth`, `papers`, `bookmarks`, `collections`, `conferences`).
|
|
38
|
+
- Moved collection name/ID resolution into a dedicated service module.
|
|
39
|
+
- Centralized API endpoint constants in `api/endpoints.py`.
|
|
40
|
+
- Reduced command boilerplate via shared `with_client()` helper for client lifecycle and error handling.
|
|
41
|
+
|
|
42
|
+
## [0.1.0] - 2026-02-01
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Initial `scholarinboxcli` command-line interface with Typer + Rich and JSON output.
|
|
47
|
+
- Auth commands: `auth login`, `auth status`, `auth logout`.
|
|
48
|
+
- Research workflow commands: `digest`, `trending`, `search`, `semantic`, `interactions`.
|
|
49
|
+
- Bookmark commands: `bookmark list`, `bookmark add`, `bookmark remove`.
|
|
50
|
+
- Collection commands: `collection list/create/rename/delete/add/remove/papers/similar`.
|
|
51
|
+
- Conference commands: `conference list`, `conference explore`.
|
|
52
|
+
- Collection name resolution by ID or name, including fallback ID map lookup.
|
|
53
|
+
- README quickstart, command examples, tested-command matrix, and release instructions.
|
|
54
|
+
- GitHub Actions trusted-publishing workflow for tag-based PyPI release.
|
|
55
|
+
- Project tagline and CLI help examples for humans and agents.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- Standardized JSON output to pretty-printed format for both `--json` and piped output.
|
|
60
|
+
- Simplified `conference explore` to list-style behavior by removing ineffective query/sort options.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Bookmark add/remove payloads aligned with the web API endpoint behavior.
|
|
65
|
+
- Collection name-to-ID resolution improved for cases where list endpoints omit IDs.
|
|
66
|
+
|
|
67
|
+
### Removed
|
|
68
|
+
|
|
69
|
+
- Tracked Python bytecode artifacts (`.pyc`) removed from repository history and ignored via `.gitignore`.
|
|
70
|
+
|
|
71
|
+
[Unreleased]: https://github.com/mrshu/scholarinboxcli/compare/v0.1.2...HEAD
|
|
72
|
+
[0.1.2]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.2
|
|
73
|
+
[0.1.1]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.1
|
|
74
|
+
[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.2
|
|
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
|
|
@@ -21,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
21
22
|
|
|
22
23
|
# scholarinboxcli
|
|
23
24
|
|
|
24
|
-
CLI for Scholar Inbox, for humans and agents alike.
|
|
25
|
+
CLI for [Scholar Inbox](https://scholar-inbox.com), for humans and agents alike.
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
27
28
|
|
|
@@ -121,6 +122,12 @@ scholarinboxcli collection papers 10759
|
|
|
121
122
|
# Similar papers for one or more collections
|
|
122
123
|
scholarinboxcli collection similar 10759 12345
|
|
123
124
|
|
|
125
|
+
# Optional local sorting for display (e.g., newest first)
|
|
126
|
+
scholarinboxcli collection similar "AIAgents" --sort year
|
|
127
|
+
|
|
128
|
+
# Sort ascending instead
|
|
129
|
+
scholarinboxcli collection similar "AIAgents" --sort year --asc
|
|
130
|
+
|
|
124
131
|
# You can also use collection names (case-insensitive). The CLI will
|
|
125
132
|
# automatically fetch collection ID mappings from the API when needed.
|
|
126
133
|
scholarinboxcli collection papers "AIAgents"
|
|
@@ -128,6 +135,7 @@ scholarinboxcli collection similar "AIAgents" "Benchmark"
|
|
|
128
135
|
```
|
|
129
136
|
|
|
130
137
|
Collection name matching is exact → prefix → contains. If multiple matches exist, the CLI reports ambiguity and shows candidate IDs.
|
|
138
|
+
`collection similar` supports client-side sorting with `--sort year|title` and optional `--asc`.
|
|
131
139
|
|
|
132
140
|
## Search
|
|
133
141
|
|
|
@@ -184,53 +192,12 @@ scholarinboxcli collection papers "AIAgents" --json
|
|
|
184
192
|
scholarinboxcli search "diffusion" --json
|
|
185
193
|
```
|
|
186
194
|
|
|
187
|
-
## Tested (2026-02-01)
|
|
188
|
-
|
|
189
|
-
The following commands were exercised against the live API (with a valid magic-link login) to confirm behavior:
|
|
190
|
-
|
|
191
|
-
```bash
|
|
192
|
-
scholarinboxcli --help
|
|
193
|
-
scholarinboxcli auth status --json
|
|
194
|
-
scholarinboxcli digest --date 01-30-2026 --json
|
|
195
|
-
scholarinboxcli trending --category ALL --days 7 --json
|
|
196
|
-
scholarinboxcli search "transformers" --limit 5 --json
|
|
197
|
-
scholarinboxcli semantic "graph neural networks" --limit 5 --json
|
|
198
|
-
scholarinboxcli interactions --type all --json
|
|
199
|
-
scholarinboxcli bookmark list --json
|
|
200
|
-
scholarinboxcli bookmark add 3302478 --json
|
|
201
|
-
scholarinboxcli bookmark remove 3302478 --json
|
|
202
|
-
scholarinboxcli collection list --json
|
|
203
|
-
scholarinboxcli collection list --expanded --json
|
|
204
|
-
scholarinboxcli collection papers "AIAgents" --json
|
|
205
|
-
scholarinboxcli collection similar "AIAgents" --json
|
|
206
|
-
scholarinboxcli conference list --json
|
|
207
|
-
scholarinboxcli conference explore --json
|
|
208
|
-
```
|
|
209
|
-
|
|
210
195
|
## Notes
|
|
211
196
|
|
|
212
197
|
- Some collection mutations (create/rename/delete/add/remove) rely on best-effort endpoints that may change on the service side. If a mutation fails, try again or use the web UI to validate the current behavior.
|
|
198
|
+
- Bookmarks are stored as a dedicated collection named "Bookmarks" in the web app; `bookmark list` pulls that collection via `/api/get_collections`.
|
|
213
199
|
- Similar papers for collections uses the server endpoint used by the web UI. Results typically appear under `digest_df` in JSON responses.
|
|
214
200
|
|
|
215
|
-
##
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
# 1) Build sdist + wheel
|
|
219
|
-
uv run --with build python -m build
|
|
220
|
-
|
|
221
|
-
# 2) Validate metadata/rendering
|
|
222
|
-
uvx twine check dist/*
|
|
223
|
-
|
|
224
|
-
# 3) (Optional) test publish first
|
|
225
|
-
uvx twine upload --repository testpypi dist/*
|
|
201
|
+
## License
|
|
226
202
|
|
|
227
|
-
|
|
228
|
-
uvx twine upload dist/*
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
If using an API token:
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
export TWINE_USERNAME=__token__
|
|
235
|
-
export TWINE_PASSWORD=<your-pypi-token>
|
|
236
|
-
```
|
|
203
|
+
MIT. See `LICENSE`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# scholarinboxcli
|
|
2
2
|
|
|
3
|
-
CLI for Scholar Inbox, for humans and agents alike.
|
|
3
|
+
CLI for [Scholar Inbox](https://scholar-inbox.com), for humans and agents alike.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -100,6 +100,12 @@ scholarinboxcli collection papers 10759
|
|
|
100
100
|
# Similar papers for one or more collections
|
|
101
101
|
scholarinboxcli collection similar 10759 12345
|
|
102
102
|
|
|
103
|
+
# Optional local sorting for display (e.g., newest first)
|
|
104
|
+
scholarinboxcli collection similar "AIAgents" --sort year
|
|
105
|
+
|
|
106
|
+
# Sort ascending instead
|
|
107
|
+
scholarinboxcli collection similar "AIAgents" --sort year --asc
|
|
108
|
+
|
|
103
109
|
# You can also use collection names (case-insensitive). The CLI will
|
|
104
110
|
# automatically fetch collection ID mappings from the API when needed.
|
|
105
111
|
scholarinboxcli collection papers "AIAgents"
|
|
@@ -107,6 +113,7 @@ scholarinboxcli collection similar "AIAgents" "Benchmark"
|
|
|
107
113
|
```
|
|
108
114
|
|
|
109
115
|
Collection name matching is exact → prefix → contains. If multiple matches exist, the CLI reports ambiguity and shows candidate IDs.
|
|
116
|
+
`collection similar` supports client-side sorting with `--sort year|title` and optional `--asc`.
|
|
110
117
|
|
|
111
118
|
## Search
|
|
112
119
|
|
|
@@ -163,53 +170,12 @@ scholarinboxcli collection papers "AIAgents" --json
|
|
|
163
170
|
scholarinboxcli search "diffusion" --json
|
|
164
171
|
```
|
|
165
172
|
|
|
166
|
-
## Tested (2026-02-01)
|
|
167
|
-
|
|
168
|
-
The following commands were exercised against the live API (with a valid magic-link login) to confirm behavior:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
scholarinboxcli --help
|
|
172
|
-
scholarinboxcli auth status --json
|
|
173
|
-
scholarinboxcli digest --date 01-30-2026 --json
|
|
174
|
-
scholarinboxcli trending --category ALL --days 7 --json
|
|
175
|
-
scholarinboxcli search "transformers" --limit 5 --json
|
|
176
|
-
scholarinboxcli semantic "graph neural networks" --limit 5 --json
|
|
177
|
-
scholarinboxcli interactions --type all --json
|
|
178
|
-
scholarinboxcli bookmark list --json
|
|
179
|
-
scholarinboxcli bookmark add 3302478 --json
|
|
180
|
-
scholarinboxcli bookmark remove 3302478 --json
|
|
181
|
-
scholarinboxcli collection list --json
|
|
182
|
-
scholarinboxcli collection list --expanded --json
|
|
183
|
-
scholarinboxcli collection papers "AIAgents" --json
|
|
184
|
-
scholarinboxcli collection similar "AIAgents" --json
|
|
185
|
-
scholarinboxcli conference list --json
|
|
186
|
-
scholarinboxcli conference explore --json
|
|
187
|
-
```
|
|
188
|
-
|
|
189
173
|
## Notes
|
|
190
174
|
|
|
191
175
|
- Some collection mutations (create/rename/delete/add/remove) rely on best-effort endpoints that may change on the service side. If a mutation fails, try again or use the web UI to validate the current behavior.
|
|
176
|
+
- Bookmarks are stored as a dedicated collection named "Bookmarks" in the web app; `bookmark list` pulls that collection via `/api/get_collections`.
|
|
192
177
|
- Similar papers for collections uses the server endpoint used by the web UI. Results typically appear under `digest_df` in JSON responses.
|
|
193
178
|
|
|
194
|
-
##
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
# 1) Build sdist + wheel
|
|
198
|
-
uv run --with build python -m build
|
|
199
|
-
|
|
200
|
-
# 2) Validate metadata/rendering
|
|
201
|
-
uvx twine check dist/*
|
|
202
|
-
|
|
203
|
-
# 3) (Optional) test publish first
|
|
204
|
-
uvx twine upload --repository testpypi dist/*
|
|
179
|
+
## License
|
|
205
180
|
|
|
206
|
-
|
|
207
|
-
uvx twine upload dist/*
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
If using an API token:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
export TWINE_USERNAME=__token__
|
|
214
|
-
export TWINE_PASSWORD=<your-pypi-token>
|
|
215
|
-
```
|
|
181
|
+
MIT. See `LICENSE`.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scholarinboxcli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "CLI for Scholar Inbox (authenticated web API)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,3 +36,9 @@ packages = ["src/scholarinboxcli"]
|
|
|
36
36
|
[tool.pytest.ini_options]
|
|
37
37
|
testpaths = ["tests"]
|
|
38
38
|
pythonpath = ["src"]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=9.0.2",
|
|
43
|
+
"ruff>=0.15.0",
|
|
44
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
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
|
|
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)
|