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