bibcite-cli 0.6.7__tar.gz → 0.6.8__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.
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/PKG-INFO +1 -1
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/pyproject.toml +1 -1
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/cli.py +37 -3
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/sources.py +6 -1
- bibcite_cli-0.6.8/tests/test_cli_normalize.py +16 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_cli_status.py +62 -1
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_status_semantics.py +25 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/uv.lock +1 -1
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.github/workflows/ci.yml +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.github/workflows/publish.yml +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.gitignore +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/LICENSE +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/Readme.md +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/assets/bibcite.svg +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/skills/bibcite/SKILL.md +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/__init__.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/bibfile.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/cache.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/data/strings.bib +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/normalize.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/resolve.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/venues.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_bibfile.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_bugfixes.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_dblp.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_entry_types.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_no_google_scholar.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_normalize.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_public_service.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_round2.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_round3.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_s2_pacing.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_source_retries.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_strings_override.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_venues.py +0 -0
- {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_webpages.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bibcite-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
4
4
|
Summary: Resolve papers (arXiv id / DOI / title) to canonical, normalized BibTeX for agents and humans
|
|
5
5
|
Project-URL: Repository, https://github.com/leo1oel/bibcite
|
|
6
6
|
License-Expression: MIT
|
|
@@ -97,6 +97,27 @@ def cmd_get(args) -> int:
|
|
|
97
97
|
return 0
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def cmd_normalize(args) -> int:
|
|
101
|
+
"""Canonicalize already retrieved BibTeX without any network lookup."""
|
|
102
|
+
try:
|
|
103
|
+
db = bibfile.parse_bib(Path(args.file).read_text())
|
|
104
|
+
normalized = []
|
|
105
|
+
for entry in db.entries:
|
|
106
|
+
raw_venue = entry.get("booktitle", "") or entry.get("journal", "")
|
|
107
|
+
canonical = canonicalize(raw_venue, entry.get("year"))
|
|
108
|
+
if canonical:
|
|
109
|
+
entry.pop("booktitle", None)
|
|
110
|
+
entry.pop("journal", None)
|
|
111
|
+
entry["ENTRYTYPE"] = canonical.entry_type
|
|
112
|
+
entry[canonical.bib_field] = canonical.name
|
|
113
|
+
normalized.append(bibfile.entry_to_bibtex(entry))
|
|
114
|
+
except (OSError, ValueError) as error:
|
|
115
|
+
_log(f"[bibcite] {error}")
|
|
116
|
+
return EXIT_ISSUES
|
|
117
|
+
_emit({"bibtex": normalized})
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
100
121
|
# ---------------------------------------------------------------------------
|
|
101
122
|
# add
|
|
102
123
|
# ---------------------------------------------------------------------------
|
|
@@ -288,7 +309,9 @@ def cmd_add(args) -> int:
|
|
|
288
309
|
# upgrade: batch-match arXiv entries in an existing file (bibMatcher, CLI-style)
|
|
289
310
|
# ---------------------------------------------------------------------------
|
|
290
311
|
|
|
291
|
-
def _upgrade_entries(
|
|
312
|
+
def _upgrade_entries(
|
|
313
|
+
path: Path, dry_run: bool, include_published_arxiv: bool = False
|
|
314
|
+
) -> dict:
|
|
292
315
|
"""Match every preprint entry in ``path`` to its published version and
|
|
293
316
|
rewrite it in place (unless dry_run). Returns the report; does NOT tidy —
|
|
294
317
|
callers decide."""
|
|
@@ -301,7 +324,9 @@ def _upgrade_entries(path: Path, dry_run: bool) -> dict:
|
|
|
301
324
|
changed = 0
|
|
302
325
|
processed = 0
|
|
303
326
|
for entry in db.entries:
|
|
304
|
-
if not bibfile.is_preprint(entry)
|
|
327
|
+
if not bibfile.is_preprint(entry) and not (
|
|
328
|
+
include_published_arxiv and bibfile.entry_arxiv_id(entry)
|
|
329
|
+
):
|
|
305
330
|
continue
|
|
306
331
|
if entry.get("pubstate", "").strip("{}") == "preprint":
|
|
307
332
|
# User-confirmed preprint-only (e.g. never-to-be-published arXiv
|
|
@@ -386,7 +411,7 @@ def cmd_upgrade(args) -> int:
|
|
|
386
411
|
if not path.exists():
|
|
387
412
|
_log(f"[bibcite] {path} does not exist")
|
|
388
413
|
return EXIT_ISSUES
|
|
389
|
-
result = _upgrade_entries(path, args.dry_run)
|
|
414
|
+
result = _upgrade_entries(path, args.dry_run, args.include_published_arxiv)
|
|
390
415
|
tidied = False
|
|
391
416
|
if result["upgraded"] and not args.no_tidy:
|
|
392
417
|
tidied = bibfile.run_tidy(path)
|
|
@@ -550,8 +575,17 @@ def main(argv=None) -> int:
|
|
|
550
575
|
u.add_argument("--dry-run", action="store_true")
|
|
551
576
|
u.add_argument("--no-tidy", action="store_true")
|
|
552
577
|
u.add_argument("--no-cache", action="store_true", help="bypass the local match cache")
|
|
578
|
+
u.add_argument(
|
|
579
|
+
"--include-published-arxiv",
|
|
580
|
+
action="store_true",
|
|
581
|
+
help="also audit published-venue entries that retain an arXiv identifier",
|
|
582
|
+
)
|
|
553
583
|
u.set_defaults(fn=cmd_upgrade)
|
|
554
584
|
|
|
585
|
+
n = sub.add_parser("normalize", help="offline canonicalization of a BibTeX file (prints JSON)")
|
|
586
|
+
n.add_argument("file")
|
|
587
|
+
n.set_defaults(fn=cmd_normalize)
|
|
588
|
+
|
|
555
589
|
t = sub.add_parser("tidy", help="run bibtex-tidy with the canonical flags")
|
|
556
590
|
t.add_argument("file")
|
|
557
591
|
t.set_defaults(fn=cmd_tidy)
|
|
@@ -211,7 +211,7 @@ def _s2_headers() -> dict:
|
|
|
211
211
|
|
|
212
212
|
def _s2_batch_status() -> str | None:
|
|
213
213
|
status = os.environ.get("BIBCITE_S2_BATCH_STATUS")
|
|
214
|
-
return status if status in {"checked", "unavailable"} else None
|
|
214
|
+
return status if status in {"checked", "unavailable", "disabled"} else None
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
def _mailto() -> str:
|
|
@@ -1048,6 +1048,11 @@ def find_published(
|
|
|
1048
1048
|
elif batch_status == "unavailable":
|
|
1049
1049
|
_log("[semanticscholar] batch unavailable")
|
|
1050
1050
|
incomplete = True
|
|
1051
|
+
elif batch_status == "disabled":
|
|
1052
|
+
# The caller intentionally excluded S2. It is neither an attempted
|
|
1053
|
+
# source nor a failed one, so clean misses from the enabled core
|
|
1054
|
+
# sources remain authoritative.
|
|
1055
|
+
_log("[semanticscholar] disabled by caller")
|
|
1051
1056
|
# Query every still-viable source concurrently. A preprint with no published
|
|
1052
1057
|
# version (the common case) misses everywhere, and used to pay the *sum* of
|
|
1053
1058
|
# each source's latency; now the wall-clock is the slowest single source.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from bibcite.cli import main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_normalize_preserves_entry_order_offline(tmp_path, capsys):
|
|
7
|
+
source = tmp_path / "s2.bib"
|
|
8
|
+
source.write_text(
|
|
9
|
+
"@inproceedings{a, title={A}, year={2024}, booktitle={CVPR}}\n"
|
|
10
|
+
"@article{b, title={B}, year={2023}, journal={Nature}}\n"
|
|
11
|
+
)
|
|
12
|
+
assert main(["normalize", str(source)]) == 0
|
|
13
|
+
payload = json.loads(capsys.readouterr().out)
|
|
14
|
+
assert len(payload["bibtex"]) == 2
|
|
15
|
+
assert "IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)" in payload["bibtex"][0]
|
|
16
|
+
assert "journal = {Nature}" in payload["bibtex"][1]
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from bibcite import bibfile, cli
|
|
4
4
|
from bibcite.cli import main
|
|
5
|
+
from bibcite.sources import Match
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def test_check_fails_for_missing_file(tmp_path):
|
|
@@ -28,6 +29,62 @@ def test_upgrade_fails_for_missing_file(tmp_path):
|
|
|
28
29
|
assert main(["upgrade", str(tmp_path / "missing.bib")]) == 1
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def test_upgrade_opt_in_audits_published_arxiv_entry_through_real_cli(
|
|
33
|
+
tmp_path, monkeypatch, capsys
|
|
34
|
+
):
|
|
35
|
+
bib = tmp_path / "refs.bib"
|
|
36
|
+
original_venue = (
|
|
37
|
+
"IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)"
|
|
38
|
+
)
|
|
39
|
+
bib.write_text(
|
|
40
|
+
"@inproceedings{ranzinger2024amradio,\n"
|
|
41
|
+
" author={Mike Ranzinger and Greg Heinrich and Jan Kautz and Pavlo Molchanov},\n"
|
|
42
|
+
f" booktitle={{{original_venue}}},\n"
|
|
43
|
+
" eprint={2312.06709},\n"
|
|
44
|
+
" title={{AM-RADIO:} Agglomerative Vision Foundation Model Reduce All Domains Into One},\n"
|
|
45
|
+
" year={2024}\n}\n"
|
|
46
|
+
)
|
|
47
|
+
monkeypatch.setattr(bibfile, "run_tidy", lambda path: True)
|
|
48
|
+
monkeypatch.setattr(
|
|
49
|
+
cli,
|
|
50
|
+
"find_published",
|
|
51
|
+
lambda *args: (
|
|
52
|
+
Match(
|
|
53
|
+
venue=original_venue,
|
|
54
|
+
year="2024",
|
|
55
|
+
doi="10.1109/CVPR52733.2024.01219",
|
|
56
|
+
source="dblp",
|
|
57
|
+
),
|
|
58
|
+
"found",
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert main(["upgrade", str(bib), "--include-published-arxiv"]) == 0
|
|
63
|
+
|
|
64
|
+
result = json.loads(capsys.readouterr().out)
|
|
65
|
+
updated = bibfile.load_bib_file(bib).entries[0]
|
|
66
|
+
assert result["upgraded"] == 1
|
|
67
|
+
assert updated["booktitle"] == original_venue
|
|
68
|
+
assert updated["doi"] == "10.1109/CVPR52733.2024.01219"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_upgrade_opt_in_preserves_published_venue_on_no_match(
|
|
72
|
+
tmp_path, monkeypatch, capsys
|
|
73
|
+
):
|
|
74
|
+
bib = tmp_path / "refs.bib"
|
|
75
|
+
bib.write_text(
|
|
76
|
+
"@inproceedings{x, title={T}, booktitle={Existing Venue}, "
|
|
77
|
+
"eprint={2312.06709}, year={2024}}\n"
|
|
78
|
+
)
|
|
79
|
+
before = bib.read_text()
|
|
80
|
+
monkeypatch.setattr(cli, "find_published", lambda *args: (None, "not_found"))
|
|
81
|
+
|
|
82
|
+
assert main(["upgrade", str(bib), "--include-published-arxiv", "--no-tidy"]) == 0
|
|
83
|
+
|
|
84
|
+
assert json.loads(capsys.readouterr().out)["upgraded"] == 0
|
|
85
|
+
assert bib.read_text() == before
|
|
86
|
+
|
|
87
|
+
|
|
31
88
|
def test_add_fails_when_automatic_tidy_fails(tmp_path, monkeypatch, capsys):
|
|
32
89
|
bib = tmp_path / "refs.bib"
|
|
33
90
|
monkeypatch.setattr(bibfile, "run_tidy", lambda path: False)
|
|
@@ -92,7 +149,11 @@ def test_upgrade_fails_when_automatic_tidy_fails(tmp_path, monkeypatch, capsys):
|
|
|
92
149
|
monkeypatch.setattr(
|
|
93
150
|
cli,
|
|
94
151
|
"_upgrade_entries",
|
|
95
|
-
lambda path, dry_run
|
|
152
|
+
lambda path, dry_run, include_published_arxiv: {
|
|
153
|
+
"upgraded": 1,
|
|
154
|
+
"matched": 1,
|
|
155
|
+
"entries": [],
|
|
156
|
+
},
|
|
96
157
|
)
|
|
97
158
|
monkeypatch.setattr(bibfile, "run_tidy", lambda path: False)
|
|
98
159
|
|
|
@@ -106,3 +106,28 @@ def test_batch_status_suppresses_s2_metadata_request(monkeypatch):
|
|
|
106
106
|
|
|
107
107
|
with pytest.raises(SourceUnavailable):
|
|
108
108
|
sources._s2_get(Client(), "https://api.semanticscholar.org/test", {})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_disabled_s2_is_never_called_and_does_not_taint_clean_misses(monkeypatch):
|
|
112
|
+
calls = []
|
|
113
|
+
|
|
114
|
+
def source(name):
|
|
115
|
+
def run(*args):
|
|
116
|
+
calls.append(name)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return run
|
|
120
|
+
|
|
121
|
+
monkeypatch.setenv("BIBCITE_S2_BATCH_STATUS", "disabled")
|
|
122
|
+
monkeypatch.setenv("S2_API_KEY", "must-not-be-used")
|
|
123
|
+
monkeypatch.setenv("BIBCITE_PUBLIC_SERVICE_URL", "https://example.test")
|
|
124
|
+
monkeypatch.setattr(
|
|
125
|
+
sources,
|
|
126
|
+
"CASCADE",
|
|
127
|
+
(("semanticscholar", source("s2")), ("dblp", source("dblp"))),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
match, status = find_published("Clean miss")
|
|
131
|
+
|
|
132
|
+
assert (match, status) == (None, "not_found")
|
|
133
|
+
assert calls == ["dblp"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|