scholarinboxcli 0.1.1__tar.gz → 0.1.3__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 → scholarinboxcli-0.1.3}/CHANGELOG.md +23 -1
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/PKG-INFO +13 -53
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/README.md +11 -51
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/pyproject.toml +8 -2
- scholarinboxcli-0.1.3/src/scholarinboxcli/__init__.py +1 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/api/client.py +67 -14
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/api/endpoints.py +1 -1
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/cli.py +1 -1
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/auth.py +2 -1
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/bookmarks.py +2 -1
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/collections.py +7 -2
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/common.py +8 -2
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/conferences.py +3 -2
- scholarinboxcli-0.1.3/src/scholarinboxcli/formatters/domain_tables.py +122 -0
- scholarinboxcli-0.1.3/src/scholarinboxcli/formatters/table.py +134 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/services/collections.py +0 -2
- scholarinboxcli-0.1.3/src/scholarinboxcli/services/paper_sort.py +54 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/tests/test_api_client_mocked.py +23 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/tests/test_cli_smoke.py +2 -1
- scholarinboxcli-0.1.3/tests/test_domain_tables.py +60 -0
- scholarinboxcli-0.1.3/tests/test_paper_sort.py +27 -0
- scholarinboxcli-0.1.3/tests/test_table_formatter.py +64 -0
- scholarinboxcli-0.1.3/uv.lock +334 -0
- scholarinboxcli-0.1.1/src/scholarinboxcli/__init__.py +0 -1
- scholarinboxcli-0.1.1/src/scholarinboxcli/formatters/table.py +0 -66
- scholarinboxcli-0.1.1/uv.lock +0 -198
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/.github/workflows/ci.yml +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/.github/workflows/publish.yml +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/.gitignore +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/LICENSE +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/__init__.py +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/commands/papers.py +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/config.py +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/formatters/json_fmt.py +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/src/scholarinboxcli/services/__init__.py +0 -0
- {scholarinboxcli-0.1.1 → scholarinboxcli-0.1.3}/tests/test_collection_resolution.py +0 -0
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.3] - 2026-02-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Updated project tagline to emphasize humans + agents use case.
|
|
15
|
+
|
|
16
|
+
## [0.1.2] - 2026-02-04
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Added collection-aware paper formatting for bookmark and collection views.
|
|
21
|
+
- Added tests for bookmark collection lookup and collection paper formatting.
|
|
22
|
+
- Added dev tooling dependencies for pytest and ruff.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Bookmarks now resolve the "Bookmarks" collection and load papers via `/api/get_collections`.
|
|
27
|
+
- Collection paper retrieval prefers `/api/get_collections` with fallback to legacy endpoints.
|
|
28
|
+
- Cleaned up minor lint issues and restored `_resolve_collection_id` export for tests.
|
|
29
|
+
|
|
10
30
|
## [0.1.1] - 2026-02-01
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -54,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
74
|
|
|
55
75
|
- Tracked Python bytecode artifacts (`.pyc`) removed from repository history and ignored via `.gitignore`.
|
|
56
76
|
|
|
57
|
-
[Unreleased]: https://github.com/mrshu/scholarinboxcli/compare/v0.1.
|
|
77
|
+
[Unreleased]: https://github.com/mrshu/scholarinboxcli/compare/v0.1.3...HEAD
|
|
78
|
+
[0.1.3]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.3
|
|
79
|
+
[0.1.2]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.2
|
|
58
80
|
[0.1.1]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.1
|
|
59
81
|
[0.1.0]: https://github.com/mrshu/scholarinboxcli/releases/tag/v0.1.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarinboxcli
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: CLI for
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Scholar Inbox CLI for digests, search, collections, and bookmarks, for humans and agents alike
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: bibliography,cli,research,scholar
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
|
|
23
23
|
# scholarinboxcli
|
|
24
24
|
|
|
25
|
-
CLI
|
|
25
|
+
Scholar Inbox CLI: digests, search, collections, and bookmarks for humans and agents alike.
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
|
@@ -122,6 +122,12 @@ scholarinboxcli collection papers 10759
|
|
|
122
122
|
# Similar papers for one or more collections
|
|
123
123
|
scholarinboxcli collection similar 10759 12345
|
|
124
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
|
+
|
|
125
131
|
# You can also use collection names (case-insensitive). The CLI will
|
|
126
132
|
# automatically fetch collection ID mappings from the API when needed.
|
|
127
133
|
scholarinboxcli collection papers "AIAgents"
|
|
@@ -129,6 +135,7 @@ scholarinboxcli collection similar "AIAgents" "Benchmark"
|
|
|
129
135
|
```
|
|
130
136
|
|
|
131
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`.
|
|
132
139
|
|
|
133
140
|
## Search
|
|
134
141
|
|
|
@@ -185,59 +192,12 @@ scholarinboxcli collection papers "AIAgents" --json
|
|
|
185
192
|
scholarinboxcli search "diffusion" --json
|
|
186
193
|
```
|
|
187
194
|
|
|
188
|
-
## Tested (2026-02-01)
|
|
189
|
-
|
|
190
|
-
The following commands were exercised against the live API (with a valid magic-link login) to confirm behavior:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
scholarinboxcli --help
|
|
194
|
-
scholarinboxcli auth status --json
|
|
195
|
-
scholarinboxcli digest --date 01-30-2026 --json
|
|
196
|
-
scholarinboxcli trending --category ALL --days 7 --json
|
|
197
|
-
scholarinboxcli search "transformers" --limit 5 --json
|
|
198
|
-
scholarinboxcli semantic "graph neural networks" --limit 5 --json
|
|
199
|
-
scholarinboxcli interactions --type all --json
|
|
200
|
-
scholarinboxcli bookmark list --json
|
|
201
|
-
scholarinboxcli bookmark add 3302478 --json
|
|
202
|
-
scholarinboxcli bookmark remove 3302478 --json
|
|
203
|
-
scholarinboxcli collection list --json
|
|
204
|
-
scholarinboxcli collection list --expanded --json
|
|
205
|
-
scholarinboxcli collection papers "AIAgents" --json
|
|
206
|
-
scholarinboxcli collection similar "AIAgents" --json
|
|
207
|
-
scholarinboxcli conference list --json
|
|
208
|
-
scholarinboxcli conference explore --json
|
|
209
|
-
```
|
|
210
|
-
|
|
211
195
|
## Notes
|
|
212
196
|
|
|
213
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`.
|
|
214
199
|
- Similar papers for collections uses the server endpoint used by the web UI. Results typically appear under `digest_df` in JSON responses.
|
|
215
200
|
|
|
216
|
-
##
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
# 1) Build sdist + wheel
|
|
220
|
-
uv run --with build python -m build
|
|
221
|
-
|
|
222
|
-
# 2) Validate metadata/rendering
|
|
223
|
-
uvx twine check dist/*
|
|
224
|
-
|
|
225
|
-
# 3) (Optional) test publish first
|
|
226
|
-
uvx twine upload --repository testpypi dist/*
|
|
227
|
-
|
|
228
|
-
# 4) Publish to PyPI
|
|
229
|
-
uvx twine upload dist/*
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
If using an API token:
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
export TWINE_USERNAME=__token__
|
|
236
|
-
export TWINE_PASSWORD=<your-pypi-token>
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
Automated publish is also configured via GitHub Actions:
|
|
201
|
+
## License
|
|
240
202
|
|
|
241
|
-
|
|
242
|
-
- Trigger: push a tag matching `v*` (for example `v0.1.1`)
|
|
243
|
-
- Auth: PyPI Trusted Publishing (OIDC)
|
|
203
|
+
MIT. See `LICENSE`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# scholarinboxcli
|
|
2
2
|
|
|
3
|
-
CLI
|
|
3
|
+
Scholar Inbox CLI: digests, search, collections, and bookmarks 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,59 +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/*
|
|
205
|
-
|
|
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
|
-
```
|
|
216
|
-
|
|
217
|
-
Automated publish is also configured via GitHub Actions:
|
|
179
|
+
## License
|
|
218
180
|
|
|
219
|
-
|
|
220
|
-
- Trigger: push a tag matching `v*` (for example `v0.1.1`)
|
|
221
|
-
- Auth: PyPI Trusted Publishing (OIDC)
|
|
181
|
+
MIT. See `LICENSE`.
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scholarinboxcli"
|
|
7
|
-
version = "0.1.
|
|
8
|
-
description = "CLI for
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Scholar Inbox CLI for digests, search, collections, and bookmarks, for humans and agents alike"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
@@ -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.3"
|
|
@@ -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,7 @@ 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
|
|
16
15
|
from scholarinboxcli.api import endpoints as ep
|
|
17
16
|
|
|
18
17
|
|
|
@@ -82,6 +81,34 @@ def _is_paper_list(data: Any) -> bool:
|
|
|
82
81
|
return False
|
|
83
82
|
|
|
84
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
|
+
|
|
85
112
|
class ScholarInboxClient:
|
|
86
113
|
def __init__(self, api_base: str | None = None, no_retry: bool = False):
|
|
87
114
|
self.no_retry = no_retry
|
|
@@ -142,13 +169,13 @@ class ScholarInboxClient:
|
|
|
142
169
|
|
|
143
170
|
def _post_first(self, endpoints: list[str], payload: dict[str, Any]) -> Any:
|
|
144
171
|
last_error: ApiError | None = None
|
|
145
|
-
for
|
|
172
|
+
for endpoint in endpoints:
|
|
146
173
|
try:
|
|
147
|
-
return self._request("POST",
|
|
174
|
+
return self._request("POST", endpoint, json=payload)
|
|
148
175
|
except ApiError as e:
|
|
149
176
|
last_error = e
|
|
150
177
|
try:
|
|
151
|
-
return self._request("POST",
|
|
178
|
+
return self._request("POST", endpoint, data=payload)
|
|
152
179
|
except ApiError as e:
|
|
153
180
|
last_error = e
|
|
154
181
|
if last_error:
|
|
@@ -199,7 +226,23 @@ class ScholarInboxClient:
|
|
|
199
226
|
)
|
|
200
227
|
|
|
201
228
|
def bookmarks(self) -> Any:
|
|
202
|
-
|
|
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])
|
|
203
246
|
|
|
204
247
|
def bookmark_add(self, paper_id: str) -> Any:
|
|
205
248
|
payload = {"bookmarked": True, "id": paper_id}
|
|
@@ -227,6 +270,13 @@ class ScholarInboxClient:
|
|
|
227
270
|
def collections_map(self) -> Any:
|
|
228
271
|
return self._request("GET", ep.COLLECTIONS_FALLBACK)
|
|
229
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)
|
|
279
|
+
|
|
230
280
|
def collection_create(self, name: str) -> Any:
|
|
231
281
|
payload = {"name": name, "collection_name": name}
|
|
232
282
|
return self._post_first(list(ep.COLLECTION_CREATE_CANDIDATES), payload)
|
|
@@ -253,16 +303,19 @@ class ScholarInboxClient:
|
|
|
253
303
|
return self._post_first(list(ep.COLLECTION_REMOVE_PAPER_CANDIDATES), payload)
|
|
254
304
|
|
|
255
305
|
def collection_papers(self, collection_id: str, limit: int | None = None, offset: int | None = None) -> Any:
|
|
256
|
-
params: dict[str, Any] = {"collection_id": collection_id}
|
|
257
|
-
if limit is not None:
|
|
258
|
-
params["limit"] = limit
|
|
259
|
-
if offset is not None:
|
|
260
|
-
params["offset"] = offset
|
|
261
306
|
try:
|
|
262
|
-
return self.
|
|
307
|
+
return self.collections_get([collection_id])
|
|
263
308
|
except ApiError:
|
|
264
|
-
|
|
265
|
-
|
|
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})
|
|
266
319
|
|
|
267
320
|
def collections_similar(self, collection_ids: list[str], limit: int | None = None, offset: int | None = None) -> Any:
|
|
268
321
|
schemas = [
|
|
@@ -14,13 +14,13 @@ SEMANTIC_SEARCH = "/api/semantic-search"
|
|
|
14
14
|
INTERACTIONS = "/api/interactions"
|
|
15
15
|
|
|
16
16
|
# Bookmarks
|
|
17
|
-
BOOKMARKS = "/api/bookmarks"
|
|
18
17
|
BOOKMARK_PAPER = "/api/bookmark_paper/"
|
|
19
18
|
|
|
20
19
|
# Collections
|
|
21
20
|
COLLECTIONS_PRIMARY = "/api/get_all_user_collections"
|
|
22
21
|
COLLECTIONS_FALLBACK = "/api/collections"
|
|
23
22
|
COLLECTIONS_EXPANDED = "/api/get_expanded_collections"
|
|
23
|
+
COLLECTIONS_GET = "/api/get_collections"
|
|
24
24
|
COLLECTION_CREATE_CANDIDATES = (
|
|
25
25
|
"/api/create_collection/",
|
|
26
26
|
"/api/collections",
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from scholarinboxcli.commands import auth, bookmarks, collections, conferences, papers
|
|
8
|
-
from scholarinboxcli.services.collections import resolve_collection_id as _resolve_collection_id
|
|
8
|
+
from scholarinboxcli.services.collections import resolve_collection_id as _resolve_collection_id # noqa: F401
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_auth_status
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(help="Authentication commands", no_args_is_help=True)
|
|
@@ -25,7 +26,7 @@ def auth_login(
|
|
|
25
26
|
def auth_status(json_output: bool = typer.Option(False, "--json", help="Output as JSON")):
|
|
26
27
|
def action(client):
|
|
27
28
|
data = client.session_info()
|
|
28
|
-
print_output(data, json_output, title="Session")
|
|
29
|
+
print_output(data, json_output, title="Session", table_formatter=format_auth_status)
|
|
29
30
|
|
|
30
31
|
with_client(False, action)
|
|
31
32
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_collection_papers
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(help="Bookmark commands", no_args_is_help=True)
|
|
@@ -17,7 +18,7 @@ def bookmark_list(
|
|
|
17
18
|
):
|
|
18
19
|
def action(client):
|
|
19
20
|
data = client.bookmarks()
|
|
20
|
-
print_output(data, json_output, title="Bookmarks")
|
|
21
|
+
print_output(data, json_output, title="Bookmarks", table_formatter=format_collection_papers)
|
|
21
22
|
|
|
22
23
|
with_client(no_retry, action)
|
|
23
24
|
|
|
@@ -7,7 +7,9 @@ from typing import Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from scholarinboxcli.commands.common import print_output, with_client
|
|
10
|
+
from scholarinboxcli.formatters.domain_tables import format_collection_list, format_collection_papers
|
|
10
11
|
from scholarinboxcli.services.collections import resolve_collection_id
|
|
12
|
+
from scholarinboxcli.services.paper_sort import sort_paper_response
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
app = typer.Typer(help="Collection commands", no_args_is_help=True)
|
|
@@ -21,7 +23,7 @@ def collection_list(
|
|
|
21
23
|
):
|
|
22
24
|
def action(client):
|
|
23
25
|
data = client.collections_expanded() if expanded else client.collections_list()
|
|
24
|
-
print_output(data, json_output, title="Collections")
|
|
26
|
+
print_output(data, json_output, title="Collections", table_formatter=format_collection_list)
|
|
25
27
|
|
|
26
28
|
with_client(no_retry, action)
|
|
27
29
|
|
|
@@ -109,7 +111,7 @@ def collection_papers(
|
|
|
109
111
|
def action(client):
|
|
110
112
|
cid = resolve_collection_id(client, collection_id)
|
|
111
113
|
data = client.collection_papers(cid, limit=limit, offset=offset)
|
|
112
|
-
print_output(data, json_output, title=f"Collection {cid}")
|
|
114
|
+
print_output(data, json_output, title=f"Collection {cid}", table_formatter=format_collection_papers)
|
|
113
115
|
|
|
114
116
|
with_client(no_retry, action)
|
|
115
117
|
|
|
@@ -119,12 +121,15 @@ def collection_similar(
|
|
|
119
121
|
collection_ids: list[str] = typer.Argument(..., help="Collection ID(s) or names"),
|
|
120
122
|
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
121
123
|
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
124
|
+
sort_by: Optional[str] = typer.Option(None, "--sort", help="Sort papers by: year, title"),
|
|
125
|
+
asc: bool = typer.Option(False, "--asc", help="Sort ascending (default is descending)"),
|
|
122
126
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
123
127
|
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
124
128
|
):
|
|
125
129
|
def action(client):
|
|
126
130
|
resolved = [resolve_collection_id(client, cid) for cid in collection_ids]
|
|
127
131
|
data = client.collections_similar(resolved, limit=limit, offset=offset)
|
|
132
|
+
data = sort_paper_response(data, sort_by, asc)
|
|
128
133
|
print_output(data, json_output, title="Similar Papers")
|
|
129
134
|
|
|
130
135
|
with_client(no_retry, action)
|
|
@@ -12,12 +12,18 @@ from scholarinboxcli.formatters.json_fmt import format_json
|
|
|
12
12
|
from scholarinboxcli.formatters.table import format_table
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def print_output(
|
|
15
|
+
def print_output(
|
|
16
|
+
data: Any,
|
|
17
|
+
use_json: bool,
|
|
18
|
+
title: str | None = None,
|
|
19
|
+
table_formatter: Callable[[Any, str | None], str] | None = None,
|
|
20
|
+
) -> None:
|
|
16
21
|
if use_json or not sys.stdout.isatty():
|
|
17
22
|
typer.echo(format_json(data))
|
|
18
23
|
return
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
formatter = table_formatter or format_table
|
|
26
|
+
table = formatter(data, title)
|
|
21
27
|
if table == "(no results)":
|
|
22
28
|
typer.echo(table)
|
|
23
29
|
return
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_conference_explore, format_conference_list
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(help="Conference commands", no_args_is_help=True)
|
|
@@ -17,7 +18,7 @@ def conference_list(
|
|
|
17
18
|
):
|
|
18
19
|
def action(client):
|
|
19
20
|
data = client.conference_list()
|
|
20
|
-
print_output(data, json_output, title="Conferences")
|
|
21
|
+
print_output(data, json_output, title="Conferences", table_formatter=format_conference_list)
|
|
21
22
|
|
|
22
23
|
with_client(no_retry, action)
|
|
23
24
|
|
|
@@ -29,6 +30,6 @@ def conference_explore(
|
|
|
29
30
|
):
|
|
30
31
|
def action(client):
|
|
31
32
|
data = client.conference_explorer()
|
|
32
|
-
print_output(data, json_output, title="Conference Explorer")
|
|
33
|
+
print_output(data, json_output, title="Conference Explorer", table_formatter=format_conference_explore)
|
|
33
34
|
|
|
34
35
|
with_client(no_retry, action)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Domain-specific table formatters for non-paper responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from scholarinboxcli.formatters.table import format_table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render(table: Table) -> str:
|
|
14
|
+
console = Console()
|
|
15
|
+
with console.capture() as capture:
|
|
16
|
+
console.print(table)
|
|
17
|
+
return capture.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_auth_status(data: Any, title: str | None = None) -> str:
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
return format_table(data, title)
|
|
23
|
+
table = Table(title=title)
|
|
24
|
+
table.add_column("Field", overflow="fold")
|
|
25
|
+
table.add_column("Value", overflow="fold")
|
|
26
|
+
for key, value in data.items():
|
|
27
|
+
table.add_row(str(key), str(value))
|
|
28
|
+
return _render(table)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_collection_list(data: Any, title: str | None = None) -> str:
|
|
32
|
+
if isinstance(data, list) and data and isinstance(data[0], str):
|
|
33
|
+
table = Table(title=title)
|
|
34
|
+
table.add_column("#", justify="right")
|
|
35
|
+
table.add_column("Name", overflow="fold")
|
|
36
|
+
for i, name in enumerate(data, start=1):
|
|
37
|
+
table.add_row(str(i), str(name))
|
|
38
|
+
return _render(table)
|
|
39
|
+
|
|
40
|
+
if isinstance(data, list) and data and isinstance(data[0], dict):
|
|
41
|
+
table = Table(title=title)
|
|
42
|
+
table.add_column("ID", overflow="fold")
|
|
43
|
+
table.add_column("Name", overflow="fold")
|
|
44
|
+
for item in data:
|
|
45
|
+
cid = item.get("id") or item.get("collection_id") or ""
|
|
46
|
+
name = item.get("name") or item.get("collection_name") or ""
|
|
47
|
+
table.add_row(str(cid), str(name))
|
|
48
|
+
return _render(table)
|
|
49
|
+
|
|
50
|
+
if isinstance(data, dict) and "expanded_collections" in data:
|
|
51
|
+
return format_collection_list(data.get("expanded_collections"), title)
|
|
52
|
+
|
|
53
|
+
return format_table(data, title)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_conference_list(data: Any, title: str | None = None) -> str:
|
|
57
|
+
rows = data.get("conferences") if isinstance(data, dict) else None
|
|
58
|
+
if isinstance(rows, list):
|
|
59
|
+
table = Table(title=title)
|
|
60
|
+
table.add_column("ID", justify="right")
|
|
61
|
+
table.add_column("Short")
|
|
62
|
+
table.add_column("Dates")
|
|
63
|
+
table.add_column("URL")
|
|
64
|
+
for row in rows:
|
|
65
|
+
cid = row.get("conference_id", "")
|
|
66
|
+
short = row.get("short_title") or row.get("full_title") or ""
|
|
67
|
+
start = row.get("start_date") or ""
|
|
68
|
+
end = row.get("end_date") or ""
|
|
69
|
+
dates = f"{start} -> {end}" if (start or end) else ""
|
|
70
|
+
url = row.get("conference_url") or ""
|
|
71
|
+
table.add_row(str(cid), str(short), str(dates), str(url))
|
|
72
|
+
return _render(table)
|
|
73
|
+
return format_table(data, title)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_conference_explore(data: Any, title: str | None = None) -> str:
|
|
77
|
+
rows = data.get("conf_data_list") if isinstance(data, dict) else None
|
|
78
|
+
if isinstance(rows, list):
|
|
79
|
+
table = Table(title=title)
|
|
80
|
+
table.add_column("Abbrev")
|
|
81
|
+
table.add_column("Conference")
|
|
82
|
+
table.add_column("Relevance", justify="right")
|
|
83
|
+
table.add_column("Years")
|
|
84
|
+
for row in rows:
|
|
85
|
+
abbrev = row.get("abbreviation") or ""
|
|
86
|
+
name = row.get("conference_name") or ""
|
|
87
|
+
rel = row.get("conf_relevance")
|
|
88
|
+
rel_str = f"{rel:.3f}" if isinstance(rel, (float, int)) else ""
|
|
89
|
+
years = row.get("list_of_years") or []
|
|
90
|
+
years_str = ", ".join(str(y) for y in years[:5])
|
|
91
|
+
table.add_row(str(abbrev), str(name), rel_str, years_str)
|
|
92
|
+
return _render(table)
|
|
93
|
+
return format_table(data, title)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_collection_papers(data: Any) -> list[dict[str, Any]]:
|
|
97
|
+
if isinstance(data, list):
|
|
98
|
+
return [item for item in data if isinstance(item, dict)]
|
|
99
|
+
if isinstance(data, dict):
|
|
100
|
+
for key in ("papers", "digest_df", "items", "results", "data"):
|
|
101
|
+
val = data.get(key)
|
|
102
|
+
if isinstance(val, list):
|
|
103
|
+
return [item for item in val if isinstance(item, dict)]
|
|
104
|
+
collections = data.get("collections")
|
|
105
|
+
if isinstance(collections, list):
|
|
106
|
+
papers: list[dict[str, Any]] = []
|
|
107
|
+
for collection in collections:
|
|
108
|
+
if isinstance(collection, dict):
|
|
109
|
+
for key in ("papers", "digest_df"):
|
|
110
|
+
val = collection.get(key)
|
|
111
|
+
if isinstance(val, list):
|
|
112
|
+
papers.extend([item for item in val if isinstance(item, dict)])
|
|
113
|
+
if papers:
|
|
114
|
+
return papers
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_collection_papers(data: Any, title: str | None = None) -> str:
|
|
119
|
+
papers = _extract_collection_papers(data)
|
|
120
|
+
if papers:
|
|
121
|
+
return format_table(papers, title)
|
|
122
|
+
return format_table(data, title)
|