easybib 0.1.0__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.
- easybib/__init__.py +32 -0
- easybib/__main__.py +5 -0
- easybib/cli.py +127 -0
- easybib/core.py +285 -0
- easybib-0.1.0.dist-info/METADATA +76 -0
- easybib-0.1.0.dist-info/RECORD +9 -0
- easybib-0.1.0.dist-info/WHEEL +5 -0
- easybib-0.1.0.dist-info/entry_points.txt +2 -0
- easybib-0.1.0.dist-info/top_level.txt +1 -0
easybib/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""easybib - Automatically fetch BibTeX entries from INSPIRE and ADS for LaTeX projects."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("easybib")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "unknown"
|
|
9
|
+
|
|
10
|
+
from easybib.core import (
|
|
11
|
+
extract_cite_keys,
|
|
12
|
+
extract_existing_bib_keys,
|
|
13
|
+
fetch_bibtex,
|
|
14
|
+
get_ads_bibtex,
|
|
15
|
+
get_inspire_bibtex,
|
|
16
|
+
is_ads_bibcode,
|
|
17
|
+
is_inspire_key,
|
|
18
|
+
replace_bibtex_key,
|
|
19
|
+
truncate_authors,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"extract_cite_keys",
|
|
24
|
+
"extract_existing_bib_keys",
|
|
25
|
+
"fetch_bibtex",
|
|
26
|
+
"get_ads_bibtex",
|
|
27
|
+
"get_inspire_bibtex",
|
|
28
|
+
"is_ads_bibcode",
|
|
29
|
+
"is_inspire_key",
|
|
30
|
+
"replace_bibtex_key",
|
|
31
|
+
"truncate_authors",
|
|
32
|
+
]
|
easybib/__main__.py
ADDED
easybib/cli.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Command-line interface for easybib."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from easybib.core import (
|
|
8
|
+
extract_cite_keys,
|
|
9
|
+
extract_existing_bib_keys,
|
|
10
|
+
fetch_bibtex,
|
|
11
|
+
replace_bibtex_key,
|
|
12
|
+
truncate_authors,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description="Extract citations and download BibTeX from NASA/ADS"
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("directory", help="Directory containing LaTeX files")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-o", "--output", default="references.bib", help="Output BibTeX file"
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-a",
|
|
26
|
+
"--max-authors",
|
|
27
|
+
type=int,
|
|
28
|
+
default=3,
|
|
29
|
+
help="Maximum number of authors before truncating with 'and others' (default: 3, use 0 for no limit)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-l",
|
|
33
|
+
"--list-keys",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="List citation keys found in LaTeX files and exit (no lookup)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--fresh",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Start from scratch, ignoring existing output file",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-s",
|
|
44
|
+
"--source",
|
|
45
|
+
choices=["ads", "inspire", "auto"],
|
|
46
|
+
default="ads",
|
|
47
|
+
help="Preferred BibTeX source: 'ads' (default), 'inspire', or 'auto' (based on key format)",
|
|
48
|
+
)
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
# Collect all citation keys
|
|
52
|
+
tex_dir = Path(args.directory)
|
|
53
|
+
all_keys = set()
|
|
54
|
+
all_warnings = []
|
|
55
|
+
for tex_file in tex_dir.glob("**/*.tex"):
|
|
56
|
+
keys, warnings = extract_cite_keys(tex_file)
|
|
57
|
+
all_keys.update(keys)
|
|
58
|
+
all_warnings.extend(warnings)
|
|
59
|
+
|
|
60
|
+
# Print warnings for invalid keys
|
|
61
|
+
if all_warnings:
|
|
62
|
+
print("Warnings:")
|
|
63
|
+
for warning in all_warnings:
|
|
64
|
+
print(f" {warning}")
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
print(f"Found {len(all_keys)} unique citation keys")
|
|
68
|
+
|
|
69
|
+
# If --list-keys, print keys and exit
|
|
70
|
+
if args.list_keys:
|
|
71
|
+
for key in sorted(all_keys):
|
|
72
|
+
print(key)
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
# Check for ADS API key (not required if using --source inspire)
|
|
76
|
+
api_key = os.getenv("ADS_API_KEY")
|
|
77
|
+
if not api_key and args.source != "inspire":
|
|
78
|
+
print("Error: ADS_API_KEY environment variable not set")
|
|
79
|
+
print("Get your API key from: https://ui.adsabs.harvard.edu/user/settings/token")
|
|
80
|
+
print("(Or use --source inspire to fetch from INSPIRE without an ADS key)")
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
# Check for existing bib file and determine which keys to fetch
|
|
84
|
+
output_path = Path(args.output)
|
|
85
|
+
existing_content = ""
|
|
86
|
+
if not args.fresh and output_path.exists():
|
|
87
|
+
existing_keys = extract_existing_bib_keys(output_path)
|
|
88
|
+
keys_to_fetch = all_keys - existing_keys
|
|
89
|
+
with open(output_path, "r", encoding="utf-8") as f:
|
|
90
|
+
existing_content = f.read().strip()
|
|
91
|
+
print(f"Found {len(existing_keys)} existing entries in {args.output}")
|
|
92
|
+
print(f"Fetching {len(keys_to_fetch)} new keys")
|
|
93
|
+
else:
|
|
94
|
+
keys_to_fetch = all_keys
|
|
95
|
+
if args.fresh and output_path.exists():
|
|
96
|
+
print(f"Starting fresh (ignoring existing {args.output})")
|
|
97
|
+
|
|
98
|
+
# Download BibTeX entries
|
|
99
|
+
bibtex_entries = []
|
|
100
|
+
not_found = []
|
|
101
|
+
for key in sorted(keys_to_fetch):
|
|
102
|
+
print(f"Fetching {key}...", end=" ")
|
|
103
|
+
bibtex, source = fetch_bibtex(key, api_key, args.source)
|
|
104
|
+
if bibtex:
|
|
105
|
+
bibtex = replace_bibtex_key(bibtex, key)
|
|
106
|
+
bibtex = truncate_authors(bibtex, args.max_authors)
|
|
107
|
+
bibtex_entries.append(bibtex)
|
|
108
|
+
print(f"\u2713 {source}")
|
|
109
|
+
else:
|
|
110
|
+
not_found.append(key)
|
|
111
|
+
print("\u2717 Not found")
|
|
112
|
+
|
|
113
|
+
# Write output (append new entries to existing content)
|
|
114
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
115
|
+
if existing_content and bibtex_entries:
|
|
116
|
+
f.write(existing_content + "\n\n" + "\n\n".join(bibtex_entries))
|
|
117
|
+
elif existing_content:
|
|
118
|
+
f.write(existing_content)
|
|
119
|
+
else:
|
|
120
|
+
f.write("\n\n".join(bibtex_entries))
|
|
121
|
+
|
|
122
|
+
print(f"\nWrote {len(bibtex_entries)} new entries to {args.output}")
|
|
123
|
+
|
|
124
|
+
if not_found:
|
|
125
|
+
print(f"\nCould not find {len(not_found)} keys:")
|
|
126
|
+
for key in not_found:
|
|
127
|
+
print(f" - {key}")
|
easybib/core.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Core functionality for fetching BibTeX entries from INSPIRE and ADS."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_cite_keys(tex_file):
|
|
9
|
+
"""Extract all citation keys from a LaTeX file.
|
|
10
|
+
|
|
11
|
+
Returns a tuple of (keys, warnings) where keys is a list of valid citation keys
|
|
12
|
+
and warnings is a list of warning messages for invalid keys.
|
|
13
|
+
"""
|
|
14
|
+
with open(tex_file, "r", encoding="utf-8") as f:
|
|
15
|
+
content = f.read()
|
|
16
|
+
# Match all citation commands: \cite{}, \citep{}, \citet{}, \citealt{}, \citealp{},
|
|
17
|
+
# \citeauthor{}, \citeyear{}, \Citep{}, \Citet{}, etc.
|
|
18
|
+
# Also handles optional arguments like \citep[e.g.][]{key}
|
|
19
|
+
pattern = r"\\[Cc]ite[a-zA-Z]*(?:\[[^\]]*\])*\{([^}]+)\}"
|
|
20
|
+
matches = re.findall(pattern, content)
|
|
21
|
+
# Split multiple keys in single cite command
|
|
22
|
+
keys = []
|
|
23
|
+
warnings = []
|
|
24
|
+
for match in matches:
|
|
25
|
+
for key in match.split(","):
|
|
26
|
+
key = key.strip()
|
|
27
|
+
if not key:
|
|
28
|
+
warnings.append(f"{tex_file}: Empty citation key found")
|
|
29
|
+
elif ":" not in key:
|
|
30
|
+
warnings.append(f"{tex_file}: Skipping key '{key}' (not an INSPIRE/ADS key)")
|
|
31
|
+
else:
|
|
32
|
+
keys.append(key)
|
|
33
|
+
return keys, warnings
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_inspire_bibtex(key):
|
|
37
|
+
"""Fetch BibTeX directly from INSPIRE for a given INSPIRE key."""
|
|
38
|
+
url = f"https://inspirehep.net/api/literature?q=texkeys:{key}"
|
|
39
|
+
headers = {"Accept": "application/x-bibtex"}
|
|
40
|
+
response = requests.get(url, headers=headers)
|
|
41
|
+
if response.status_code == 200 and response.text.strip():
|
|
42
|
+
return response.text.strip()
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_ads_info_from_inspire(key):
|
|
47
|
+
"""Fetch ADS bibcode and arXiv ID from INSPIRE for a given INSPIRE key.
|
|
48
|
+
|
|
49
|
+
Returns a tuple of (ads_bibcode, arxiv_id), either may be None.
|
|
50
|
+
"""
|
|
51
|
+
# Use texkeys field to avoid colon being interpreted as field operator
|
|
52
|
+
url = f"https://inspirehep.net/api/literature?q=texkeys:{key}"
|
|
53
|
+
headers = {"Accept": "application/json"}
|
|
54
|
+
response = requests.get(url, headers=headers)
|
|
55
|
+
|
|
56
|
+
ads_bibcode = None
|
|
57
|
+
arxiv_id = None
|
|
58
|
+
|
|
59
|
+
if response.status_code == 200:
|
|
60
|
+
data = response.json()
|
|
61
|
+
hits = data.get("hits", {}).get("hits", [])
|
|
62
|
+
if hits:
|
|
63
|
+
metadata = hits[0].get("metadata", {})
|
|
64
|
+
|
|
65
|
+
# Try to get ADS bibcode
|
|
66
|
+
external_ids = metadata.get("external_system_identifiers", [])
|
|
67
|
+
for ext_id in external_ids:
|
|
68
|
+
if ext_id.get("schema") == "ADS":
|
|
69
|
+
ads_bibcode = ext_id.get("value")
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
# Get arXiv ID as fallback
|
|
73
|
+
arxiv_eprints = metadata.get("arxiv_eprints", [])
|
|
74
|
+
if arxiv_eprints:
|
|
75
|
+
arxiv_id = arxiv_eprints[0].get("value")
|
|
76
|
+
|
|
77
|
+
return ads_bibcode, arxiv_id
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def search_ads_by_arxiv(arxiv_id, api_key):
|
|
81
|
+
"""Search ADS for a paper by arXiv ID and return its bibcode."""
|
|
82
|
+
url = "https://api.adsabs.harvard.edu/v1/search/query"
|
|
83
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
84
|
+
params = {"q": f"arXiv:{arxiv_id}", "fl": "bibcode"}
|
|
85
|
+
response = requests.get(url, headers=headers, params=params)
|
|
86
|
+
if response.status_code == 200:
|
|
87
|
+
result = response.json()
|
|
88
|
+
docs = result.get("response", {}).get("docs", [])
|
|
89
|
+
if docs:
|
|
90
|
+
return docs[0].get("bibcode")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_ads_bibtex(bibcode, api_key):
|
|
95
|
+
"""Fetch BibTeX from ADS for a given bibcode."""
|
|
96
|
+
url = "https://api.adsabs.harvard.edu/v1/export/bibtex"
|
|
97
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
98
|
+
data = {"bibcode": [bibcode]}
|
|
99
|
+
response = requests.post(url, headers=headers, json=data)
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
result = response.json()
|
|
102
|
+
export = result.get("export", "").strip()
|
|
103
|
+
if export and not export.startswith("No records"):
|
|
104
|
+
return export
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def extract_existing_bib_keys(bib_file):
|
|
109
|
+
"""Extract citation keys from an existing BibTeX file."""
|
|
110
|
+
if not bib_file.exists():
|
|
111
|
+
return set()
|
|
112
|
+
with open(bib_file, "r", encoding="utf-8") as f:
|
|
113
|
+
content = f.read()
|
|
114
|
+
# Match @type{key,
|
|
115
|
+
pattern = r"@\w+\s*\{\s*([^,\s]+)\s*,"
|
|
116
|
+
return set(re.findall(pattern, content))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def replace_bibtex_key(bibtex, new_key):
|
|
120
|
+
"""Replace the citation key in a BibTeX entry with a new key."""
|
|
121
|
+
# Match the entry type and key: @article{old_key,
|
|
122
|
+
pattern = r"(@\w+\s*\{)\s*([^,\s]+)\s*,"
|
|
123
|
+
return re.sub(pattern, rf"\1{new_key},", bibtex, count=1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def truncate_authors(bibtex, max_authors):
|
|
127
|
+
"""Truncate the author list in a BibTeX entry to max_authors.
|
|
128
|
+
|
|
129
|
+
If there are more than max_authors, keep the first max_authors and add "and others".
|
|
130
|
+
If max_authors is None or 0, no truncation is performed.
|
|
131
|
+
"""
|
|
132
|
+
if not max_authors:
|
|
133
|
+
return bibtex
|
|
134
|
+
|
|
135
|
+
# Match the author field (handles multiline author fields)
|
|
136
|
+
author_pattern = r"(\s*author\s*=\s*\{)(.+?)(\},?\s*\n)"
|
|
137
|
+
match = re.search(author_pattern, bibtex, re.IGNORECASE | re.DOTALL)
|
|
138
|
+
|
|
139
|
+
if not match:
|
|
140
|
+
return bibtex
|
|
141
|
+
|
|
142
|
+
prefix = match.group(1)
|
|
143
|
+
authors_str = match.group(2)
|
|
144
|
+
suffix = match.group(3)
|
|
145
|
+
|
|
146
|
+
# Split authors by " and " (BibTeX standard separator)
|
|
147
|
+
authors = [a.strip() for a in re.split(r"\s+and\s+", authors_str)]
|
|
148
|
+
|
|
149
|
+
if len(authors) <= max_authors:
|
|
150
|
+
return bibtex
|
|
151
|
+
|
|
152
|
+
# Keep first max_authors and add "others"
|
|
153
|
+
truncated_authors = authors[:max_authors] + ["others"]
|
|
154
|
+
new_authors_str = " and ".join(truncated_authors)
|
|
155
|
+
|
|
156
|
+
# Replace the author field
|
|
157
|
+
new_author_field = f"{prefix}{new_authors_str}{suffix}"
|
|
158
|
+
return bibtex[: match.start()] + new_author_field + bibtex[match.end() :]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_ads_bibcode(key):
|
|
162
|
+
"""Check if a key looks like an ADS bibcode (e.g., 2016PhRvL.116f1102A)."""
|
|
163
|
+
# ADS bibcodes are typically 19 characters: 4-digit year + journal code + volume + page + author initial
|
|
164
|
+
# Pattern: YYYYJJJJJVVVVMPPPPA where Y=year, J=journal, V=volume, M=section, P=page, A=author
|
|
165
|
+
ads_pattern = r"^\d{4}[A-Za-z&.]+\..*[A-Z]$"
|
|
166
|
+
return bool(re.match(ads_pattern, key)) and len(key) >= 15
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def is_inspire_key(key):
|
|
170
|
+
"""Check if a key looks like an INSPIRE texkey (e.g., Author:2020abc)."""
|
|
171
|
+
# INSPIRE keys are typically Author:YYYYxxx where xxx is 2-3 lowercase letters
|
|
172
|
+
inspire_pattern = r"^[A-Za-z][A-Za-z0-9-]+:\d{4}[a-z]{2,3}$"
|
|
173
|
+
return bool(re.match(inspire_pattern, key))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def fetch_bibtex_ads_preferred(key, api_key):
|
|
177
|
+
"""Fetch BibTeX preferring ADS, with INSPIRE as fallback."""
|
|
178
|
+
# First check if it's already an ADS bibcode
|
|
179
|
+
if is_ads_bibcode(key):
|
|
180
|
+
bibtex = get_ads_bibtex(key, api_key)
|
|
181
|
+
if bibtex:
|
|
182
|
+
return bibtex, "ADS (direct)"
|
|
183
|
+
|
|
184
|
+
# Try to get ADS bibcode or arXiv ID from INSPIRE
|
|
185
|
+
ads_bibcode, arxiv_id = get_ads_info_from_inspire(key)
|
|
186
|
+
|
|
187
|
+
# Try ADS bibcode first
|
|
188
|
+
if ads_bibcode:
|
|
189
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
190
|
+
if bibtex:
|
|
191
|
+
return bibtex, f"ADS via INSPIRE ({ads_bibcode})"
|
|
192
|
+
|
|
193
|
+
# Fall back to arXiv ID search on ADS
|
|
194
|
+
if arxiv_id:
|
|
195
|
+
ads_bibcode = search_ads_by_arxiv(arxiv_id, api_key)
|
|
196
|
+
if ads_bibcode:
|
|
197
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
198
|
+
if bibtex:
|
|
199
|
+
return bibtex, f"ADS via arXiv ({arxiv_id})"
|
|
200
|
+
|
|
201
|
+
# Try the key directly as ADS bibcode
|
|
202
|
+
bibtex = get_ads_bibtex(key, api_key)
|
|
203
|
+
if bibtex:
|
|
204
|
+
return bibtex, "ADS (direct fallback)"
|
|
205
|
+
|
|
206
|
+
# Final fallback: fetch BibTeX directly from INSPIRE
|
|
207
|
+
bibtex = get_inspire_bibtex(key)
|
|
208
|
+
if bibtex:
|
|
209
|
+
return bibtex, "INSPIRE (fallback)"
|
|
210
|
+
|
|
211
|
+
return None, None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def fetch_bibtex_inspire_preferred(key, api_key):
|
|
215
|
+
"""Fetch BibTeX preferring INSPIRE, with ADS as fallback."""
|
|
216
|
+
# Try INSPIRE first
|
|
217
|
+
bibtex = get_inspire_bibtex(key)
|
|
218
|
+
if bibtex:
|
|
219
|
+
return bibtex, "INSPIRE"
|
|
220
|
+
|
|
221
|
+
# Fall back to ADS
|
|
222
|
+
if is_ads_bibcode(key):
|
|
223
|
+
bibtex = get_ads_bibtex(key, api_key)
|
|
224
|
+
if bibtex:
|
|
225
|
+
return bibtex, "ADS (fallback, direct)"
|
|
226
|
+
|
|
227
|
+
# Try to get ADS bibcode from INSPIRE metadata
|
|
228
|
+
ads_bibcode, arxiv_id = get_ads_info_from_inspire(key)
|
|
229
|
+
if ads_bibcode:
|
|
230
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
231
|
+
if bibtex:
|
|
232
|
+
return bibtex, "ADS (fallback, via INSPIRE)"
|
|
233
|
+
|
|
234
|
+
if arxiv_id:
|
|
235
|
+
ads_bibcode = search_ads_by_arxiv(arxiv_id, api_key)
|
|
236
|
+
if ads_bibcode:
|
|
237
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
238
|
+
if bibtex:
|
|
239
|
+
return bibtex, "ADS (fallback, via arXiv)"
|
|
240
|
+
|
|
241
|
+
return None, None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fetch_bibtex_auto(key, api_key):
|
|
245
|
+
"""Fetch BibTeX using the source that matches the key format."""
|
|
246
|
+
if is_ads_bibcode(key):
|
|
247
|
+
# Key looks like ADS bibcode, prefer ADS
|
|
248
|
+
bibtex = get_ads_bibtex(key, api_key)
|
|
249
|
+
if bibtex:
|
|
250
|
+
return bibtex, "ADS (auto)"
|
|
251
|
+
# Fallback to INSPIRE
|
|
252
|
+
bibtex = get_inspire_bibtex(key)
|
|
253
|
+
if bibtex:
|
|
254
|
+
return bibtex, "INSPIRE (fallback)"
|
|
255
|
+
else:
|
|
256
|
+
# Key looks like INSPIRE key, prefer INSPIRE
|
|
257
|
+
bibtex = get_inspire_bibtex(key)
|
|
258
|
+
if bibtex:
|
|
259
|
+
return bibtex, "INSPIRE (auto)"
|
|
260
|
+
# Fallback to ADS via INSPIRE cross-reference
|
|
261
|
+
ads_bibcode, arxiv_id = get_ads_info_from_inspire(key)
|
|
262
|
+
if ads_bibcode:
|
|
263
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
264
|
+
if bibtex:
|
|
265
|
+
return bibtex, "ADS (fallback, via INSPIRE)"
|
|
266
|
+
if arxiv_id:
|
|
267
|
+
ads_bibcode = search_ads_by_arxiv(arxiv_id, api_key)
|
|
268
|
+
if ads_bibcode:
|
|
269
|
+
bibtex = get_ads_bibtex(ads_bibcode, api_key)
|
|
270
|
+
if bibtex:
|
|
271
|
+
return bibtex, "ADS (fallback, via arXiv)"
|
|
272
|
+
|
|
273
|
+
return None, None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def fetch_bibtex(key, api_key, source="ads"):
|
|
277
|
+
"""Fetch BibTeX using the specified source preference."""
|
|
278
|
+
if source == "ads":
|
|
279
|
+
return fetch_bibtex_ads_preferred(key, api_key)
|
|
280
|
+
elif source == "inspire":
|
|
281
|
+
return fetch_bibtex_inspire_preferred(key, api_key)
|
|
282
|
+
elif source == "auto":
|
|
283
|
+
return fetch_bibtex_auto(key, api_key)
|
|
284
|
+
else:
|
|
285
|
+
return fetch_bibtex_ads_preferred(key, api_key)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easybib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatically fetch BibTeX entries from INSPIRE and ADS for LaTeX projects
|
|
5
|
+
Author-email: Gregory Ashton <gregory.ashton@ligo.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GregoryAshton/easybib
|
|
8
|
+
Project-URL: Repository, https://github.com/GregoryAshton/easybib
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
|
|
13
|
+
# easybib
|
|
14
|
+
|
|
15
|
+
Automatically fetch BibTeX entries from [INSPIRE](https://inspirehep.net/) and [NASA/ADS](https://ui.adsabs.harvard.edu/) for LaTeX projects.
|
|
16
|
+
|
|
17
|
+
easybib scans your `.tex` files for citation keys, looks them up on INSPIRE and/or ADS, and writes a `.bib` file with the results. It handles INSPIRE texkeys (e.g. `Author:2020abc`) and ADS bibcodes (e.g. `2016PhRvL.116f1102A`).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install easybib
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
easybib /path/to/latex/project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This will scan all `.tex` files in the directory, fetch BibTeX entries, and write them to `references.bib`.
|
|
32
|
+
|
|
33
|
+
### Options
|
|
34
|
+
|
|
35
|
+
| Flag | Description |
|
|
36
|
+
|------|-------------|
|
|
37
|
+
| `-o`, `--output` | Output BibTeX file (default: `references.bib`) |
|
|
38
|
+
| `-s`, `--source` | Preferred source: `ads` (default), `inspire`, or `auto` |
|
|
39
|
+
| `-a`, `--max-authors` | Truncate author lists (default: 3, use 0 for no limit) |
|
|
40
|
+
| `-l`, `--list-keys` | List found citation keys and exit (no fetching) |
|
|
41
|
+
| `--fresh` | Ignore existing output file and start from scratch |
|
|
42
|
+
|
|
43
|
+
### Examples
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Fetch from INSPIRE (no API key needed)
|
|
47
|
+
easybib ./paper -s inspire
|
|
48
|
+
|
|
49
|
+
# Use a custom output file
|
|
50
|
+
easybib ./paper -o paper.bib
|
|
51
|
+
|
|
52
|
+
# List citation keys without fetching
|
|
53
|
+
easybib ./paper -l
|
|
54
|
+
|
|
55
|
+
# Keep all authors
|
|
56
|
+
easybib ./paper -a 0
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### ADS API key
|
|
60
|
+
|
|
61
|
+
When using ADS as the source (the default), set your API key:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export ADS_API_KEY="your-key-here"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Get one from https://ui.adsabs.harvard.edu/user/settings/token.
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
1. Scans `.tex` files for `\cite{...}`, `\citep{...}`, `\citet{...}`, and related commands
|
|
72
|
+
2. Filters for keys containing `:` (INSPIRE/ADS format)
|
|
73
|
+
3. Fetches BibTeX from the preferred source, with automatic fallback
|
|
74
|
+
4. Replaces citation keys to match those used in your `.tex` files
|
|
75
|
+
5. Truncates long author lists
|
|
76
|
+
6. Skips keys already present in the output file (use `--fresh` to override)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
easybib/__init__.py,sha256=zCCoB5obi_21h_a_5LK-Zr4G3VkO6XF6Yft5J8pqruU,721
|
|
2
|
+
easybib/__main__.py,sha256=91xpHXOEBRWH-6SL2xquZoAsyDHasxiEPpCK62izpnU,90
|
|
3
|
+
easybib/cli.py,sha256=nU6DC9IEyycbcjNd7hB7G4OBz4V7R12ETlQTlpdrBng,4237
|
|
4
|
+
easybib/core.py,sha256=OvXeVF_jV3FZlrdtcztxlbU4tCnN0Voei3luJPe3V_8,10066
|
|
5
|
+
easybib-0.1.0.dist-info/METADATA,sha256=Ia24a6a3E73u7LxTndNW9TIIkB3QxkGgIAMwbcbpdBc,2314
|
|
6
|
+
easybib-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
easybib-0.1.0.dist-info/entry_points.txt,sha256=IHBiLzF5bW19fQOGvK3e1LKnGcYIL81icdGsvrvZ_Zs,45
|
|
8
|
+
easybib-0.1.0.dist-info/top_level.txt,sha256=Xi0IA9fNmv68UPp2bZCEEIXiQYQy0NweD0J9iqZ-LdQ,8
|
|
9
|
+
easybib-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
easybib
|