citegate 0.1.0__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.
- citegate-0.1.0/LICENSE +21 -0
- citegate-0.1.0/PKG-INFO +119 -0
- citegate-0.1.0/README.md +94 -0
- citegate-0.1.0/pyproject.toml +52 -0
- citegate-0.1.0/setup.cfg +4 -0
- citegate-0.1.0/src/citegate/__init__.py +3 -0
- citegate-0.1.0/src/citegate/cli.py +144 -0
- citegate-0.1.0/src/citegate/core.py +388 -0
- citegate-0.1.0/src/citegate/report.py +117 -0
- citegate-0.1.0/src/citegate/sources.py +190 -0
- citegate-0.1.0/src/citegate.egg-info/PKG-INFO +119 -0
- citegate-0.1.0/src/citegate.egg-info/SOURCES.txt +16 -0
- citegate-0.1.0/src/citegate.egg-info/dependency_links.txt +1 -0
- citegate-0.1.0/src/citegate.egg-info/entry_points.txt +2 -0
- citegate-0.1.0/src/citegate.egg-info/requires.txt +5 -0
- citegate-0.1.0/src/citegate.egg-info/top_level.txt +1 -0
- citegate-0.1.0/tests/test_core.py +170 -0
- citegate-0.1.0/tests/test_live.py +47 -0
citegate-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yang Song
|
|
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.
|
citegate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: citegate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Citation integrity as a CI gate: verify BibTeX references against Crossref and OpenAlex, catch fabricated citations, and get alerted when a paper you cite is retracted.
|
|
5
|
+
Author-email: Yang Song <songyang0714@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/chrisyangsong/citegate
|
|
8
|
+
Project-URL: Issues, https://github.com/chrisyangsong/citegate/issues
|
|
9
|
+
Keywords: bibtex,citations,references,hallucination,retraction,crossref,openalex,ci,research-integrity
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.28
|
|
21
|
+
Requires-Dist: bibtexparser<2,>=1.4
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# citegate
|
|
27
|
+
|
|
28
|
+
**Citation integrity as a CI gate.** citegate verifies every entry in your BibTeX files against [Crossref](https://www.crossref.org/) and [OpenAlex](https://openalex.org/), then fails your build when a reference is fabricated, wrong, or retracted.
|
|
29
|
+
|
|
30
|
+
LLM writing assistants fabricate plausible-looking references, and even careful humans cite papers that later get retracted. Existing checkers are interactive tools you have to remember to run. citegate is the piece that belongs in your repository instead: a GitHub Action, a pre-commit hook, and a weekly monitor that opens an issue the day a paper you cite is retracted.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
OK he2016deep
|
|
34
|
+
NOT FOUND fabricated2023 (best title similarity 0.42)
|
|
35
|
+
- no record with a similar title in Crossref or OpenAlex — possibly a fabricated reference
|
|
36
|
+
RETRACTED wakefield1998retracted
|
|
37
|
+
- OpenAlex marks this work as retracted
|
|
38
|
+
- Crossref/Retraction Watch records: retraction
|
|
39
|
+
|
|
40
|
+
citegate: checked 3 entries — 1 verified, 1 retracted, 1 not-found
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it checks
|
|
44
|
+
|
|
45
|
+
| Verdict | Meaning |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `verified` | The entry matches a real indexed work (title, year, authors agree). |
|
|
48
|
+
| `not-found` | No similar record exists in Crossref or OpenAlex. Likely fabricated. |
|
|
49
|
+
| `retracted` | The cited work is retracted or withdrawn, per OpenAlex and the [Retraction Watch data in Crossref](https://www.crossref.org/blog/news-crossref-and-retraction-watch/). |
|
|
50
|
+
| `mismatch` | A real work exists, but the year, title, or authors in your entry disagree with the index. |
|
|
51
|
+
| `unverifiable` | Websites, standards, and other entries without a DOI that scholarly indexes do not cover. Never fails the build. |
|
|
52
|
+
| `error` | A source API was unreachable. |
|
|
53
|
+
|
|
54
|
+
Entries with a DOI are resolved directly and their metadata compared field by field. Entries without a DOI are matched by fuzzy bibliographic search across both indexes; strong matches also get a `suggestion` with the DOI you should add.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install citegate # or: pipx install citegate
|
|
60
|
+
citegate paper/references.bib --mailto you@example.edu
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--mailto` is optional but recommended: it identifies you to the APIs and places you in Crossref's polite pool. The exit code is non-zero when any `not-found` or `retracted` entry appears (configurable with `--fail-on not-found,retracted,mismatch`), so the same command works locally and in CI. Add `--json report.json` for machine-readable output and `--cache` to skip unchanged entries on repeated runs.
|
|
64
|
+
|
|
65
|
+
## GitHub Action
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
name: References
|
|
69
|
+
on: [push, pull_request]
|
|
70
|
+
jobs:
|
|
71
|
+
citegate:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- uses: chrisyangsong/citegate@main
|
|
76
|
+
with:
|
|
77
|
+
files: '**/*.bib'
|
|
78
|
+
mailto: 'you@example.edu'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Failures show up as inline annotations and a job-summary table listing exactly which entries are suspect and why.
|
|
82
|
+
|
|
83
|
+
## Retraction monitoring
|
|
84
|
+
|
|
85
|
+
A bibliography that verified cleanly last month can go bad without you touching it: about [one in 500 published papers is eventually retracted](https://www.crossref.org/blog/news-crossref-and-retraction-watch/), and citing one in a submission is an avoidable reviewer complaint. Copy [`examples/retraction-monitor.yml`](examples/retraction-monitor.yml) into `.github/workflows/` and citegate re-verifies your references every Monday, opening an issue in your repository when a cited paper is retracted or stops resolving.
|
|
86
|
+
|
|
87
|
+
## pre-commit hook
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
# .pre-commit-config.yaml
|
|
91
|
+
repos:
|
|
92
|
+
- repo: https://github.com/chrisyangsong/citegate
|
|
93
|
+
rev: v0.1.0
|
|
94
|
+
hooks:
|
|
95
|
+
- id: citegate
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Relation to other tools
|
|
99
|
+
|
|
100
|
+
Several good interactive checkers exist, including [refchecker](https://github.com/markrussinovich/refchecker) and [hallucinator](https://github.com/gianlucasb/hallucinator) for auditing a finished paper or PDF, and browser tools like [BibTeX Verifier](https://merfanian.github.io/Bibtex-Verifier/). citegate covers the other half of the problem: it lives in the repository with your `.bib` files, runs automatically on every push, and keeps watching after you stop looking. If you want a one-off deep audit of a PDF, use those tools; if you want your references checked continuously, use citegate.
|
|
101
|
+
|
|
102
|
+
## Design notes
|
|
103
|
+
|
|
104
|
+
- Sources: Crossref (REST API, polite pool) and OpenAlex. Retraction status is the union of OpenAlex's `is_retracted` flag and Crossref update records, which include the Retraction Watch database.
|
|
105
|
+
- Matching is deliberately conservative: `@misc` and other non-indexed entry types without DOIs are skipped rather than flagged, and a one-year slack is allowed on years (print vs online dates). False alarms are the fastest way to get a checker removed from CI.
|
|
106
|
+
- No LLMs are involved in verification; every verdict is traceable to an index record.
|
|
107
|
+
|
|
108
|
+
## Roadmap
|
|
109
|
+
|
|
110
|
+
- Parallel lookups for large bibliographies
|
|
111
|
+
- DOCX/PDF reference-list extraction (currently BibTeX only)
|
|
112
|
+
- arXiv and DBLP as additional sources
|
|
113
|
+
- An `--only retractions` fast mode for high-frequency monitoring
|
|
114
|
+
|
|
115
|
+
Issues and pull requests are welcome.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT © 2026 Yang Song. Not affiliated with Crossref, OpenAlex, or Retraction Watch; please respect their API terms.
|
citegate-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# citegate
|
|
2
|
+
|
|
3
|
+
**Citation integrity as a CI gate.** citegate verifies every entry in your BibTeX files against [Crossref](https://www.crossref.org/) and [OpenAlex](https://openalex.org/), then fails your build when a reference is fabricated, wrong, or retracted.
|
|
4
|
+
|
|
5
|
+
LLM writing assistants fabricate plausible-looking references, and even careful humans cite papers that later get retracted. Existing checkers are interactive tools you have to remember to run. citegate is the piece that belongs in your repository instead: a GitHub Action, a pre-commit hook, and a weekly monitor that opens an issue the day a paper you cite is retracted.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
OK he2016deep
|
|
9
|
+
NOT FOUND fabricated2023 (best title similarity 0.42)
|
|
10
|
+
- no record with a similar title in Crossref or OpenAlex — possibly a fabricated reference
|
|
11
|
+
RETRACTED wakefield1998retracted
|
|
12
|
+
- OpenAlex marks this work as retracted
|
|
13
|
+
- Crossref/Retraction Watch records: retraction
|
|
14
|
+
|
|
15
|
+
citegate: checked 3 entries — 1 verified, 1 retracted, 1 not-found
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it checks
|
|
19
|
+
|
|
20
|
+
| Verdict | Meaning |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `verified` | The entry matches a real indexed work (title, year, authors agree). |
|
|
23
|
+
| `not-found` | No similar record exists in Crossref or OpenAlex. Likely fabricated. |
|
|
24
|
+
| `retracted` | The cited work is retracted or withdrawn, per OpenAlex and the [Retraction Watch data in Crossref](https://www.crossref.org/blog/news-crossref-and-retraction-watch/). |
|
|
25
|
+
| `mismatch` | A real work exists, but the year, title, or authors in your entry disagree with the index. |
|
|
26
|
+
| `unverifiable` | Websites, standards, and other entries without a DOI that scholarly indexes do not cover. Never fails the build. |
|
|
27
|
+
| `error` | A source API was unreachable. |
|
|
28
|
+
|
|
29
|
+
Entries with a DOI are resolved directly and their metadata compared field by field. Entries without a DOI are matched by fuzzy bibliographic search across both indexes; strong matches also get a `suggestion` with the DOI you should add.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install citegate # or: pipx install citegate
|
|
35
|
+
citegate paper/references.bib --mailto you@example.edu
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--mailto` is optional but recommended: it identifies you to the APIs and places you in Crossref's polite pool. The exit code is non-zero when any `not-found` or `retracted` entry appears (configurable with `--fail-on not-found,retracted,mismatch`), so the same command works locally and in CI. Add `--json report.json` for machine-readable output and `--cache` to skip unchanged entries on repeated runs.
|
|
39
|
+
|
|
40
|
+
## GitHub Action
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
name: References
|
|
44
|
+
on: [push, pull_request]
|
|
45
|
+
jobs:
|
|
46
|
+
citegate:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: chrisyangsong/citegate@main
|
|
51
|
+
with:
|
|
52
|
+
files: '**/*.bib'
|
|
53
|
+
mailto: 'you@example.edu'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Failures show up as inline annotations and a job-summary table listing exactly which entries are suspect and why.
|
|
57
|
+
|
|
58
|
+
## Retraction monitoring
|
|
59
|
+
|
|
60
|
+
A bibliography that verified cleanly last month can go bad without you touching it: about [one in 500 published papers is eventually retracted](https://www.crossref.org/blog/news-crossref-and-retraction-watch/), and citing one in a submission is an avoidable reviewer complaint. Copy [`examples/retraction-monitor.yml`](examples/retraction-monitor.yml) into `.github/workflows/` and citegate re-verifies your references every Monday, opening an issue in your repository when a cited paper is retracted or stops resolving.
|
|
61
|
+
|
|
62
|
+
## pre-commit hook
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
# .pre-commit-config.yaml
|
|
66
|
+
repos:
|
|
67
|
+
- repo: https://github.com/chrisyangsong/citegate
|
|
68
|
+
rev: v0.1.0
|
|
69
|
+
hooks:
|
|
70
|
+
- id: citegate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Relation to other tools
|
|
74
|
+
|
|
75
|
+
Several good interactive checkers exist, including [refchecker](https://github.com/markrussinovich/refchecker) and [hallucinator](https://github.com/gianlucasb/hallucinator) for auditing a finished paper or PDF, and browser tools like [BibTeX Verifier](https://merfanian.github.io/Bibtex-Verifier/). citegate covers the other half of the problem: it lives in the repository with your `.bib` files, runs automatically on every push, and keeps watching after you stop looking. If you want a one-off deep audit of a PDF, use those tools; if you want your references checked continuously, use citegate.
|
|
76
|
+
|
|
77
|
+
## Design notes
|
|
78
|
+
|
|
79
|
+
- Sources: Crossref (REST API, polite pool) and OpenAlex. Retraction status is the union of OpenAlex's `is_retracted` flag and Crossref update records, which include the Retraction Watch database.
|
|
80
|
+
- Matching is deliberately conservative: `@misc` and other non-indexed entry types without DOIs are skipped rather than flagged, and a one-year slack is allowed on years (print vs online dates). False alarms are the fastest way to get a checker removed from CI.
|
|
81
|
+
- No LLMs are involved in verification; every verdict is traceable to an index record.
|
|
82
|
+
|
|
83
|
+
## Roadmap
|
|
84
|
+
|
|
85
|
+
- Parallel lookups for large bibliographies
|
|
86
|
+
- DOCX/PDF reference-list extraction (currently BibTeX only)
|
|
87
|
+
- arXiv and DBLP as additional sources
|
|
88
|
+
- An `--only retractions` fast mode for high-frequency monitoring
|
|
89
|
+
|
|
90
|
+
Issues and pull requests are welcome.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT © 2026 Yang Song. Not affiliated with Crossref, OpenAlex, or Retraction Watch; please respect their API terms.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "citegate"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Citation integrity as a CI gate: verify BibTeX references against Crossref and OpenAlex, catch fabricated citations, and get alerted when a paper you cite is retracted."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Yang Song", email = "songyang0714@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"bibtex",
|
|
15
|
+
"citations",
|
|
16
|
+
"references",
|
|
17
|
+
"hallucination",
|
|
18
|
+
"retraction",
|
|
19
|
+
"crossref",
|
|
20
|
+
"openalex",
|
|
21
|
+
"ci",
|
|
22
|
+
"research-integrity",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Environment :: Console",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Topic :: Scientific/Engineering",
|
|
31
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"requests>=2.28",
|
|
35
|
+
"bibtexparser>=1.4,<2",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest>=7"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/chrisyangsong/citegate"
|
|
43
|
+
Issues = "https://github.com/chrisyangsong/citegate/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
citegate = "citegate.cli:main"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
citegate-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""citegate command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import glob
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .core import (
|
|
14
|
+
Entry,
|
|
15
|
+
Result,
|
|
16
|
+
ResultCache,
|
|
17
|
+
Verdict,
|
|
18
|
+
entry_cache_key,
|
|
19
|
+
parse_bib_file,
|
|
20
|
+
result_from_dict,
|
|
21
|
+
verify_entry,
|
|
22
|
+
)
|
|
23
|
+
from .report import github_annotations, github_step_summary, print_console, write_json
|
|
24
|
+
from .sources import make_clients
|
|
25
|
+
|
|
26
|
+
FAIL_CHOICES = {
|
|
27
|
+
"not-found": Verdict.NOT_FOUND,
|
|
28
|
+
"retracted": Verdict.RETRACTED,
|
|
29
|
+
"mismatch": Verdict.MISMATCH,
|
|
30
|
+
"error": Verdict.ERROR,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="citegate",
|
|
37
|
+
description=(
|
|
38
|
+
"Verify BibTeX references against Crossref and OpenAlex: catch fabricated "
|
|
39
|
+
"citations, metadata errors, and retracted papers — and fail CI when they appear."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument("files", nargs="+", help=".bib files or globs to check")
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--fail-on",
|
|
45
|
+
default="not-found,retracted",
|
|
46
|
+
help="comma-separated verdicts that fail the run "
|
|
47
|
+
"(choices: not-found, retracted, mismatch, error; default: not-found,retracted)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--mailto",
|
|
51
|
+
default=os.environ.get("CITEGATE_MAILTO", ""),
|
|
52
|
+
help="contact email sent to the APIs (enables the Crossref polite pool); "
|
|
53
|
+
"also read from CITEGATE_MAILTO",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument("--json", metavar="PATH", help="write a JSON report to PATH ('-' for stdout)")
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--cache",
|
|
58
|
+
metavar="PATH",
|
|
59
|
+
nargs="?",
|
|
60
|
+
const=".citegate-cache.json",
|
|
61
|
+
help="cache results in a JSON file so repeated runs skip unchanged entries "
|
|
62
|
+
"(default path: .citegate-cache.json)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--quiet", action="store_true", help="only print entries with problems")
|
|
65
|
+
parser.add_argument("--version", action="version", version=f"citegate {__version__}")
|
|
66
|
+
return parser
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collect_files(patterns: List[str]) -> List[Path]:
|
|
70
|
+
paths: List[Path] = []
|
|
71
|
+
for pattern in patterns:
|
|
72
|
+
matches = sorted(glob.glob(pattern, recursive=True))
|
|
73
|
+
if matches:
|
|
74
|
+
paths.extend(Path(m) for m in matches)
|
|
75
|
+
elif Path(pattern).exists():
|
|
76
|
+
paths.append(Path(pattern))
|
|
77
|
+
else:
|
|
78
|
+
print(f"citegate: no files match '{pattern}'", file=sys.stderr)
|
|
79
|
+
seen = set()
|
|
80
|
+
unique = []
|
|
81
|
+
for p in paths:
|
|
82
|
+
if p not in seen:
|
|
83
|
+
seen.add(p)
|
|
84
|
+
unique.append(p)
|
|
85
|
+
return unique
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv: List[str] = None) -> int:
|
|
89
|
+
args = build_parser().parse_args(argv)
|
|
90
|
+
|
|
91
|
+
failing = set()
|
|
92
|
+
for name in args.fail_on.split(","):
|
|
93
|
+
name = name.strip().lower()
|
|
94
|
+
if not name:
|
|
95
|
+
continue
|
|
96
|
+
if name not in FAIL_CHOICES:
|
|
97
|
+
print(f"citegate: unknown --fail-on value '{name}'", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
failing.add(FAIL_CHOICES[name])
|
|
100
|
+
|
|
101
|
+
files = collect_files(args.files)
|
|
102
|
+
if not files:
|
|
103
|
+
print("citegate: no .bib files to check", file=sys.stderr)
|
|
104
|
+
return 2
|
|
105
|
+
|
|
106
|
+
entries: List[Entry] = []
|
|
107
|
+
for path in files:
|
|
108
|
+
try:
|
|
109
|
+
entries.extend(parse_bib_file(path))
|
|
110
|
+
except Exception as exc: # bibtexparser raises plain Exceptions on bad input
|
|
111
|
+
print(f"citegate: could not parse {path}: {exc}", file=sys.stderr)
|
|
112
|
+
return 2
|
|
113
|
+
|
|
114
|
+
cache = ResultCache(Path(args.cache)) if args.cache else None
|
|
115
|
+
crossref, openalex = make_clients(mailto=args.mailto or None)
|
|
116
|
+
|
|
117
|
+
results: List[Result] = []
|
|
118
|
+
for entry in entries:
|
|
119
|
+
cached = cache.get(entry_cache_key(entry)) if cache else None
|
|
120
|
+
if cached is not None:
|
|
121
|
+
result = result_from_dict(cached)
|
|
122
|
+
result.file = entry.file
|
|
123
|
+
else:
|
|
124
|
+
result = verify_entry(entry, crossref, openalex)
|
|
125
|
+
if cache and result.verdict is not Verdict.ERROR:
|
|
126
|
+
cache.put(entry_cache_key(entry), result)
|
|
127
|
+
results.append(result)
|
|
128
|
+
|
|
129
|
+
if cache:
|
|
130
|
+
cache.save()
|
|
131
|
+
|
|
132
|
+
print_console(results, quiet=args.quiet)
|
|
133
|
+
github_annotations(results, failing)
|
|
134
|
+
github_step_summary(results)
|
|
135
|
+
if args.json:
|
|
136
|
+
write_json(results, args.json)
|
|
137
|
+
|
|
138
|
+
if any(r.verdict in failing for r in results):
|
|
139
|
+
return 1
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
sys.exit(main())
|