papers-cli 2.15.2__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.
Files changed (46) hide show
  1. {papers_cli-2.15.2 → papers_cli-2.15.4}/PKG-INFO +1 -1
  2. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/__main__.py +1 -1
  3. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/_version.py +3 -3
  4. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/bib.py +1 -0
  5. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/extract.py +86 -25
  6. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/filename.py +2 -2
  7. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/PKG-INFO +1 -1
  8. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/SOURCES.txt +0 -1
  9. papers_cli-2.15.2/papers/compat.py +0 -8
  10. {papers_cli-2.15.2 → papers_cli-2.15.4}/.gitattributes +0 -0
  11. {papers_cli-2.15.2 → papers_cli-2.15.4}/.github/FUNDING.yml +0 -0
  12. {papers_cli-2.15.2 → papers_cli-2.15.4}/.github/workflows/pypi.yml +0 -0
  13. {papers_cli-2.15.2 → papers_cli-2.15.4}/.github/workflows/tests.yml +0 -0
  14. {papers_cli-2.15.2 → papers_cli-2.15.4}/.gitignore +0 -0
  15. {papers_cli-2.15.2 → papers_cli-2.15.4}/LICENSE +0 -0
  16. {papers_cli-2.15.2 → papers_cli-2.15.4}/README.md +0 -0
  17. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/__init__.py +0 -0
  18. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/config.py +0 -0
  19. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/duplicate.py +0 -0
  20. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/encoding.py +0 -0
  21. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/latexenc.py +0 -0
  22. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers/utils.py +0 -0
  23. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/dependency_links.txt +0 -0
  24. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/entry_points.txt +0 -0
  25. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/requires.txt +0 -0
  26. {papers_cli-2.15.2 → papers_cli-2.15.4}/papers_cli.egg-info/top_level.txt +0 -0
  27. {papers_cli-2.15.2 → papers_cli-2.15.4}/pyproject.toml +0 -0
  28. {papers_cli-2.15.2 → papers_cli-2.15.4}/requirements.txt +0 -0
  29. {papers_cli-2.15.2 → papers_cli-2.15.4}/setup.cfg +0 -0
  30. {papers_cli-2.15.2 → papers_cli-2.15.4}/setup.py +0 -0
  31. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/__init__.py +0 -0
  32. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/common.py +0 -0
  33. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/config.py +0 -0
  34. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/download.py +0 -0
  35. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/downloadedpapers/.gitignore +0 -0
  36. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
  37. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_add.py +0 -0
  38. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_biblio.py +0 -0
  39. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_duplicates.py +0 -0
  40. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_encoding.py +0 -0
  41. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_extract.py +0 -0
  42. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_filecheck.py +0 -0
  43. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_install.py +0 -0
  44. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_list.py +0 -0
  45. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_misc.py +0 -0
  46. {papers_cli-2.15.2 → papers_cli-2.15.4}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papers-cli
3
- Version: 2.15.2
3
+ Version: 2.15.4
4
4
  Summary: utilities to keep your PDF library organized
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT
@@ -10,6 +10,7 @@ import subprocess as sp
10
10
  import shutil
11
11
  import itertools
12
12
  import fnmatch # unix-like match
13
+ from slugify import slugify
13
14
 
14
15
  import papers
15
16
  from papers import logger
@@ -21,7 +22,6 @@ from papers.duplicate import list_duplicates, list_uniques, edit_entries
21
22
  from papers.bib import (Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck,
22
23
  backupfile as backupfile_func, isvalidkey, DuplicateKeyError, clean_filesdir)
23
24
  from papers.utils import move, checksum, view_pdf, open_folder
24
- from papers.compat import slugify
25
25
  from papers import __version__
26
26
 
27
27
 
@@ -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.2'
32
- __version_tuple__ = version_tuple = (2, 15, 2)
31
+ __version__ = version = '2.15.4'
32
+ __version_tuple__ = version_tuple = (2, 15, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g08670fade'
34
+ __commit_id__ = commit_id = 'g552f2d284'
@@ -430,6 +430,7 @@ class Biblio:
430
430
  f.write(response.content)
431
431
  else:
432
432
  raise ValueError(f"Failed to download PDF from {pdf}")
433
+ kw['rename'] = True # always rename downloaded files
433
434
 
434
435
  if doi:
435
436
  bibtex = fetch_bibtex_by_doi(doi)
@@ -110,43 +110,98 @@ def readpdf_image(pdf, first=None, last=None):
110
110
 
111
111
  return txt
112
112
 
113
- REGEXP = re.compile(r'[doi,doi.org/][\s\.\:]{0,2}(10\.\d{4}[\d\:\.\-\/a-z]+)[A-Z\s,\n]')
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 parse_doi(txt):
117
- # based on: https://doeidoei.wordpress.com/2009/10/22/regular-expression-to-match-a-doi-digital-object-identifier/
118
- # doi = r'[doi|DOI][\s\.\:]{0,2}(10\.\d{4}[\d\:\.\-\/a-z]+)[A-Z\s]'
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
- # maybe try that? (need to convert to python-regex)
121
- # https://www.crossref.org/blog/dois-and-matching-regular-expressions/
122
- # a. /^10.\d{4,9}/[-._;()/:A-Z0-9]+$/i
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
- matches = REGEXP.findall(' '+txt.lower()+' ')
146
+ with fitz.open(pdf_path) as doc:
147
+ metadata = doc.metadata
129
148
 
130
- if not matches:
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
- # try arxiv pattern
133
- match = ARXIV.search(txt.lower())
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
- arxiv_id = match.group(1)
136
- matches = [ f"10.48550/arXiv.{arxiv_id}" ]
190
+ return f"10.48550/arXiv.{match.group(1)}"
137
191
 
138
- else:
139
- raise DOIParsingError('parse_doi::no matches')
192
+ raise DOIParsingError('parse_doi::no matches')
140
193
 
141
- match = matches[0]
194
+ # Use the first match (typically the article's own DOI, not a citation)
195
+ doi = matches[0]
142
196
 
143
- # clean expression
144
- doi = match.replace('\n','').strip('.')
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
- if doi.lower().endswith('.received'):
147
- doi = doi[:-len('.received')]
201
+ # Clean up trailing periods, colons
202
+ doi = doi.rstrip('.:')
148
203
 
149
- # quality check
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
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Key and file name formatting
3
3
  """
4
- from papers.compat import slugify, normalize
4
+ from slugify import slugify
5
5
  from papers.encoding import family_names
6
6
 
7
7
  def listtag(words, maxlength=30, minwordlen=3, n=100, sep='-'):
@@ -69,7 +69,7 @@ def make_template_fields(
69
69
  title = UNKNOWN_TITLE
70
70
  Title = UNKNOWN_TITLE
71
71
  else:
72
- titlewords = normalize(entry["title"]).lower().split()
72
+ titlewords = slugify(entry["title"]).split('-')
73
73
  _titles = listtag(
74
74
  titlewords,
75
75
  n=title_word_num,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papers-cli
3
- Version: 2.15.2
3
+ Version: 2.15.4
4
4
  Summary: utilities to keep your PDF library organized
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT
@@ -12,7 +12,6 @@ papers/__init__.py
12
12
  papers/__main__.py
13
13
  papers/_version.py
14
14
  papers/bib.py
15
- papers/compat.py
16
15
  papers/config.py
17
16
  papers/duplicate.py
18
17
  papers/encoding.py
@@ -1,8 +0,0 @@
1
- # compat
2
- from slugify import slugify
3
- from unidecode import unidecode
4
-
5
- def normalize(text):
6
- if text is None:
7
- return None
8
- return unidecode(text).strip()
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