scholarcli 1.26__tar.gz → 1.28__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.
- {scholarcli-1.26/src/scholarcli.egg-info → scholarcli-1.28}/PKG-INFO +63 -3
- {scholarcli-1.26 → scholarcli-1.28}/README.md +59 -2
- {scholarcli-1.26 → scholarcli-1.28}/pyproject.toml +7 -1
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/__init__.py +6 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/cache.py +41 -8
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/cli.py +392 -113
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/crossref.py +10 -18
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/enrich.py +12 -14
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/llm_review.py +165 -75
- scholarcli-1.28/src/scholar/network.py +226 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/notes.py +147 -30
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/pdf.py +162 -54
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/providers.py +589 -178
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/ratelimit.py +3 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/review.py +600 -144
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/scholar.py +94 -2
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/tui.py +110 -37
- scholarcli-1.28/src/scholar/tutorials/enrich-verify-export.md +162 -0
- scholarcli-1.28/src/scholar/tutorials/first-search.md +190 -0
- scholarcli-1.28/src/scholar/tutorials/llm-synthesis.md +316 -0
- scholarcli-1.28/src/scholar/tutorials/reviewing-in-the-tui.md +206 -0
- scholarcli-1.28/src/scholar/tutorials/scholar-with-an-agent.md +286 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/unpaywall.py +13 -18
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/utils.py +206 -0
- {scholarcli-1.26 → scholarcli-1.28/src/scholarcli.egg-info}/PKG-INFO +63 -3
- {scholarcli-1.26 → scholarcli-1.28}/src/scholarcli.egg-info/SOURCES.txt +7 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholarcli.egg-info/requires.txt +3 -0
- scholarcli-1.28/tests/test_cache.py +210 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_cli.py +593 -5
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_crossref.py +19 -2
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_enrich.py +12 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_llm_review.py +197 -20
- scholarcli-1.28/tests/test_network.py +193 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_notes.py +176 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_pdf.py +147 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_providers.py +796 -37
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_ratelimit.py +24 -1
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_review.py +708 -9
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_scholar.py +123 -1
- scholarcli-1.28/tests/test_tui.py +204 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_unpaywall.py +24 -3
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_utils.py +172 -0
- scholarcli-1.26/tests/test_cache.py +0 -98
- scholarcli-1.26/tests/test_tui.py +0 -31
- {scholarcli-1.26 → scholarcli-1.28}/LICENSE +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/setup.cfg +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/__main__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/preprint.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholar/questionary.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholarcli.egg-info/dependency_links.txt +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholarcli.egg-info/entry_points.txt +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/scholarcli.egg-info/top_level.txt +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/aggregator.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/arxiv.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/base.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/crossref.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/google_scholar.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/openalex.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/opencitations.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/apis/semantic_scholar.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/cli.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/exporters/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/exporters/bibtex.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/exporters/csv_exporter.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/exporters/tikz.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/filters/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/filters/filter_engine.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/models.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/paper_utils.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/parsers/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/parsers/pdf_parser.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/scoring/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/scoring/base.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/scoring/llm_scorer.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/scoring/tfidf_scorer.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/services.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/snowballing.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/storage/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/storage/json_storage.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/tui/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/tui/app.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/tui/dialogs.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/tui/setup.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/snowball/visualization.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/__init__.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/analysis.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/cli.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/clustering.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/database.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/grobid.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/logging.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/models.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/project.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/src/tuxedo/tui.py +0 -0
- {scholarcli-1.26 → scholarcli-1.28}/tests/test_preprint.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarcli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28
|
|
4
4
|
Summary: A tool for structured literature searches across bibliographic databases
|
|
5
5
|
Author-email: Daniel Bosk <dbosk@kth.se>, Ric Glassey <glassey@kth.se>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,6 +14,7 @@ Requires-Dist: pyalex>=0.19
|
|
|
14
14
|
Requires-Dist: arxiv>=2.1.0
|
|
15
15
|
Requires-Dist: cachetools>=6.2.4
|
|
16
16
|
Requires-Dist: platformdirs>=4.5.1
|
|
17
|
+
Requires-Dist: filelock>=3.12
|
|
17
18
|
Requires-Dist: textual>=6.11.0
|
|
18
19
|
Requires-Dist: pypandoc>=1.14
|
|
19
20
|
Requires-Dist: click>=8.0.0
|
|
@@ -36,6 +37,8 @@ Requires-Dist: pyyaml>=6.0
|
|
|
36
37
|
Requires-Dist: questionary>=2.0.0
|
|
37
38
|
Requires-Dist: llm-github-copilot>=0.3.1
|
|
38
39
|
Requires-Dist: llm-mistral<0.16,>=0.15
|
|
40
|
+
Requires-Dist: llm-openai-via-codex>=0.1a0
|
|
41
|
+
Requires-Dist: pytorial>=0.8.1
|
|
39
42
|
Dynamic: license-file
|
|
40
43
|
|
|
41
44
|
# Scholar
|
|
@@ -160,7 +163,8 @@ Search results are cached to avoid redundant API calls:
|
|
|
160
163
|
|
|
161
164
|
```bash
|
|
162
165
|
scholar cache info # Show cache statistics
|
|
163
|
-
scholar cache clear # Delete cached results
|
|
166
|
+
scholar cache clear # Delete cached results (keeps paid LLM output;
|
|
167
|
+
# add --include-llm to delete that too)
|
|
164
168
|
scholar cache path # Print cache directory
|
|
165
169
|
```
|
|
166
170
|
|
|
@@ -227,6 +231,30 @@ scholar search "machine learning privacy"
|
|
|
227
231
|
scholar rq "How do LLMs support novice programming?" --count 20
|
|
228
232
|
```
|
|
229
233
|
|
|
234
|
+
### Tutorials
|
|
235
|
+
|
|
236
|
+
Scholar ships interactive tutorials (built on
|
|
237
|
+
[pytorial](https://github.com/dbosk/pytorial)): each step opens a shell,
|
|
238
|
+
you type the command the step asks for, and the transcript is checked
|
|
239
|
+
before the lesson moves on.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
scholar tutorial list # the lessons and your progress
|
|
243
|
+
scholar tutorial run first-search # start or resume a lesson
|
|
244
|
+
scholar tutorial run --restart first-search
|
|
245
|
+
scholar tutorial review first-search # read back what you typed
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The lessons, in order: `first-search` (search, providers, output formats,
|
|
249
|
+
saving a session), `reviewing-in-the-tui` (recording decisions),
|
|
250
|
+
`enrich-verify-export` (abstracts and open access, retractions, CSV and
|
|
251
|
+
LaTeX exports), `llm-synthesis` (setting up the `llm` command, LLM
|
|
252
|
+
classification, a literature overview) and `scholar-with-an-agent` (what
|
|
253
|
+
an agent running the `backing-claims` skill does with Scholar, and how to
|
|
254
|
+
audit it). Every lesson runs in its own workspace with its own session
|
|
255
|
+
store, so practice sessions never appear in your real `scholar sessions
|
|
256
|
+
list`.
|
|
257
|
+
|
|
230
258
|
## Installation
|
|
231
259
|
|
|
232
260
|
If you don't use `pipx`, you can install with `pip`:
|
|
@@ -270,7 +298,7 @@ Some providers require API keys set as environment variables:
|
|
|
270
298
|
| SSRN, bioRxiv, medRxiv, ChemRxiv, Research Square, Preprints.org, IACR | `SCHOLAR_EMAIL` | No | Any email (they search via OpenAlex) |
|
|
271
299
|
| Web of Science | `WOS_EXPANDED_API_KEY` or `WOS_STARTER_API_KEY` | Yes | [developer.clarivate.com](https://developer.clarivate.com) |
|
|
272
300
|
| IEEE Xplore | `IEEE_API_KEY` | Yes | [developer.ieee.org](https://developer.ieee.org) |
|
|
273
|
-
| Scopus | `SCOPUS_API_KEY` | Yes | [dev.elsevier.com](https://dev.elsevier.com) |
|
|
301
|
+
| Scopus | `SCOPUS_API_KEY` | Yes | [dev.elsevier.com](https://dev.elsevier.com). Searching works from any network, but abstracts, keywords and full author lists come only with Scopus's COMPLETE view, which is bound to the institution's IP range (or `SCOPUS_INST_TOKEN`). scholar asks for it and falls back by itself (`scholar providers check` tells which view you get); `SCOPUS_VIEW=standard` or `complete` overrides. A 400 "Error translating query" on *every* query is an Elsevier-side refusal, not a query error: it has cleared by itself before, so retry later |
|
|
274
302
|
|
|
275
303
|
View provider status:
|
|
276
304
|
|
|
@@ -278,6 +306,38 @@ View provider status:
|
|
|
278
306
|
scholar providers
|
|
279
307
|
```
|
|
280
308
|
|
|
309
|
+
### Network: proxy and User-Agent
|
|
310
|
+
|
|
311
|
+
To reach subscriber-only content from off campus (Scopus's COMPLETE
|
|
312
|
+
view, a publisher that admits the institution's IP range), route
|
|
313
|
+
scholar's requests through a proxy on that network, for example an SSH
|
|
314
|
+
SOCKS tunnel:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
ssh -D 127.0.0.1:1081 -N you@login.your-university.example &
|
|
318
|
+
scholar --proxy socks5h://127.0.0.1:1081 search "..."
|
|
319
|
+
export SCHOLAR_PROXY=socks5h://127.0.0.1:1081 # or for every command
|
|
320
|
+
scholar providers check --show-ip # route, exit IP, health
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`--proxy URL` (a global option, before the command) wins over
|
|
324
|
+
`SCHOLAR_PROXY`; with neither, the standard `HTTP(S)_PROXY`/`ALL_PROXY`
|
|
325
|
+
variables apply as usual. Use `socks5h://` rather than `socks5://` so
|
|
326
|
+
host names are resolved by the proxy, on the far side of the tunnel.
|
|
327
|
+
SOCKS needs the PySocks package (`pipx inject scholarcli PySocks`);
|
|
328
|
+
scholar refuses a SOCKS proxy without it rather than failing every
|
|
329
|
+
request. `scholar providers check` always prints the route; `--show-ip`
|
|
330
|
+
also asks api.ipify.org for the exit address (a third-party request, so
|
|
331
|
+
only on demand).
|
|
332
|
+
|
|
333
|
+
Requests identify themselves as
|
|
334
|
+
`scholar/<version> (+https://github.com/dbosk/scholar)`. Your
|
|
335
|
+
`SCHOLAR_EMAIL` is not in it, so publishers never see the address; only
|
|
336
|
+
the services that ask for one (OpenAlex, Crossref, Unpaywall) get it,
|
|
337
|
+
through their own parameters. Set `SCHOLAR_USER_AGENT` to replace it. Scholar does not imitate a browser: a Cloudflare challenge
|
|
338
|
+
or a login page from a publisher means the PDF is not available to
|
|
339
|
+
scripts, and scholar stops there.
|
|
340
|
+
|
|
281
341
|
## Usage Examples
|
|
282
342
|
|
|
283
343
|
### Basic Search
|
|
@@ -120,7 +120,8 @@ Search results are cached to avoid redundant API calls:
|
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
122
|
scholar cache info # Show cache statistics
|
|
123
|
-
scholar cache clear # Delete cached results
|
|
123
|
+
scholar cache clear # Delete cached results (keeps paid LLM output;
|
|
124
|
+
# add --include-llm to delete that too)
|
|
124
125
|
scholar cache path # Print cache directory
|
|
125
126
|
```
|
|
126
127
|
|
|
@@ -187,6 +188,30 @@ scholar search "machine learning privacy"
|
|
|
187
188
|
scholar rq "How do LLMs support novice programming?" --count 20
|
|
188
189
|
```
|
|
189
190
|
|
|
191
|
+
### Tutorials
|
|
192
|
+
|
|
193
|
+
Scholar ships interactive tutorials (built on
|
|
194
|
+
[pytorial](https://github.com/dbosk/pytorial)): each step opens a shell,
|
|
195
|
+
you type the command the step asks for, and the transcript is checked
|
|
196
|
+
before the lesson moves on.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
scholar tutorial list # the lessons and your progress
|
|
200
|
+
scholar tutorial run first-search # start or resume a lesson
|
|
201
|
+
scholar tutorial run --restart first-search
|
|
202
|
+
scholar tutorial review first-search # read back what you typed
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The lessons, in order: `first-search` (search, providers, output formats,
|
|
206
|
+
saving a session), `reviewing-in-the-tui` (recording decisions),
|
|
207
|
+
`enrich-verify-export` (abstracts and open access, retractions, CSV and
|
|
208
|
+
LaTeX exports), `llm-synthesis` (setting up the `llm` command, LLM
|
|
209
|
+
classification, a literature overview) and `scholar-with-an-agent` (what
|
|
210
|
+
an agent running the `backing-claims` skill does with Scholar, and how to
|
|
211
|
+
audit it). Every lesson runs in its own workspace with its own session
|
|
212
|
+
store, so practice sessions never appear in your real `scholar sessions
|
|
213
|
+
list`.
|
|
214
|
+
|
|
190
215
|
## Installation
|
|
191
216
|
|
|
192
217
|
If you don't use `pipx`, you can install with `pip`:
|
|
@@ -230,7 +255,7 @@ Some providers require API keys set as environment variables:
|
|
|
230
255
|
| SSRN, bioRxiv, medRxiv, ChemRxiv, Research Square, Preprints.org, IACR | `SCHOLAR_EMAIL` | No | Any email (they search via OpenAlex) |
|
|
231
256
|
| Web of Science | `WOS_EXPANDED_API_KEY` or `WOS_STARTER_API_KEY` | Yes | [developer.clarivate.com](https://developer.clarivate.com) |
|
|
232
257
|
| IEEE Xplore | `IEEE_API_KEY` | Yes | [developer.ieee.org](https://developer.ieee.org) |
|
|
233
|
-
| Scopus | `SCOPUS_API_KEY` | Yes | [dev.elsevier.com](https://dev.elsevier.com) |
|
|
258
|
+
| Scopus | `SCOPUS_API_KEY` | Yes | [dev.elsevier.com](https://dev.elsevier.com). Searching works from any network, but abstracts, keywords and full author lists come only with Scopus's COMPLETE view, which is bound to the institution's IP range (or `SCOPUS_INST_TOKEN`). scholar asks for it and falls back by itself (`scholar providers check` tells which view you get); `SCOPUS_VIEW=standard` or `complete` overrides. A 400 "Error translating query" on *every* query is an Elsevier-side refusal, not a query error: it has cleared by itself before, so retry later |
|
|
234
259
|
|
|
235
260
|
View provider status:
|
|
236
261
|
|
|
@@ -238,6 +263,38 @@ View provider status:
|
|
|
238
263
|
scholar providers
|
|
239
264
|
```
|
|
240
265
|
|
|
266
|
+
### Network: proxy and User-Agent
|
|
267
|
+
|
|
268
|
+
To reach subscriber-only content from off campus (Scopus's COMPLETE
|
|
269
|
+
view, a publisher that admits the institution's IP range), route
|
|
270
|
+
scholar's requests through a proxy on that network, for example an SSH
|
|
271
|
+
SOCKS tunnel:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
ssh -D 127.0.0.1:1081 -N you@login.your-university.example &
|
|
275
|
+
scholar --proxy socks5h://127.0.0.1:1081 search "..."
|
|
276
|
+
export SCHOLAR_PROXY=socks5h://127.0.0.1:1081 # or for every command
|
|
277
|
+
scholar providers check --show-ip # route, exit IP, health
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`--proxy URL` (a global option, before the command) wins over
|
|
281
|
+
`SCHOLAR_PROXY`; with neither, the standard `HTTP(S)_PROXY`/`ALL_PROXY`
|
|
282
|
+
variables apply as usual. Use `socks5h://` rather than `socks5://` so
|
|
283
|
+
host names are resolved by the proxy, on the far side of the tunnel.
|
|
284
|
+
SOCKS needs the PySocks package (`pipx inject scholarcli PySocks`);
|
|
285
|
+
scholar refuses a SOCKS proxy without it rather than failing every
|
|
286
|
+
request. `scholar providers check` always prints the route; `--show-ip`
|
|
287
|
+
also asks api.ipify.org for the exit address (a third-party request, so
|
|
288
|
+
only on demand).
|
|
289
|
+
|
|
290
|
+
Requests identify themselves as
|
|
291
|
+
`scholar/<version> (+https://github.com/dbosk/scholar)`. Your
|
|
292
|
+
`SCHOLAR_EMAIL` is not in it, so publishers never see the address; only
|
|
293
|
+
the services that ask for one (OpenAlex, Crossref, Unpaywall) get it,
|
|
294
|
+
through their own parameters. Set `SCHOLAR_USER_AGENT` to replace it. Scholar does not imitate a browser: a Cloudflare challenge
|
|
295
|
+
or a login page from a publisher means the PDF is not available to
|
|
296
|
+
scripts, and scholar stops there.
|
|
297
|
+
|
|
241
298
|
## Usage Examples
|
|
242
299
|
|
|
243
300
|
### Basic Search
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scholarcli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.28"
|
|
4
4
|
description = "A tool for structured literature searches across bibliographic databases"
|
|
5
5
|
authors = [{ name = "Daniel Bosk", email = "dbosk@kth.se" },
|
|
6
6
|
{ name = "Ric Glassey", email = "glassey@kth.se" }]
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"arxiv>=2.1.0",
|
|
16
16
|
"cachetools>=6.2.4",
|
|
17
17
|
"platformdirs>=4.5.1",
|
|
18
|
+
"filelock>=3.12",
|
|
18
19
|
"textual>=6.11.0",
|
|
19
20
|
"pypandoc>=1.14",
|
|
20
21
|
"click>=8.0.0",
|
|
@@ -39,6 +40,8 @@ dependencies = [
|
|
|
39
40
|
"questionary>=2.0.0",
|
|
40
41
|
"llm-github-copilot>=0.3.1",
|
|
41
42
|
"llm-mistral (>=0.15,<0.16)",
|
|
43
|
+
"llm-openai-via-codex>=0.1a0",
|
|
44
|
+
"pytorial>=0.8.1",
|
|
42
45
|
]
|
|
43
46
|
|
|
44
47
|
[project.scripts]
|
|
@@ -51,6 +54,9 @@ build-backend = "setuptools.build_meta"
|
|
|
51
54
|
[tool.setuptools.packages.find]
|
|
52
55
|
where = ["src"]
|
|
53
56
|
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
scholar = ["tutorials/*.md"]
|
|
59
|
+
|
|
54
60
|
[tool.pytest.ini_options]
|
|
55
61
|
testpaths = ["tests"]
|
|
56
62
|
python_files = ["test_*.py"]
|
|
@@ -6,9 +6,15 @@ from .scholar import Search, SearchResult, Paper, SearchFilters
|
|
|
6
6
|
from .scholar import search, filter_papers, deduplicate_papers
|
|
7
7
|
from .scholar import deduplicate_versions, merge_version_of
|
|
8
8
|
from .scholar import get_registry, isolated_registry
|
|
9
|
+
from .scholar import hits_label, provider_outcome, result_outcome
|
|
10
|
+
from .scholar import normalize_venue
|
|
9
11
|
from .utils import safe_get_nested, ensure_list
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
14
|
+
"hits_label",
|
|
15
|
+
"provider_outcome",
|
|
16
|
+
"result_outcome",
|
|
17
|
+
"normalize_venue",
|
|
12
18
|
"Search",
|
|
13
19
|
"SearchResult",
|
|
14
20
|
"Paper",
|
|
@@ -15,6 +15,7 @@ import pickle
|
|
|
15
15
|
from typing import Any
|
|
16
16
|
import atexit
|
|
17
17
|
|
|
18
|
+
LLM_CACHE_PREFIX = "llm_"
|
|
18
19
|
# Registry of caches to save on exit: provider_name -> cache dict
|
|
19
20
|
CACHE_REGISTRY: dict[str, dict[Any, Any]] = {}
|
|
20
21
|
|
|
@@ -70,22 +71,49 @@ def save_cache(provider_name: str, cache: dict[Any, Any]) -> None:
|
|
|
70
71
|
logger.warning(f"Failed to save cache for {provider_name}: {e}")
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
def
|
|
74
|
+
def is_llm_cache(name: str) -> bool:
|
|
75
|
+
"""Return whether the cache `name` holds language-model output."""
|
|
76
|
+
return name.startswith(LLM_CACHE_PREFIX)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def llm_cache_files() -> list[Path]:
|
|
80
|
+
"""Return the LLM cache files in the cache directory."""
|
|
81
|
+
return sorted(
|
|
82
|
+
cache_file
|
|
83
|
+
for cache_file in get_cache_dir().glob("*.pkl")
|
|
84
|
+
if is_llm_cache(cache_file.stem)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def clear_cache(include_llm: bool = False) -> int:
|
|
74
89
|
"""
|
|
75
90
|
Clear all cached search results.
|
|
76
91
|
|
|
92
|
+
The LLM caches (see `is_llm_cache`) hold paid model output and are
|
|
93
|
+
kept unless `include_llm` is true.
|
|
94
|
+
|
|
95
|
+
Also empties the in-memory copy of every deleted cache that is
|
|
96
|
+
registered for saving at exit, so the exit hook cannot write the
|
|
97
|
+
old contents back.
|
|
98
|
+
|
|
77
99
|
Returns the number of cache files deleted.
|
|
78
100
|
"""
|
|
79
101
|
cache_dir = get_cache_dir()
|
|
80
102
|
count = 0
|
|
81
103
|
logger.info("Clearing all cache files")
|
|
82
104
|
for cache_file in cache_dir.glob("*.pkl"):
|
|
105
|
+
if is_llm_cache(cache_file.stem) and not include_llm:
|
|
106
|
+
continue
|
|
83
107
|
try:
|
|
84
108
|
cache_file.unlink()
|
|
85
109
|
count += 1
|
|
86
110
|
logger.debug(f"Deleted cache file: {cache_file.name}")
|
|
87
111
|
except OSError as e:
|
|
88
112
|
logger.warning(f"Failed to delete {cache_file.name}: {e}")
|
|
113
|
+
continue
|
|
114
|
+
registered = CACHE_REGISTRY.get(cache_file.stem)
|
|
115
|
+
if registered is not None:
|
|
116
|
+
registered.clear()
|
|
89
117
|
logger.info(f"Cleared {count} cache file(s)")
|
|
90
118
|
return count
|
|
91
119
|
|
|
@@ -97,26 +125,31 @@ def get_cache_stats() -> dict[str, Any]:
|
|
|
97
125
|
Returns a dictionary with:
|
|
98
126
|
- cache_dir: Path to the cache directory
|
|
99
127
|
- providers: Dict mapping provider names to entry counts
|
|
100
|
-
|
|
101
|
-
-
|
|
128
|
+
(search and lookup caches, which `clear_cache` removes)
|
|
129
|
+
- llm_caches: Dict mapping LLM cache names to entry counts
|
|
130
|
+
(kept by `clear_cache` unless `include_llm` is true)
|
|
131
|
+
- total_entries: Total number of cached entries in all files
|
|
132
|
+
- total_size_bytes: Total size of all cache files
|
|
102
133
|
"""
|
|
103
134
|
cache_dir = get_cache_dir()
|
|
104
135
|
providers: dict[str, int] = {}
|
|
136
|
+
llm_caches: dict[str, int] = {}
|
|
105
137
|
total_size = 0
|
|
106
138
|
|
|
107
139
|
for cache_file in cache_dir.glob("*.pkl"):
|
|
108
|
-
|
|
140
|
+
name = cache_file.stem
|
|
109
141
|
total_size += cache_file.stat().st_size
|
|
142
|
+
counts = llm_caches if is_llm_cache(name) else providers
|
|
110
143
|
try:
|
|
111
|
-
|
|
112
|
-
providers[provider_name] = len(cache)
|
|
144
|
+
counts[name] = len(load_cache(name))
|
|
113
145
|
except Exception:
|
|
114
|
-
|
|
146
|
+
counts[name] = 0
|
|
115
147
|
|
|
116
148
|
return {
|
|
117
149
|
"cache_dir": str(cache_dir),
|
|
118
150
|
"providers": providers,
|
|
119
|
-
"
|
|
151
|
+
"llm_caches": llm_caches,
|
|
152
|
+
"total_entries": sum(providers.values()) + sum(llm_caches.values()),
|
|
120
153
|
"total_size_bytes": total_size,
|
|
121
154
|
}
|
|
122
155
|
|