papers-cli 2.15.3__tar.gz → 2.15.4__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.
- {papers_cli-2.15.3 → papers_cli-2.15.4}/PKG-INFO +1 -1
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/_version.py +3 -3
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/extract.py +86 -25
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/PKG-INFO +1 -1
- {papers_cli-2.15.3 → papers_cli-2.15.4}/.gitattributes +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/.github/FUNDING.yml +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/.github/workflows/tests.yml +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/.gitignore +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/LICENSE +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/README.md +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/__init__.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/__main__.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/bib.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/config.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/duplicate.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/encoding.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/filename.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/latexenc.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers/utils.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/SOURCES.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/requires.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/pyproject.toml +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/requirements.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/setup.cfg +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/setup.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/__init__.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/common.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/config.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/download.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_add.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_biblio.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_duplicates.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_encoding.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_extract.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_filecheck.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_install.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_list.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_misc.py +0 -0
- {papers_cli-2.15.3 → papers_cli-2.15.4}/tests/test_undo.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.15.
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 15,
|
|
31
|
+
__version__ = version = '2.15.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 15, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g552f2d284'
|
|
@@ -110,43 +110,98 @@ def readpdf_image(pdf, first=None, last=None):
|
|
|
110
110
|
|
|
111
111
|
return txt
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
# CrossRef DOI standard: /^10.\d{4,9}/[-._;()/:A-Z0-9]+$/i
|
|
114
|
+
REGEXP = re.compile(r'[doi,doi.org/][\s\.\:]{0,2}(10\.\d{4,9}/[-._;()/:a-z0-9]+)')
|
|
114
115
|
ARXIV = re.compile(r'arxiv:\s*(\d{4}\.\d{4,5})')
|
|
115
116
|
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
def _parse_doi_from_metadata_string(metadata):
|
|
118
|
+
"""Extract DOI from XMP metadata string."""
|
|
119
|
+
patterns = [
|
|
120
|
+
r'<prism:doi>(10\.\d{4,9}/[-._;()/:a-z0-9]+)</prism:doi>',
|
|
121
|
+
r'<dc:identifier>doi:(10\.\d{4,9}/[-._;()/:a-z0-9]+)</dc:identifier>',
|
|
122
|
+
r'<pdfx:doi>(10\.\d{4,9}/[-._;()/:a-z0-9]+)</pdfx:doi>',
|
|
123
|
+
r'<crossmark:DOI>(10\.\d{4,9}/[-._;()/:a-z0-9]+)</crossmark:DOI>',
|
|
124
|
+
]
|
|
125
|
+
for pattern in patterns:
|
|
126
|
+
match = re.search(pattern, metadata, re.IGNORECASE)
|
|
127
|
+
if match:
|
|
128
|
+
return match.group(1)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def parse_doi_from_pdf_metadata_poppler(pdf_path):
|
|
132
|
+
"""Extract DOI from PDF metadata using pdfinfo (poppler-utils)."""
|
|
133
|
+
try:
|
|
134
|
+
result = sp.run(['pdfinfo', '-meta', pdf_path],
|
|
135
|
+
capture_output=True, text=True, timeout=5)
|
|
136
|
+
if result.returncode == 0:
|
|
137
|
+
return _parse_doi_from_metadata_string(result.stdout)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
return None
|
|
119
141
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# b. /^10.1002/[^\s]+$/i
|
|
124
|
-
# c. /^10.\d{4}/\d+-\d+X?(\d+)\d+<[\d\w]+:[\d\w]*>\d+.\d+.\w+;\d$/i
|
|
125
|
-
# d. /^10.1021/\w\w\d++$/i
|
|
126
|
-
# e. /^10.1207/[\w\d]+\&\d+_\d+$/i
|
|
142
|
+
def parse_doi_from_pdf_metadata_fitz(pdf_path):
|
|
143
|
+
"""Extract DOI from PDF metadata using PyMuPDF/fitz."""
|
|
144
|
+
import fitz
|
|
127
145
|
|
|
128
|
-
|
|
146
|
+
with fitz.open(pdf_path) as doc:
|
|
147
|
+
metadata = doc.metadata
|
|
129
148
|
|
|
130
|
-
|
|
149
|
+
# Try direct metadata fields first
|
|
150
|
+
if metadata:
|
|
151
|
+
# Check common metadata fields
|
|
152
|
+
for key in ['subject', 'keywords', 'title']:
|
|
153
|
+
value = metadata.get(key, '')
|
|
154
|
+
if value and '10.' in value:
|
|
155
|
+
# Try to extract DOI from the field
|
|
156
|
+
doi_match = re.search(r'10\.\d{4,9}/[-._;()/:a-z0-9]+', value, re.IGNORECASE)
|
|
157
|
+
if doi_match:
|
|
158
|
+
return doi_match.group(0)
|
|
159
|
+
|
|
160
|
+
# Fall back to XMP metadata
|
|
161
|
+
xmp = doc.get_xml_metadata() if hasattr(doc, 'get_xml_metadata') else doc.xref_get_key(-1, "Metadata")
|
|
162
|
+
if xmp:
|
|
163
|
+
xmp_str = xmp if isinstance(xmp, str) else xmp.decode('utf-8', errors='ignore')
|
|
164
|
+
return _parse_doi_from_metadata_string(xmp_str)
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
|
|
167
|
+
def parse_doi_from_pdf_metadata(pdf_path):
|
|
168
|
+
"""Extract DOI from PDF metadata (tries fitz, falls back to poppler)."""
|
|
169
|
+
# Try fitz first (no subprocess overhead)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
return parse_doi_from_pdf_metadata_fitz(pdf_path)
|
|
173
|
+
except ImportError:
|
|
174
|
+
# Fall back to poppler-utils
|
|
175
|
+
return parse_doi_from_pdf_metadata_poppler(pdf_path)
|
|
176
|
+
|
|
177
|
+
def parse_doi(txt):
|
|
178
|
+
# PDF extraction sometimes splits DOIs across lines - join them back
|
|
179
|
+
# e.g., "10.1073/pnas.\n2511370123" -> "10.1073/pnas.2511370123"
|
|
180
|
+
# Also remove invisible Unicode characters that can appear in PDFs
|
|
181
|
+
txt_clean = txt.replace('\u200b', '').replace('\xad', '') # zero-width space, soft hyphen
|
|
182
|
+
# Handle both direct newlines and space+newline patterns
|
|
183
|
+
txt_normalized = re.sub(r'([a-z0-9\.\-_/])\s*\n\s*([a-z0-9])', r'\1\2', txt_clean.lower())
|
|
184
|
+
matches = REGEXP.findall(txt_normalized)
|
|
185
|
+
|
|
186
|
+
if not matches:
|
|
187
|
+
# Try arxiv pattern
|
|
188
|
+
match = ARXIV.search(txt_normalized)
|
|
134
189
|
if match:
|
|
135
|
-
|
|
136
|
-
matches = [ f"10.48550/arXiv.{arxiv_id}" ]
|
|
190
|
+
return f"10.48550/arXiv.{match.group(1)}"
|
|
137
191
|
|
|
138
|
-
|
|
139
|
-
raise DOIParsingError('parse_doi::no matches')
|
|
192
|
+
raise DOIParsingError('parse_doi::no matches')
|
|
140
193
|
|
|
141
|
-
match
|
|
194
|
+
# Use the first match (typically the article's own DOI, not a citation)
|
|
195
|
+
doi = matches[0]
|
|
142
196
|
|
|
143
|
-
#
|
|
144
|
-
|
|
197
|
+
# Remove common non-DOI suffixes (may be chained)
|
|
198
|
+
# These can include path segments, document sections, or text that got concatenated
|
|
199
|
+
doi = re.sub(r'(\.?((/-/dcsupplemental)|(/-/dc)|received|published|\.pdf|\.full|\.abstract|reference:|www\.[a-z]+\.[a-z]+|[0-9]+\.introduction))+$', '', doi, flags=re.IGNORECASE)
|
|
145
200
|
|
|
146
|
-
|
|
147
|
-
|
|
201
|
+
# Clean up trailing periods, colons
|
|
202
|
+
doi = doi.rstrip('.:')
|
|
148
203
|
|
|
149
|
-
#
|
|
204
|
+
# Quality check
|
|
150
205
|
if len(doi) <= 8:
|
|
151
206
|
raise DOIParsingError('failed to extract doi: '+doi)
|
|
152
207
|
|
|
@@ -178,6 +233,12 @@ def pdfhead(pdf, maxpages=10, minwords=200, image=False):
|
|
|
178
233
|
|
|
179
234
|
|
|
180
235
|
def extract_pdf_doi(pdf, image=False):
|
|
236
|
+
# Try PDF metadata first (fast and reliable for many publishers)
|
|
237
|
+
metadata_doi = parse_doi_from_pdf_metadata(pdf)
|
|
238
|
+
if metadata_doi:
|
|
239
|
+
return metadata_doi
|
|
240
|
+
|
|
241
|
+
# Fall back to text extraction if metadata doesn't have DOI
|
|
181
242
|
return parse_doi(pdfhead(pdf, image=image))
|
|
182
243
|
|
|
183
244
|
|
|
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
|
|
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
|