papers-cli 2.6.1__tar.gz → 2.7__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.6.1 → papers_cli-2.7}/PKG-INFO +1 -1
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/__main__.py +61 -12
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/_version.py +2 -2
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/bib.py +23 -15
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/duplicate.py +1 -1
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/PKG-INFO +1 -1
- {papers_cli-2.6.1 → papers_cli-2.7}/.gitattributes +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/.github/workflows/tests.yml +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/.gitignore +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/LICENSE +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/README.md +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/__init__.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/config.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/encoding.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/extract.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/filename.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/latexenc.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers/utils.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/SOURCES.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/requires.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/pyproject.toml +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/requirements.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/setup.cfg +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/setup.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/__init__.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/common.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/config.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/download.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_add.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_biblio.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_duplicates.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_encoding.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_extract.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_filecheck.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_install.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_list.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_misc.py +0 -0
- {papers_cli-2.6.1 → papers_cli-2.7}/tests/test_undo.py +0 -0
|
@@ -9,12 +9,14 @@ import subprocess as sp
|
|
|
9
9
|
import shutil
|
|
10
10
|
import itertools
|
|
11
11
|
import fnmatch # unix-like match
|
|
12
|
+
import hashlib
|
|
13
|
+
import time
|
|
12
14
|
|
|
13
15
|
import papers
|
|
14
16
|
from papers import logger
|
|
15
17
|
from papers.extract import extract_pdf_doi, isvaliddoi, extract_pdf_metadata
|
|
16
18
|
from papers.extract import fetch_bibtex_by_doi
|
|
17
|
-
from papers.encoding import parse_file, format_file, family_names, format_entries
|
|
19
|
+
from papers.encoding import parse_file, format_file, family_names, format_entries, standard_name
|
|
18
20
|
from papers.config import bcolors, Config, search_config, CONFIG_FILE, CONFIG_FILE_LOCAL, DATA_DIR, CONFIG_FILE_LEGACY, BACKUP_DIR
|
|
19
21
|
from papers.duplicate import list_duplicates, list_uniques, edit_entries
|
|
20
22
|
from papers.bib import Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck, backupfile as backupfile_func, isvalidkey
|
|
@@ -223,6 +225,12 @@ def savebib(biblio, config):
|
|
|
223
225
|
# config.gitcommit()
|
|
224
226
|
|
|
225
227
|
|
|
228
|
+
def view_entry_files(biblio, entry):
|
|
229
|
+
for f in biblio.get_files(entry):
|
|
230
|
+
logger.info(f"opening {f} ...")
|
|
231
|
+
view_pdf(f)
|
|
232
|
+
|
|
233
|
+
|
|
226
234
|
def set_keyformat_config_from_cmd(o, config):
|
|
227
235
|
"""
|
|
228
236
|
Given options and a config state, applies the desired key options to the config.
|
|
@@ -505,6 +513,8 @@ def addcmd(parser, o, config):
|
|
|
505
513
|
kw = {'on_conflict':o.mode, 'check_duplicate':not o.no_check_duplicate,
|
|
506
514
|
'mergefiles':not o.no_merge_files, 'update_key':o.update_key}
|
|
507
515
|
|
|
516
|
+
entries = []
|
|
517
|
+
|
|
508
518
|
if len(o.file) > 1:
|
|
509
519
|
if o.attachment:
|
|
510
520
|
logger.error('--attachment is only valid for one PDF / BIBTEX entry')
|
|
@@ -514,35 +524,37 @@ def addcmd(parser, o, config):
|
|
|
514
524
|
raise PapersExit()
|
|
515
525
|
|
|
516
526
|
if len(o.file) == 0:
|
|
517
|
-
if
|
|
518
|
-
|
|
527
|
+
if o.edit:
|
|
528
|
+
pass
|
|
529
|
+
elif not o.doi:
|
|
530
|
+
logger.error('Please provide either a PDF file or BIBTEX entry or specify `--doi DOI` or `--edit`')
|
|
519
531
|
raise PapersExit()
|
|
520
532
|
elif o.no_query_doi:
|
|
521
533
|
logger.error('If no file is present, --no-query-doi is not compatible with --doi')
|
|
522
534
|
raise PapersExit()
|
|
523
535
|
else:
|
|
524
|
-
biblio.fetch_doi(o.doi, attachments=o.attachment, rename=o.rename, copy=o.copy, **kw)
|
|
536
|
+
entries.extend( biblio.fetch_doi(o.doi, attachments=o.attachment, rename=o.rename, copy=o.copy, **kw) )
|
|
525
537
|
|
|
526
538
|
for file in o.file:
|
|
527
539
|
try:
|
|
528
540
|
if os.path.isdir(file):
|
|
529
541
|
if o.recursive:
|
|
530
|
-
biblio.scan_dir(file, rename=o.rename, copy=o.copy,
|
|
542
|
+
entries.extend( biblio.scan_dir(file, rename=o.rename, copy=o.copy,
|
|
531
543
|
search_doi=not o.no_query_doi,
|
|
532
544
|
search_fulltext=not o.no_query_fulltext,
|
|
533
|
-
**kw)
|
|
545
|
+
**kw) )
|
|
534
546
|
else:
|
|
535
547
|
raise ValueError(file+' is a directory, requires --recursive to explore')
|
|
536
548
|
|
|
537
549
|
elif file.endswith('.pdf'):
|
|
538
|
-
biblio.add_pdf(file, attachments=o.attachment, rename=o.rename, copy=o.copy,
|
|
550
|
+
entries.extend( biblio.add_pdf(file, attachments=o.attachment, rename=o.rename, copy=o.copy,
|
|
539
551
|
search_doi=not o.no_query_doi,
|
|
540
552
|
search_fulltext=not o.no_query_fulltext,
|
|
541
553
|
scholar=o.scholar, doi=o.doi,
|
|
542
|
-
**kw)
|
|
554
|
+
**kw) )
|
|
543
555
|
|
|
544
556
|
else: # file.endswith('.bib'):
|
|
545
|
-
biblio.add_bibtex_file(file, attachments=o.attachment, rename=o.rename, copy=o.copy, **kw)
|
|
557
|
+
entries.extend( biblio.add_bibtex_file(file, attachments=o.attachment, rename=o.rename, copy=o.copy, **kw) )
|
|
546
558
|
|
|
547
559
|
except Exception as error:
|
|
548
560
|
# print(error)
|
|
@@ -554,8 +566,43 @@ def addcmd(parser, o, config):
|
|
|
554
566
|
logger.error('use --ignore to add other files anyway')
|
|
555
567
|
raise PapersExit()
|
|
556
568
|
|
|
569
|
+
# The list of new entries potentially contains duplicates if more than one file is added sequentially
|
|
570
|
+
# If action is required on the added entry, we need to make sure we're consistent with the biblio.
|
|
571
|
+
if len(entries) > 1 and (o.edit or o.open):
|
|
572
|
+
unique_keys = set(biblio.key(e) for e in entries)
|
|
573
|
+
entries = [e for e in biblio.entries if biblio.key(e) in unique_keys]
|
|
574
|
+
|
|
575
|
+
if o.edit:
|
|
576
|
+
if len(entries) == 0:
|
|
577
|
+
entries = [{
|
|
578
|
+
"ID": "EDIT-NEW-KEY",
|
|
579
|
+
"ENTRYTYPE": "article",
|
|
580
|
+
"author": standard_name("John Doe and Jane Roe"),
|
|
581
|
+
"title": "",
|
|
582
|
+
"journal": "",
|
|
583
|
+
"year": "",
|
|
584
|
+
"doi": o.doi or "",
|
|
585
|
+
"file": format_file(o.attachment or [], relative_to=biblio.relative_to),
|
|
586
|
+
}]
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
otherentries = [e for e in biblio.entries if e not in entries]
|
|
590
|
+
entries = edit_entries(entries)
|
|
591
|
+
entries = [{k:v for k,v in e.items() if v != ""} for e in entries]
|
|
592
|
+
biblio.db.entries = otherentries
|
|
593
|
+
for e in entries:
|
|
594
|
+
biblio.insert_entry(e, **kw)
|
|
595
|
+
|
|
596
|
+
except Exception as error:
|
|
597
|
+
logger.error(str(error))
|
|
598
|
+
return
|
|
599
|
+
|
|
557
600
|
savebib(biblio, config)
|
|
558
601
|
|
|
602
|
+
if o.open:
|
|
603
|
+
for e in entries:
|
|
604
|
+
view_entry_files(biblio, e)
|
|
605
|
+
|
|
559
606
|
def checkcmd(parser, o, config):
|
|
560
607
|
"""
|
|
561
608
|
Loops over the entire bib file that the Papers install sees, and checks each entry for formatting and for the existance of duplicates. Then writes the Biblio object back to your Bibtex file.
|
|
@@ -801,9 +848,7 @@ def listcmd(parser, o, config):
|
|
|
801
848
|
|
|
802
849
|
elif o.open:
|
|
803
850
|
for e in entries:
|
|
804
|
-
|
|
805
|
-
logger.info(f"opening {f} ...")
|
|
806
|
-
view_pdf(f)
|
|
851
|
+
view_entry_files(biblio, e)
|
|
807
852
|
|
|
808
853
|
elif o.field:
|
|
809
854
|
# entries = [{k:e[k] for k in e if k in o.field+['ID','ENTRYTYPE']} for e in entries]
|
|
@@ -985,6 +1030,10 @@ def get_parser(config=None):
|
|
|
985
1030
|
grp.add_argument('-c','--copy', action='store_true',
|
|
986
1031
|
help='copy file instead of moving them')
|
|
987
1032
|
|
|
1033
|
+
grp = addp.add_argument_group('actions')
|
|
1034
|
+
grp.add_argument('-e', '--edit', action='store_true', help='edit entry')
|
|
1035
|
+
grp.add_argument('-o', '--open', action='store_true', help='open files')
|
|
1036
|
+
|
|
988
1037
|
# check
|
|
989
1038
|
# =====
|
|
990
1039
|
checkp = subparsers.add_parser('check', description='check and fix entries',
|
|
@@ -285,8 +285,8 @@ class Biblio:
|
|
|
285
285
|
# additional checks on DOI etc...
|
|
286
286
|
if check_duplicate:
|
|
287
287
|
logger.debug('check duplicates : TRUE')
|
|
288
|
-
|
|
289
|
-
return
|
|
288
|
+
print("entering...")
|
|
289
|
+
return self.insert_entry_check(entry, update_key=update_key, rename=rename, copy=copy, **checkopt)
|
|
290
290
|
else:
|
|
291
291
|
logger.debug('check duplicates : FALSE')
|
|
292
292
|
|
|
@@ -312,13 +312,14 @@ class Biblio:
|
|
|
312
312
|
|
|
313
313
|
if rename: self.rename_entry_files(entry, copy=copy)
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
return [ entry ]
|
|
316
316
|
|
|
317
|
+
def insert_entry_check(self, entry, update_key=False, mergefiles=True, on_conflict='i', rename=False, copy=False):
|
|
317
318
|
duplicates = [e for e in self.entries if self.eq(e, entry)]
|
|
318
319
|
|
|
319
320
|
if not duplicates:
|
|
320
321
|
logger.debug('not a duplicate')
|
|
321
|
-
self.insert_entry(entry, update_key, rename=rename, copy=copy)
|
|
322
|
+
return self.insert_entry(entry, update_key, rename=rename, copy=copy)
|
|
322
323
|
|
|
323
324
|
|
|
324
325
|
else:
|
|
@@ -336,7 +337,7 @@ class Biblio:
|
|
|
336
337
|
logger.debug('exact duplicate')
|
|
337
338
|
if rename: self.rename_entry_files(candidate, copy=copy)
|
|
338
339
|
print("Existing entry:", self.key(entry))
|
|
339
|
-
return
|
|
340
|
+
return [ entry ] # do nothing
|
|
340
341
|
|
|
341
342
|
if update_key and entry['ID'] != candidate['ID']:
|
|
342
343
|
logger.info('duplicate :: update key to match existing entry: {} => {}'.format(entry['ID'], candidate['ID']))
|
|
@@ -355,13 +356,15 @@ class Biblio:
|
|
|
355
356
|
entry = candidate
|
|
356
357
|
if rename: self.rename_entry_files(candidate, copy=copy)
|
|
357
358
|
print("Existing entry:", self.key(entry))
|
|
358
|
-
return
|
|
359
|
+
return [ entry ] # do nothing
|
|
359
360
|
|
|
360
361
|
logger.debug('conflict resolution: '+on_conflict)
|
|
361
362
|
resolved = conflict_resolution_on_insert(candidate, entry, mode=on_conflict)
|
|
362
363
|
self.entries.remove(candidate) # maybe in resolved entries
|
|
364
|
+
entries = []
|
|
363
365
|
for e in resolved:
|
|
364
|
-
self.insert_entry(e, update_key, rename=rename, copy=copy)
|
|
366
|
+
entries.extend( self.insert_entry(e, update_key, rename=rename, copy=copy) )
|
|
367
|
+
return entries
|
|
365
368
|
|
|
366
369
|
|
|
367
370
|
def generate_key(self, entry):
|
|
@@ -389,6 +392,7 @@ class Biblio:
|
|
|
389
392
|
bib = bibtexparser.loads(bibtex)
|
|
390
393
|
if convert_to_unicode:
|
|
391
394
|
bib = bibtexparser.customization.convert_to_unicode(bib)
|
|
395
|
+
entries = []
|
|
392
396
|
for e in bib.entries:
|
|
393
397
|
files = []
|
|
394
398
|
if "file" in e:
|
|
@@ -399,7 +403,8 @@ class Biblio:
|
|
|
399
403
|
if files:
|
|
400
404
|
self.set_files(e, files)
|
|
401
405
|
|
|
402
|
-
self.insert_entry(e, **kw)
|
|
406
|
+
entries.extend( self.insert_entry(e, **kw) )
|
|
407
|
+
return entries
|
|
403
408
|
|
|
404
409
|
|
|
405
410
|
def add_bibtex_file(self, file, **kw):
|
|
@@ -409,8 +414,7 @@ class Biblio:
|
|
|
409
414
|
|
|
410
415
|
def fetch_doi(self, doi, **kw):
|
|
411
416
|
bibtex = fetch_bibtex_by_doi(doi)
|
|
412
|
-
self.add_bibtex(bibtex, **kw)
|
|
413
|
-
|
|
417
|
+
return self.add_bibtex(bibtex, **kw)
|
|
414
418
|
|
|
415
419
|
def add_pdf(self, pdf, attachments=None, search_doi=True, search_fulltext=True, scholar=False, doi=None, **kw):
|
|
416
420
|
|
|
@@ -435,10 +439,10 @@ class Biblio:
|
|
|
435
439
|
|
|
436
440
|
kw.pop('update_key', True)
|
|
437
441
|
# logger.warning('fetched key is always updated when adding PDF to existing bib')
|
|
438
|
-
self.insert_entry(entry, update_key=True, **kw)
|
|
442
|
+
return self.insert_entry(entry, update_key=True, **kw)
|
|
439
443
|
|
|
440
444
|
|
|
441
|
-
def
|
|
445
|
+
def scan_dir_iter(self, direc, search_doi=True, search_fulltext=True, **kw):
|
|
442
446
|
|
|
443
447
|
for root, direcs, files in os.walk(direc):
|
|
444
448
|
dirname = os.path.basename(root)
|
|
@@ -450,7 +454,7 @@ class Biblio:
|
|
|
450
454
|
logger.debug('read from hidden bibtex')
|
|
451
455
|
try:
|
|
452
456
|
entry = read_entry_dir(root, relative_to=self.relative_to)
|
|
453
|
-
self.insert_entry(entry, **kw)
|
|
457
|
+
yield from self.insert_entry(entry, **kw)
|
|
454
458
|
except Exception:
|
|
455
459
|
logger.warning(root+'::'+str(error))
|
|
456
460
|
continue
|
|
@@ -461,13 +465,17 @@ class Biblio:
|
|
|
461
465
|
path = os.path.join(root, file)
|
|
462
466
|
try:
|
|
463
467
|
if file.endswith('.pdf'):
|
|
464
|
-
self.add_pdf(path, search_doi=search_doi, search_fulltext=search_fulltext, **kw)
|
|
468
|
+
yield from self.add_pdf(path, search_doi=search_doi, search_fulltext=search_fulltext, **kw)
|
|
465
469
|
elif file.endswith('.bib'):
|
|
466
|
-
self.add_bibtex_file(path, **kw)
|
|
470
|
+
yield from self.add_bibtex_file(path, **kw)
|
|
467
471
|
except Exception as error:
|
|
468
472
|
logger.warning(path+'::'+str(error))
|
|
469
473
|
continue
|
|
470
474
|
|
|
475
|
+
def scan_dir(self, direc, **kw):
|
|
476
|
+
" like scan_dir_iter but returns a list"
|
|
477
|
+
return list(self.scan_dir_iter(direc, **kw))
|
|
478
|
+
|
|
471
479
|
|
|
472
480
|
def format(self):
|
|
473
481
|
return bibtexparser.dumps(self.db)
|
|
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
|