scholarinboxcli 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ """Collection name/ID resolution helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from scholarinboxcli.api.client import ApiError, ScholarInboxClient
6
+
7
+
8
+ def normalize_name(name: str) -> str:
9
+ return name.strip().lower()
10
+
11
+
12
+ def collection_candidates(items: object) -> list[tuple[str, str]]:
13
+ if not isinstance(items, list):
14
+ return []
15
+ candidates: list[tuple[str, str]] = []
16
+ for item in items:
17
+ if isinstance(item, dict):
18
+ name = item.get("name") or item.get("collection_name") or ""
19
+ cid = str(item.get("id") or item.get("collection_id") or "")
20
+ elif isinstance(item, str):
21
+ name = item
22
+ cid = ""
23
+ else:
24
+ continue
25
+ if name:
26
+ candidates.append((name, cid))
27
+ return candidates
28
+
29
+
30
+ def collection_items_from_response(data: object) -> object:
31
+ if isinstance(data, dict):
32
+ for key in ("collections", "expanded_collections", "collection_names"):
33
+ if key in data:
34
+ return data.get(key)
35
+ return data
36
+ return data
37
+
38
+
39
+ def collection_candidates_from_map(data: object) -> list[tuple[str, str]]:
40
+ if not isinstance(data, dict):
41
+ return []
42
+ mapping = data.get("collection_names_to_ids_dict")
43
+ if not isinstance(mapping, dict):
44
+ return []
45
+ return [(str(name), str(cid)) for name, cid in mapping.items() if name and cid is not None]
46
+
47
+
48
+ def candidates_have_ids(candidates: list[tuple[str, str]]) -> bool:
49
+ return any(cid for _, cid in candidates)
50
+
51
+
52
+ def match_collection_name(candidates: list[tuple[str, str]], identifier: str) -> str | None:
53
+ target = normalize_name(identifier)
54
+ names = [(name, cid) for name, cid in candidates if name]
55
+
56
+ for name, _ in names:
57
+ if normalize_name(name) == target:
58
+ return name
59
+
60
+ prefix = [c for c in names if normalize_name(c[0]).startswith(target)]
61
+ if len(prefix) == 1:
62
+ return prefix[0][0]
63
+ if len(prefix) > 1:
64
+ names_str = ", ".join([n for n, _ in prefix[:10]])
65
+ raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
66
+
67
+ contains = [c for c in names if target in normalize_name(c[0])]
68
+ if len(contains) == 1:
69
+ return contains[0][0]
70
+ if len(contains) > 1:
71
+ names_str = ", ".join([n for n, _ in contains[:10]])
72
+ raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
73
+
74
+ return None
75
+
76
+
77
+ def resolve_collection_id(client: ScholarInboxClient, identifier: str) -> str:
78
+ """Resolve numeric IDs directly, otherwise match by collection name."""
79
+ if identifier.isdigit():
80
+ return identifier
81
+
82
+ data = client.collections_list()
83
+ items = collection_items_from_response(data)
84
+ candidates = collection_candidates(items)
85
+
86
+ if not candidates_have_ids(candidates):
87
+ try:
88
+ data = client.collections_expanded()
89
+ items = collection_items_from_response(data)
90
+ candidates = collection_candidates(items)
91
+ except ApiError:
92
+ pass
93
+
94
+ if not candidates_have_ids(candidates):
95
+ try:
96
+ data = client.collections_map()
97
+ mapped = collection_candidates_from_map(data)
98
+ if mapped:
99
+ candidates = mapped
100
+ except ApiError:
101
+ pass
102
+
103
+ if not candidates_have_ids(candidates):
104
+ matched = match_collection_name(candidates, identifier)
105
+ if matched:
106
+ return matched
107
+ raise ApiError("Unable to resolve collection name (no IDs available)")
108
+
109
+ candidates = [(name, cid) for name, cid in candidates if cid]
110
+ target = normalize_name(identifier)
111
+
112
+ for name, cid in candidates:
113
+ if normalize_name(name) == target:
114
+ return cid
115
+
116
+ prefix = [c for c in candidates if normalize_name(c[0]).startswith(target)]
117
+ if len(prefix) == 1:
118
+ return prefix[0][1]
119
+ if len(prefix) > 1:
120
+ names = ", ".join([f"{n}({cid})" for n, cid in prefix[:10]])
121
+ raise ApiError(f"Ambiguous collection name. Matches: {names}")
122
+
123
+ contains = [c for c in candidates if target in normalize_name(c[0])]
124
+ if len(contains) == 1:
125
+ return contains[0][1]
126
+ if len(contains) > 1:
127
+ names = ", ".join([f"{n}({cid})" for n, cid in contains[:10]])
128
+ raise ApiError(f"Ambiguous collection name. Matches: {names}")
129
+
130
+ raise ApiError("Collection name not found")
@@ -0,0 +1,54 @@
1
+ """Helpers for sorting paper-like API responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ def _year_from_paper(paper: dict[str, Any]) -> int:
10
+ year = paper.get("year") or paper.get("publication_year") or paper.get("conference_year")
11
+ if isinstance(year, int):
12
+ return year
13
+ if isinstance(year, float):
14
+ return int(year)
15
+ if isinstance(year, str) and year.isdigit():
16
+ return int(year)
17
+
18
+ publication_date = paper.get("publication_date")
19
+ if isinstance(publication_date, str) and len(publication_date) >= 4 and publication_date[:4].isdigit():
20
+ return int(publication_date[:4])
21
+ if isinstance(publication_date, (int, float)):
22
+ ts = float(publication_date)
23
+ if ts > 10_000_000_000:
24
+ ts /= 1000.0
25
+ try:
26
+ return datetime.fromtimestamp(ts, tz=timezone.utc).year
27
+ except Exception:
28
+ return 0
29
+ return 0
30
+
31
+
32
+ def sort_paper_response(data: Any, sort_by: str | None, asc: bool) -> Any:
33
+ """Return a sorted copy of known paper-list structures."""
34
+ if not sort_by:
35
+ return data
36
+ if not isinstance(data, dict):
37
+ return data
38
+
39
+ for key in ("digest_df", "papers", "results", "items", "data"):
40
+ rows = data.get(key)
41
+ if not isinstance(rows, list) or not rows or not isinstance(rows[0], dict):
42
+ continue
43
+
44
+ if sort_by == "year":
45
+ sorted_rows = sorted(rows, key=_year_from_paper, reverse=not asc)
46
+ elif sort_by == "title":
47
+ sorted_rows = sorted(rows, key=lambda p: str(p.get("title") or p.get("paper_title") or "").lower(), reverse=not asc)
48
+ else:
49
+ return data
50
+
51
+ out = dict(data)
52
+ out[key] = sorted_rows
53
+ return out
54
+ return data
@@ -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`.
@@ -0,0 +1,23 @@
1
+ scholarinboxcli/__init__.py,sha256=YvuYzWnKtqBb-IqG8HAu-nhIYAsgj9Vmc_b9o7vO-js,22
2
+ scholarinboxcli/cli.py,sha256=BPXTPhBVO6mDoihXfDIp6zmbTqIcMJ8-VqKiF616DrI,1015
3
+ scholarinboxcli/config.py,sha256=cxp1RzNwzT6Iu225EPvs8NhH2YTTMg9fQBjaYIRVDoc,1545
4
+ scholarinboxcli/api/client.py,sha256=pEUMcoTauEmDdB7weV5m82-yyjUpdcvbRe8oY9Rj8Qo,15142
5
+ scholarinboxcli/api/endpoints.py,sha256=lwSk3PAOITGurTliP9Ko8KSP-DFR8WqKrRxC760rpPc,1515
6
+ scholarinboxcli/commands/__init__.py,sha256=aOPGu7M21finmICNxDSxNBpe1C3Vw2Iimh1UBESfekU,42
7
+ scholarinboxcli/commands/auth.py,sha256=6iwFsuAqiwiRTJbaJFgEt4JXRo1HVrLEF3WSsMo3GHA,1024
8
+ scholarinboxcli/commands/bookmarks.py,sha256=UbR99VrrCyCdu1EhyHIgwn3sPOYKfRc4jF4gUx1fU6w,1598
9
+ scholarinboxcli/commands/collections.py,sha256=pBZFXi_e3Dto-4E2GS6pgIrcVMzWpWaGLNTLB5My5sQ,5634
10
+ scholarinboxcli/commands/common.py,sha256=5EcH0RDuX8DKzyB6lrPqGEok5TKx8SSV37mXczD9dqI,1654
11
+ scholarinboxcli/commands/conferences.py,sha256=e1QjX9R9WZhkSkPXKgoxuRPbl612qEVbsjX90zEgKSw,1183
12
+ scholarinboxcli/commands/papers.py,sha256=mdb3-_iecy_pjavPL_K0cXfVnDsQMU93SbD8T3hJY10,3968
13
+ scholarinboxcli/formatters/domain_tables.py,sha256=9RZcq-k9YycO64FvQAblE1dzYmytSj7NBasBEZeDW9I,4702
14
+ scholarinboxcli/formatters/json_fmt.py,sha256=Ntcp4EqHugCXg79RIF62c7QHa-lexptLDDTT3IEP65U,197
15
+ scholarinboxcli/formatters/table.py,sha256=G0ehvuGgo77XX5kw_9u5DEf8oUTIR-fOJ5rhYQvMEkc,4587
16
+ scholarinboxcli/services/__init__.py,sha256=i-8EAvmxCDyYEjmfILYRcUHUh7ryY5hFS4PZARScFXo,56
17
+ scholarinboxcli/services/collections.py,sha256=rz9rqgf-nfzBl-scBhweA-22--Z70qKEyBhW84os4bY,4353
18
+ scholarinboxcli/services/paper_sort.py,sha256=HYoIHry14QIUsfd9D9kZvy7phVhvCnwEjKHv6zO-0IU,1809
19
+ scholarinboxcli-0.1.2.dist-info/METADATA,sha256=oqmUh6jnAapWYtUA13efz3k4r1Q4kQutPP_nGEOzS00,5328
20
+ scholarinboxcli-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
+ scholarinboxcli-0.1.2.dist-info/entry_points.txt,sha256=iescoEMF_CPwSNSmvlzNDl5pT2VpBL9_1bIq_FFIAKc,60
22
+ scholarinboxcli-0.1.2.dist-info/licenses/LICENSE,sha256=sP1DPhQGvTFx1mH4JhNuczMMbApQgFvhBcVjZKM79gU,1068
23
+ scholarinboxcli-0.1.2.dist-info/RECORD,,
@@ -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,10 +0,0 @@
1
- scholarinboxcli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
- scholarinboxcli/cli.py,sha256=bugBfT66k5EuVPashcG6Pz0IRM0KtRz_c6lNOalm4pk,19242
3
- scholarinboxcli/config.py,sha256=cxp1RzNwzT6Iu225EPvs8NhH2YTTMg9fQBjaYIRVDoc,1545
4
- scholarinboxcli/api/client.py,sha256=TE8pIRXh7pHhQGto9CvRpPPT61SJp9SzNhRnQ-2U4M4,13723
5
- scholarinboxcli/formatters/json_fmt.py,sha256=Ntcp4EqHugCXg79RIF62c7QHa-lexptLDDTT3IEP65U,197
6
- scholarinboxcli/formatters/table.py,sha256=GnzpmSJ7M_yq-R-c8no8SE9vXbycvWWPUu6hV4tcJAA,2133
7
- scholarinboxcli-0.1.0.dist-info/METADATA,sha256=JZISdlP49Ezyh-UoLv2FlJIrliHUvZrnj0IvFNWKCMw,6062
8
- scholarinboxcli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- scholarinboxcli-0.1.0.dist-info/entry_points.txt,sha256=iescoEMF_CPwSNSmvlzNDl5pT2VpBL9_1bIq_FFIAKc,60
10
- scholarinboxcli-0.1.0.dist-info/RECORD,,