papers-cli 2.14__tar.gz → 2.14.1__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.14 → papers_cli-2.14.1}/.github/workflows/tests.yml +0 -1
- {papers_cli-2.14 → papers_cli-2.14.1}/PKG-INFO +4 -4
- {papers_cli-2.14 → papers_cli-2.14.1}/README.md +2 -3
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/__main__.py +28 -1
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/_version.py +2 -2
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/bib.py +6 -8
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/config.py +2 -2
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/duplicate.py +0 -6
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/extract.py +32 -4
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/filename.py +34 -15
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/PKG-INFO +4 -4
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/requires.txt +1 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/pyproject.toml +2 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/requirements.txt +1 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_add.py +2 -2
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_duplicates.py +1 -1
- {papers_cli-2.14 → papers_cli-2.14.1}/.gitattributes +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/.github/FUNDING.yml +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/.gitignore +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/LICENSE +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/__init__.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/encoding.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/latexenc.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers/utils.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/SOURCES.txt +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/setup.cfg +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/setup.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/__init__.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/common.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/config.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/download.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_biblio.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_encoding.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_extract.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_filecheck.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_install.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_list.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_misc.py +0 -0
- {papers_cli-2.14 → papers_cli-2.14.1}/tests/test_undo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: papers-cli
|
|
3
|
-
Version: 2.14
|
|
3
|
+
Version: 2.14.1
|
|
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 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: PyMuPDF
|
|
13
14
|
Requires-Dist: unidecode
|
|
14
15
|
Requires-Dist: crossrefapi
|
|
15
16
|
Requires-Dist: bibtexparser
|
|
@@ -44,7 +45,7 @@ command-line bibliography management tool. Aims:
|
|
|
44
45
|
## Dependencies
|
|
45
46
|
|
|
46
47
|
- python 3.9+
|
|
47
|
-
- [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext
|
|
48
|
+
- [PyMuPDF](https://github.com/pymupdf/PyMuPDF) (preferred) or [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext`; deprecated): convert PDF to text for parsing
|
|
48
49
|
- [bibtexparser](https://bibtexparser.readthedocs.io) : parse bibtex files
|
|
49
50
|
- [crossrefapi](https://github.com/fabiobatalha/crossrefapi) : make polite requests to crossref API
|
|
50
51
|
- [scholarly](https://github.com/OrganicIrradiation/scholarly) : interface for google scholar
|
|
@@ -54,7 +55,6 @@ command-line bibliography management tool. Aims:
|
|
|
54
55
|
## Install
|
|
55
56
|
|
|
56
57
|
- `pip install papers-cli`
|
|
57
|
-
- install third-party dependencies (Ubuntu: `sudo apt install poppler-utils`)
|
|
58
58
|
|
|
59
59
|
Note there is another project registered on pypi as papers, hence `papers-cli` for command-line-interface.
|
|
60
60
|
|
|
@@ -311,7 +311,7 @@ There is also a command specifically designed to clean up the zombie files and f
|
|
|
311
311
|
papers filecheck --clean-filesdir
|
|
312
312
|
|
|
313
313
|
That command will ask before removig anything, unless `--force` is passed. Currently
|
|
314
|
-
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
314
|
+
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
315
315
|
which is the convention `papers` follows to store multiple attachments. That command works best
|
|
316
316
|
when the files are in their own folder, and not mixed up with other things, obviously.
|
|
317
317
|
|
|
@@ -25,7 +25,7 @@ command-line bibliography management tool. Aims:
|
|
|
25
25
|
## Dependencies
|
|
26
26
|
|
|
27
27
|
- python 3.9+
|
|
28
|
-
- [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext
|
|
28
|
+
- [PyMuPDF](https://github.com/pymupdf/PyMuPDF) (preferred) or [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext`; deprecated): convert PDF to text for parsing
|
|
29
29
|
- [bibtexparser](https://bibtexparser.readthedocs.io) : parse bibtex files
|
|
30
30
|
- [crossrefapi](https://github.com/fabiobatalha/crossrefapi) : make polite requests to crossref API
|
|
31
31
|
- [scholarly](https://github.com/OrganicIrradiation/scholarly) : interface for google scholar
|
|
@@ -35,7 +35,6 @@ command-line bibliography management tool. Aims:
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
37
37
|
- `pip install papers-cli`
|
|
38
|
-
- install third-party dependencies (Ubuntu: `sudo apt install poppler-utils`)
|
|
39
38
|
|
|
40
39
|
Note there is another project registered on pypi as papers, hence `papers-cli` for command-line-interface.
|
|
41
40
|
|
|
@@ -292,7 +291,7 @@ There is also a command specifically designed to clean up the zombie files and f
|
|
|
292
291
|
papers filecheck --clean-filesdir
|
|
293
292
|
|
|
294
293
|
That command will ask before removig anything, unless `--force` is passed. Currently
|
|
295
|
-
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
294
|
+
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
296
295
|
which is the convention `papers` follows to store multiple attachments. That command works best
|
|
297
296
|
when the files are in their own folder, and not mixed up with other things, obviously.
|
|
298
297
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""That is the script called by the papers cli command
|
|
2
2
|
"""
|
|
3
3
|
import os
|
|
4
|
+
import copy
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import logging
|
|
@@ -80,6 +81,8 @@ def _backup_bib(biblio, config, message=None):
|
|
|
80
81
|
shutil.copy(config.bibtex, config.backupfile)
|
|
81
82
|
config.gitcmd(f"add {config.backupfile.name}")
|
|
82
83
|
|
|
84
|
+
biblio = copy.deepcopy(biblio)
|
|
85
|
+
|
|
83
86
|
if config.backup_files:
|
|
84
87
|
logger.info('backup bibliography with files')
|
|
85
88
|
backupfilesdir = backupdir/"files"
|
|
@@ -561,6 +564,7 @@ def addcmd(parser, o, config):
|
|
|
561
564
|
set_keyformat_config_from_cmd(o, config)
|
|
562
565
|
|
|
563
566
|
biblio = get_biblio(config)
|
|
567
|
+
biblio_init = copy.deepcopy(biblio)
|
|
564
568
|
|
|
565
569
|
entries = []
|
|
566
570
|
|
|
@@ -650,10 +654,33 @@ def addcmd(parser, o, config):
|
|
|
650
654
|
return
|
|
651
655
|
|
|
652
656
|
entries = [{k:v for k,v in e.items() if v != ""} for e in entries]
|
|
653
|
-
biblio.db.entries = otherentries + entries
|
|
657
|
+
biblio.db.entries = sorted(otherentries + entries, key=lambda e: biblio.key(e))
|
|
654
658
|
|
|
655
659
|
savebib(biblio, config)
|
|
656
660
|
|
|
661
|
+
# compare entries to inform user
|
|
662
|
+
# this is not very efficient but I have yet to hear a complaint about speed
|
|
663
|
+
|
|
664
|
+
old_set = set(e["ID"] for e in biblio_init.entries)
|
|
665
|
+
old_entries_by_key = {e["ID"]:e for e in biblio_init.db.entries}
|
|
666
|
+
new_set = set(e["ID"] for e in biblio.entries)
|
|
667
|
+
new_entries_by_key = {e["ID"]:e for e in biblio.db.entries}
|
|
668
|
+
modified_set = set(e["ID"] for e in entries ).intersection(set.intersection(old_set, new_set))
|
|
669
|
+
|
|
670
|
+
for ID in sorted(old_set - new_set):
|
|
671
|
+
print(format_entry(biblio_init, old_entries_by_key[ID], prefix="Removed"))
|
|
672
|
+
|
|
673
|
+
for ID in sorted(new_set - old_set):
|
|
674
|
+
print(format_entry(biblio, new_entries_by_key[ID], prefix="Added"))
|
|
675
|
+
|
|
676
|
+
for ID in sorted(modified_set):
|
|
677
|
+
before = old_entries_by_key[ID]
|
|
678
|
+
after = new_entries_by_key[ID]
|
|
679
|
+
if before != after:
|
|
680
|
+
print(format_entry(biblio, after, prefix="Modified"))
|
|
681
|
+
else:
|
|
682
|
+
print(format_entry(biblio, after, prefix="Existing"))
|
|
683
|
+
|
|
657
684
|
if o.open:
|
|
658
685
|
for e in entries:
|
|
659
686
|
view_entry_files(biblio, e)
|
|
@@ -273,7 +273,6 @@ class Biblio:
|
|
|
273
273
|
keys = [self.key(ei) for ei in self.db.entries]
|
|
274
274
|
return bisect.bisect_left(keys, self.key(entry))
|
|
275
275
|
|
|
276
|
-
|
|
277
276
|
def insert_entry(self, entry, update_key=False, check_duplicate=False, rename=False, copy=False, metadata={}, **checkopt):
|
|
278
277
|
"""
|
|
279
278
|
"""
|
|
@@ -296,20 +295,21 @@ class Biblio:
|
|
|
296
295
|
if i < len(self.entries) and self.key(self.entries[i]) == self.key(entry):
|
|
297
296
|
logger.info('key duplicate: '+self.key(self.entries[i]))
|
|
298
297
|
|
|
298
|
+
if self.entries[i]["title"] == entry["title"]:
|
|
299
|
+
logger.info('exact duplicate')
|
|
300
|
+
return [ self.entries[i] ]
|
|
301
|
+
|
|
299
302
|
if update_key:
|
|
300
303
|
newkey = self.append_abc_to_key(entry) # add abc
|
|
301
304
|
logger.info('update key: {} => {}'.format(entry['ID'], newkey))
|
|
302
305
|
entry['ID'] = newkey
|
|
303
306
|
|
|
304
307
|
else:
|
|
305
|
-
print(format_entry(self, entry, prefix="New entry"))
|
|
306
|
-
print(format_entry(self, self.entries[i], prefix="Conflicts with"))
|
|
307
308
|
raise DuplicateKeyError('Key conflict. Try update_key is True or specify --key KEY explicitly')
|
|
308
309
|
|
|
309
310
|
else:
|
|
310
311
|
logger.info('new entry: '+self.key(entry))
|
|
311
312
|
|
|
312
|
-
print(format_entry(self, entry, prefix="Added"))
|
|
313
313
|
self.entries.insert(i, entry)
|
|
314
314
|
|
|
315
315
|
if rename: self.rename_entry_files(entry, copy=copy)
|
|
@@ -338,7 +338,6 @@ class Biblio:
|
|
|
338
338
|
if entry == candidate:
|
|
339
339
|
logger.debug('exact duplicate')
|
|
340
340
|
if rename: self.rename_entry_files(candidate, copy=copy)
|
|
341
|
-
print(format_entry(self, entry, prefix="Existing"))
|
|
342
341
|
return [ entry ] # do nothing
|
|
343
342
|
|
|
344
343
|
if update_key and entry['ID'] != candidate['ID']:
|
|
@@ -357,7 +356,6 @@ class Biblio:
|
|
|
357
356
|
logger.debug('fixed: exact duplicate')
|
|
358
357
|
entry = candidate
|
|
359
358
|
if rename: self.rename_entry_files(candidate, copy=copy)
|
|
360
|
-
print(format_entry(self, entry, prefix="Existing"))
|
|
361
359
|
return [ entry ] # do nothing
|
|
362
360
|
|
|
363
361
|
logger.debug('conflict resolution: '+on_conflict)
|
|
@@ -438,10 +436,10 @@ class Biblio:
|
|
|
438
436
|
files += attachments
|
|
439
437
|
|
|
440
438
|
self.set_files(entry, [os.path.abspath(f) for f in files])
|
|
441
|
-
entry['ID'] = self.
|
|
439
|
+
entry['ID'] = self.keyformat(entry)
|
|
442
440
|
logger.debug('generated PDF key: '+entry['ID'])
|
|
443
441
|
|
|
444
|
-
kw["update_key"] =
|
|
442
|
+
kw["update_key"] = True
|
|
445
443
|
return self.insert_entry(entry, **kw)
|
|
446
444
|
|
|
447
445
|
|
|
@@ -143,9 +143,9 @@ class Config:
|
|
|
143
143
|
def load(cls, the_file):
|
|
144
144
|
js = json.load(open(the_file))
|
|
145
145
|
if 'nameformat' in js:
|
|
146
|
-
js['nameformat'] = Format(**js.get('nameformat'))
|
|
146
|
+
js['nameformat'] = Format(**{**vars(NAMEFORMAT), **js.get('nameformat')})
|
|
147
147
|
if 'keyformat' in js:
|
|
148
|
-
js['keyformat'] = Format(**js.get('keyformat'))
|
|
148
|
+
js['keyformat'] = Format(**{**vars(KEYFORMAT), **js.get('keyformat')})
|
|
149
149
|
cfg = cls(file=the_file, **js)
|
|
150
150
|
cfg._update_paths_to_absolute()
|
|
151
151
|
return cfg
|
|
@@ -367,12 +367,6 @@ def edit_entries(entries, diff=False, ndiff=False, editor=None):
|
|
|
367
367
|
if res == 0:
|
|
368
368
|
logger.info('sucessfully edited file, insert edited entries')
|
|
369
369
|
db_updated = bibtexparser.loads(open(filename).read())
|
|
370
|
-
keys_before = [e.get('ID') for e in entries]
|
|
371
|
-
keys_after = [e.get('ID') for e in db_updated.entries]
|
|
372
|
-
if set(keys_before) != set(keys_after):
|
|
373
|
-
print(f"{bcolors.WARNING}Keys edited ->", bcolors.OKBLUE ," ".join(sorted(set(keys_before).difference(keys_after))), bcolors.ENDC,
|
|
374
|
-
"=>", bcolors.OKBLUE,
|
|
375
|
-
" ".join(sorted(set(keys_after).difference(keys_before))), bcolors.ENDC) if keys_after else '(deleted)'
|
|
376
370
|
return db_updated.entries
|
|
377
371
|
|
|
378
372
|
else:
|
|
@@ -26,12 +26,28 @@ class DOIRequestError(ValueError):
|
|
|
26
26
|
|
|
27
27
|
# PDF parsing / crossref requests
|
|
28
28
|
# ===============================
|
|
29
|
+
def readpdf_fitz(pdf_path, pages=None, first=None, last=None):
|
|
30
|
+
import fitz
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# Open the PDF file
|
|
33
|
+
document = fitz.open(pdf_path)
|
|
34
|
+
text = ""
|
|
35
|
+
|
|
36
|
+
# Iterate through the pages
|
|
37
|
+
for page_num in range(len(document)):
|
|
38
|
+
if pages is not None and page_num+1 not in pages:
|
|
39
|
+
continue
|
|
40
|
+
elif first is not None and page_num+1 < first:
|
|
41
|
+
continue
|
|
42
|
+
elif last is not None and page_num+1 > last:
|
|
43
|
+
continue
|
|
44
|
+
page = document.load_page(page_num)
|
|
45
|
+
text += page.get_text()
|
|
46
|
+
|
|
47
|
+
return text
|
|
34
48
|
|
|
49
|
+
def readpdf_poputils(pdf, first=None, last=None, pages=None):
|
|
50
|
+
# DEPRECATED
|
|
35
51
|
tmptxt = tempfile.mktemp(suffix='.txt')
|
|
36
52
|
|
|
37
53
|
cmd = ['pdftotext']
|
|
@@ -46,6 +62,18 @@ def readpdf(pdf, first=None, last=None):
|
|
|
46
62
|
|
|
47
63
|
return txt
|
|
48
64
|
|
|
65
|
+
def readpdf(pdf, first=None, last=None):
|
|
66
|
+
# TODO the python package pdftotext can do this directly, with no temp file and no I/O.
|
|
67
|
+
if not os.path.isfile(pdf):
|
|
68
|
+
raise ValueError(repr(pdf) + ": not a file")
|
|
69
|
+
try:
|
|
70
|
+
return readpdf_fitz(pdf, first=first, last=last)
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.warning("PyMuPDF not installed, using pdftotext")
|
|
73
|
+
return readpdf_fitz(pdf, first=first, last=last)
|
|
74
|
+
|
|
75
|
+
return txt
|
|
76
|
+
|
|
49
77
|
|
|
50
78
|
def readpdf_image(pdf, first=None, last=None):
|
|
51
79
|
|
|
@@ -22,6 +22,10 @@ def _cite_author(names):
|
|
|
22
22
|
else:
|
|
23
23
|
return names[0]
|
|
24
24
|
|
|
25
|
+
UNKNOWN_AUTHOR = 'unknown'
|
|
26
|
+
UNKNOWN_YEAR = '0000'
|
|
27
|
+
UNKNOWN_JOURNAL = None
|
|
28
|
+
UNKNOWN_TITLE = ""
|
|
25
29
|
|
|
26
30
|
def make_template_fields(
|
|
27
31
|
entry,
|
|
@@ -31,6 +35,7 @@ def make_template_fields(
|
|
|
31
35
|
title_length=100,
|
|
32
36
|
author_sep="_",
|
|
33
37
|
title_sep="-",
|
|
38
|
+
**ignore,
|
|
34
39
|
):
|
|
35
40
|
"""
|
|
36
41
|
Available fields in output are explicitly listed here, and this is the single source of truth for these.
|
|
@@ -43,10 +48,12 @@ def make_template_fields(
|
|
|
43
48
|
- Title: same as title by with capitalized words
|
|
44
49
|
- year
|
|
45
50
|
- ID : bibtex key
|
|
51
|
+
- doi : bibtex DOI
|
|
52
|
+
- doi- : slugified doi (for use in filenames)
|
|
46
53
|
Each one of these needs a specific, explicit assignment below.
|
|
47
54
|
"""
|
|
48
55
|
# names = bibtexparser.customization.getnames(entry.get('author','unknown').lower().split(' and '))
|
|
49
|
-
_names = family_names(entry.get("author",
|
|
56
|
+
_names = family_names(entry.get("author", UNKNOWN_AUTHOR).lower())
|
|
50
57
|
_names = [slugify(nm) for nm in _names]
|
|
51
58
|
author = author_sep.join([nm for nm in _names[:author_num]])
|
|
52
59
|
Author = author_sep.join([nm.capitalize() for nm in _names[:author_num]])
|
|
@@ -54,13 +61,13 @@ def make_template_fields(
|
|
|
54
61
|
authorX = AuthorX.lower()
|
|
55
62
|
|
|
56
63
|
# a thing that's not a bibtex article won't have a journal
|
|
57
|
-
journal = entry.get("journal",
|
|
64
|
+
journal = entry.get("journal", UNKNOWN_JOURNAL)
|
|
58
65
|
|
|
59
|
-
year = str(entry.get("year",
|
|
66
|
+
year = str(entry.get("year", UNKNOWN_YEAR))
|
|
60
67
|
|
|
61
68
|
if not title_word_num or not entry.get("title", ""):
|
|
62
|
-
title =
|
|
63
|
-
Title =
|
|
69
|
+
title = UNKNOWN_TITLE
|
|
70
|
+
Title = UNKNOWN_TITLE
|
|
64
71
|
else:
|
|
65
72
|
titlewords = normalize(entry["title"]).lower().split()
|
|
66
73
|
_titles = listtag(
|
|
@@ -84,15 +91,12 @@ def make_template_fields(
|
|
|
84
91
|
"title": title,
|
|
85
92
|
"Title": Title,
|
|
86
93
|
"ID": entry.get("ID"),
|
|
94
|
+
"doi": entry.get("doi"),
|
|
95
|
+
"doi_": slugify(entry.get("doi", "")),
|
|
96
|
+
"doi_or_id": slugify(entry.get("doi", entry.get("ID", ""))),
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
|
|
90
|
-
def stringify_entry(entry, template, **opt):
|
|
91
|
-
fields = make_template_fields(entry, **opt)
|
|
92
|
-
res = template.format(**fields)
|
|
93
|
-
return res
|
|
94
|
-
|
|
95
|
-
|
|
96
100
|
class Format:
|
|
97
101
|
"""
|
|
98
102
|
Store formatting info as python template formatted with str.format() method. See make_template_fields for available fields.
|
|
@@ -113,7 +117,8 @@ class Format:
|
|
|
113
117
|
|
|
114
118
|
"""
|
|
115
119
|
# def __init__(self, template="{author}{year}{title}", author_num=2, title_word_num=5, author_sep="_", title_sep="-")
|
|
116
|
-
def __init__(self, template, author_num=2, title_word_num=100, title_word_size=1, title_length=100,
|
|
120
|
+
def __init__(self, template, author_num=2, title_word_num=100, title_word_size=1, title_length=100,
|
|
121
|
+
author_sep="_", title_sep="-", unknown_strict=False, unknown_template=None):
|
|
117
122
|
self.template = template
|
|
118
123
|
self.author_num = author_num
|
|
119
124
|
self.author_sep = author_sep
|
|
@@ -121,16 +126,30 @@ class Format:
|
|
|
121
126
|
self.title_sep = title_sep
|
|
122
127
|
self.title_word_num = title_word_num
|
|
123
128
|
self.title_word_size = title_word_size
|
|
129
|
+
self.unknown_strict = unknown_strict
|
|
130
|
+
self.unknown_template = unknown_template
|
|
124
131
|
|
|
125
132
|
def todict(self):
|
|
126
133
|
return vars(self)
|
|
127
134
|
|
|
135
|
+
def is_unknown(self, entry):
|
|
136
|
+
conditions = ( entry.get("author", UNKNOWN_AUTHOR) == UNKNOWN_AUTHOR, entry.get("year", UNKNOWN_YEAR) == UNKNOWN_YEAR, entry.get("title", UNKNOWN_TITLE) == UNKNOWN_TITLE)
|
|
137
|
+
if self.unknown_strict:
|
|
138
|
+
return all(conditions)
|
|
139
|
+
else:
|
|
140
|
+
return any(conditions)
|
|
141
|
+
|
|
128
142
|
def render(self, **entry):
|
|
129
|
-
|
|
143
|
+
fields = make_template_fields(entry, **vars(self))
|
|
144
|
+
|
|
145
|
+
if self.unknown_template and self.is_unknown(entry):
|
|
146
|
+
return self.unknown_template.format(**fields)
|
|
147
|
+
|
|
148
|
+
return self.template.format(**fields)
|
|
130
149
|
|
|
131
150
|
def __call__(self, entry):
|
|
132
151
|
return self.render(**entry)
|
|
133
152
|
|
|
134
153
|
|
|
135
|
-
KEYFORMAT = Format(template='{author}{year}', author_num=2, author_sep="_")
|
|
136
|
-
NAMEFORMAT = Format(template='{authorX}_{year}_{title}', author_sep="_", title_sep="-")
|
|
154
|
+
KEYFORMAT = Format(template='{author}{year}', author_num=2, author_sep="_", unknown_template="{doi}")
|
|
155
|
+
NAMEFORMAT = Format(template='{authorX}_{year}_{title}', author_sep="_", title_sep="-", unknown_template="unknown_{doi_or_id}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: papers-cli
|
|
3
|
-
Version: 2.14
|
|
3
|
+
Version: 2.14.1
|
|
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 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: PyMuPDF
|
|
13
14
|
Requires-Dist: unidecode
|
|
14
15
|
Requires-Dist: crossrefapi
|
|
15
16
|
Requires-Dist: bibtexparser
|
|
@@ -44,7 +45,7 @@ command-line bibliography management tool. Aims:
|
|
|
44
45
|
## Dependencies
|
|
45
46
|
|
|
46
47
|
- python 3.9+
|
|
47
|
-
- [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext
|
|
48
|
+
- [PyMuPDF](https://github.com/pymupdf/PyMuPDF) (preferred) or [poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software)) (only:`pdftotext`; deprecated): convert PDF to text for parsing
|
|
48
49
|
- [bibtexparser](https://bibtexparser.readthedocs.io) : parse bibtex files
|
|
49
50
|
- [crossrefapi](https://github.com/fabiobatalha/crossrefapi) : make polite requests to crossref API
|
|
50
51
|
- [scholarly](https://github.com/OrganicIrradiation/scholarly) : interface for google scholar
|
|
@@ -54,7 +55,6 @@ command-line bibliography management tool. Aims:
|
|
|
54
55
|
## Install
|
|
55
56
|
|
|
56
57
|
- `pip install papers-cli`
|
|
57
|
-
- install third-party dependencies (Ubuntu: `sudo apt install poppler-utils`)
|
|
58
58
|
|
|
59
59
|
Note there is another project registered on pypi as papers, hence `papers-cli` for command-line-interface.
|
|
60
60
|
|
|
@@ -311,7 +311,7 @@ There is also a command specifically designed to clean up the zombie files and f
|
|
|
311
311
|
papers filecheck --clean-filesdir
|
|
312
312
|
|
|
313
313
|
That command will ask before removig anything, unless `--force` is passed. Currently
|
|
314
|
-
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
314
|
+
it ignores hidden files and folders, and will only consider folder that have a `.{folder}.bib` file inside,
|
|
315
315
|
which is the convention `papers` follows to store multiple attachments. That command works best
|
|
316
316
|
when the files are in their own folder, and not mixed up with other things, obviously.
|
|
317
317
|
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
+
"PyMuPDF",
|
|
19
20
|
"unidecode",
|
|
20
21
|
"crossrefapi",
|
|
21
22
|
"bibtexparser",
|
|
@@ -58,6 +59,7 @@ python =
|
|
|
58
59
|
commands = pytest --cov=papers --cov-append --cov-report=term-missing {posargs} -xv
|
|
59
60
|
deps =
|
|
60
61
|
bibtexparser < 2.0.0
|
|
62
|
+
PyMuPDF
|
|
61
63
|
scholarly
|
|
62
64
|
crossrefapi
|
|
63
65
|
rapidfuzz
|
|
@@ -305,7 +305,7 @@ class TestAddConflict(BibTest):
|
|
|
305
305
|
title = {Something else entirely}
|
|
306
306
|
}"""
|
|
307
307
|
|
|
308
|
-
bibtex_conflict_key_fixed = """@article{
|
|
308
|
+
bibtex_conflict_key_fixed = """@article{10.5194/bg-8-515-2011XXX,
|
|
309
309
|
author = {M. Perrette and Another author},
|
|
310
310
|
doi = {10.5194/bg-8-515-2011XXX},
|
|
311
311
|
title = {Something else entirely}
|
|
@@ -416,7 +416,7 @@ class TestAddConflict(BibTest):
|
|
|
416
416
|
# key conflict and update entry
|
|
417
417
|
open(self.otherbib, 'w').write(self.bibtex_conflict_key)
|
|
418
418
|
paperscmd(f'add {self.otherbib} --bibtex {self.mybib} -u')
|
|
419
|
-
expected = self.
|
|
419
|
+
expected = self.bibtex_conflict_key_fixed+'\n\n'+self.bibtex
|
|
420
420
|
self.assertMultiLineEqual(open(self.mybib).read().strip(), expected) # entries did not change
|
|
421
421
|
|
|
422
422
|
def test_add_same_doi_unchecked(self):
|
|
@@ -266,7 +266,7 @@ class TestAddResolveDuplicate(BibTest):
|
|
|
266
266
|
|
|
267
267
|
def test_conflict_updated_from_original_but_originalkey(self):
|
|
268
268
|
|
|
269
|
-
expected = """@article{
|
|
269
|
+
expected = """@article{10.5194/bg-8-515-2011,
|
|
270
270
|
author = {New Author Field},
|
|
271
271
|
doi = {10.5194/bg-8-515-2011},
|
|
272
272
|
journal = {ConflictJournal},
|
|
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
|