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.
Files changed (36) hide show
  1. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/PKG-INFO +1 -1
  2. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/pyproject.toml +1 -1
  3. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/cli.py +37 -3
  4. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/sources.py +6 -1
  5. bibcite_cli-0.6.8/tests/test_cli_normalize.py +16 -0
  6. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_cli_status.py +62 -1
  7. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_status_semantics.py +25 -0
  8. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/uv.lock +1 -1
  9. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.github/workflows/ci.yml +0 -0
  10. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.github/workflows/publish.yml +0 -0
  11. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/.gitignore +0 -0
  12. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/LICENSE +0 -0
  13. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/Readme.md +0 -0
  14. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/assets/bibcite.svg +0 -0
  15. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/skills/bibcite/SKILL.md +0 -0
  16. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/__init__.py +0 -0
  17. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/bibfile.py +0 -0
  18. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/cache.py +0 -0
  19. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/data/strings.bib +0 -0
  20. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/normalize.py +0 -0
  21. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/resolve.py +0 -0
  22. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/src/bibcite/venues.py +0 -0
  23. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_bibfile.py +0 -0
  24. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_bugfixes.py +0 -0
  25. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_dblp.py +0 -0
  26. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_entry_types.py +0 -0
  27. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_no_google_scholar.py +0 -0
  28. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_normalize.py +0 -0
  29. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_public_service.py +0 -0
  30. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_round2.py +0 -0
  31. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_round3.py +0 -0
  32. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_s2_pacing.py +0 -0
  33. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_source_retries.py +0 -0
  34. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_strings_override.py +0 -0
  35. {bibcite_cli-0.6.7 → bibcite_cli-0.6.8}/tests/test_venues.py +0 -0
  36. {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.7
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bibcite-cli"
3
- version = "0.6.7"
3
+ version = "0.6.8"
4
4
  description = "Resolve papers (arXiv id / DOI / title) to canonical, normalized BibTeX for agents and humans"
5
5
  readme = "Readme.md"
6
6
  license = "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(path: Path, dry_run: bool) -> dict:
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: {"upgraded": 1, "matched": 1, "entries": []},
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"]
@@ -18,7 +18,7 @@ wheels = [
18
18
 
19
19
  [[package]]
20
20
  name = "bibcite-cli"
21
- version = "0.6.7"
21
+ version = "0.6.8"
22
22
  source = { editable = "." }
23
23
  dependencies = [
24
24
  { name = "bibtexparser" },
File without changes
File without changes
File without changes