papers-cli 2.15.5__tar.gz → 2.16__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 (59) hide show
  1. {papers_cli-2.15.5 → papers_cli-2.16}/.github/workflows/pypi.yml +4 -4
  2. papers_cli-2.16/.github/workflows/release.yml +58 -0
  3. {papers_cli-2.15.5 → papers_cli-2.16}/.github/workflows/tests.yml +3 -3
  4. {papers_cli-2.15.5 → papers_cli-2.16}/PKG-INFO +10 -3
  5. {papers_cli-2.15.5 → papers_cli-2.16}/README.md +8 -1
  6. {papers_cli-2.15.5 → papers_cli-2.16}/papers/__main__.py +26 -24
  7. {papers_cli-2.15.5 → papers_cli-2.16}/papers/_version.py +3 -3
  8. {papers_cli-2.15.5 → papers_cli-2.16}/papers/bib.py +125 -89
  9. {papers_cli-2.15.5 → papers_cli-2.16}/papers/config.py +2 -2
  10. {papers_cli-2.15.5 → papers_cli-2.16}/papers/duplicate.py +38 -24
  11. {papers_cli-2.15.5 → papers_cli-2.16}/papers/encoding.py +50 -23
  12. papers_cli-2.16/papers/entries.py +107 -0
  13. {papers_cli-2.15.5 → papers_cli-2.16}/papers/extract.py +21 -25
  14. {papers_cli-2.15.5 → papers_cli-2.16}/papers/filename.py +12 -9
  15. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/PKG-INFO +10 -3
  16. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/SOURCES.txt +12 -0
  17. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/requires.txt +1 -1
  18. {papers_cli-2.15.5 → papers_cli-2.16}/pyproject.toml +7 -5
  19. {papers_cli-2.15.5 → papers_cli-2.16}/requirements.txt +1 -1
  20. papers_cli-2.16/scripts/README.md +38 -0
  21. papers_cli-2.16/scripts/benchmark_bibtexparser_load.py +112 -0
  22. papers_cli-2.16/scripts/generate_dummy_bib.py +51 -0
  23. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_add.py +23 -10
  24. papers_cli-2.16/tests/test_bib_unit.py +137 -0
  25. papers_cli-2.16/tests/test_check.py +103 -0
  26. papers_cli-2.16/tests/test_config_unit.py +86 -0
  27. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_duplicates.py +78 -8
  28. papers_cli-2.16/tests/test_encoding.py +166 -0
  29. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_extract.py +4 -4
  30. papers_cli-2.16/tests/test_extract_unit.py +133 -0
  31. papers_cli-2.16/tests/test_filecheck.py +102 -0
  32. papers_cli-2.16/tests/test_filename_unit.py +17 -0
  33. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_install.py +9 -0
  34. papers_cli-2.16/tests/test_latexenc.py +85 -0
  35. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_list.py +55 -0
  36. papers_cli-2.16/tests/test_utils.py +118 -0
  37. papers_cli-2.15.5/tests/test_encoding.py +0 -41
  38. papers_cli-2.15.5/tests/test_filecheck.py +0 -48
  39. {papers_cli-2.15.5 → papers_cli-2.16}/.gitattributes +0 -0
  40. {papers_cli-2.15.5 → papers_cli-2.16}/.github/FUNDING.yml +0 -0
  41. {papers_cli-2.15.5 → papers_cli-2.16}/.gitignore +0 -0
  42. {papers_cli-2.15.5 → papers_cli-2.16}/LICENSE +0 -0
  43. {papers_cli-2.15.5 → papers_cli-2.16}/papers/__init__.py +0 -0
  44. {papers_cli-2.15.5 → papers_cli-2.16}/papers/latexenc.py +0 -0
  45. {papers_cli-2.15.5 → papers_cli-2.16}/papers/utils.py +0 -0
  46. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/dependency_links.txt +0 -0
  47. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/entry_points.txt +0 -0
  48. {papers_cli-2.15.5 → papers_cli-2.16}/papers_cli.egg-info/top_level.txt +0 -0
  49. {papers_cli-2.15.5 → papers_cli-2.16}/setup.cfg +0 -0
  50. {papers_cli-2.15.5 → papers_cli-2.16}/setup.py +0 -0
  51. {papers_cli-2.15.5 → papers_cli-2.16}/tests/__init__.py +0 -0
  52. {papers_cli-2.15.5 → papers_cli-2.16}/tests/common.py +0 -0
  53. {papers_cli-2.15.5 → papers_cli-2.16}/tests/config.py +0 -0
  54. {papers_cli-2.15.5 → papers_cli-2.16}/tests/download.py +0 -0
  55. {papers_cli-2.15.5 → papers_cli-2.16}/tests/downloadedpapers/.gitignore +0 -0
  56. {papers_cli-2.15.5 → papers_cli-2.16}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
  57. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_biblio.py +0 -0
  58. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_misc.py +0 -0
  59. {papers_cli-2.15.5 → papers_cli-2.16}/tests/test_undo.py +0 -0
@@ -3,7 +3,7 @@ name: Publish to PyPI
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - '*'
6
+ - 'v*'
7
7
 
8
8
  jobs:
9
9
  build-and-publish:
@@ -16,10 +16,10 @@ jobs:
16
16
 
17
17
  steps:
18
18
  - name: Checkout code
19
- uses: actions/checkout@v2
19
+ uses: actions/checkout@v5
20
20
 
21
21
  - name: Set up Python
22
- uses: actions/setup-python@v2
22
+ uses: actions/setup-python@v5
23
23
  with:
24
24
  python-version: '3.x'
25
25
 
@@ -33,4 +33,4 @@ jobs:
33
33
  python setup.py sdist bdist_wheel
34
34
 
35
35
  - name: Publish to PyPI
36
- uses: pypa/gh-action-pypi-publish@release/v1
36
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,58 @@
1
+ name: Create GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ create-release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write # Required to create releases
13
+
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0 # Fetch all history for changelog generation
19
+
20
+ - name: Get tag name
21
+ id: tag
22
+ run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
23
+
24
+ - name: Generate changelog
25
+ id: changelog
26
+ run: |
27
+ # Get the previous tag
28
+ PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
29
+
30
+ if [ -z "$PREV_TAG" ]; then
31
+ # First release - get all commits
32
+ CHANGES=$(git log --pretty=format:"- %s (%h)" --no-merges)
33
+ else
34
+ # Get commits since previous tag
35
+ CHANGES=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges)
36
+ fi
37
+
38
+ # Save to file to handle multiline content
39
+ echo "$CHANGES" > changelog.txt
40
+
41
+ # Also output to GitHub output
42
+ echo "changelog<<EOF" >> $GITHUB_OUTPUT
43
+ echo "$CHANGES" >> $GITHUB_OUTPUT
44
+ echo "EOF" >> $GITHUB_OUTPUT
45
+
46
+ - name: Create Release
47
+ uses: actions/create-release@v1
48
+ env:
49
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50
+ with:
51
+ tag_name: ${{ steps.tag.outputs.tag }}
52
+ release_name: Release ${{ steps.tag.outputs.tag }}
53
+ body: |
54
+ ## Changes in ${{ steps.tag.outputs.tag }}
55
+
56
+ ${{ steps.changelog.outputs.changelog }}
57
+ draft: false
58
+ prerelease: false
@@ -10,12 +10,12 @@ jobs:
10
10
  runs-on: ubuntu-latest
11
11
  strategy:
12
12
  matrix:
13
- python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
13
+ python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t" ]
14
14
 
15
15
  steps:
16
- - uses: actions/checkout@v2
16
+ - uses: actions/checkout@v5
17
17
  - name: Set up Python ${{ matrix.python-version }}
18
- uses: actions/setup-python@v2
18
+ uses: actions/setup-python@v5
19
19
  with:
20
20
  python-version: ${{ matrix.python-version }}
21
21
  - name: Install dependencies
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papers-cli
3
- Version: 2.15.5
3
+ Version: 2.16
4
4
  Summary: utilities to keep your PDF library organized
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT
@@ -13,7 +13,7 @@ License-File: LICENSE
13
13
  Requires-Dist: PyMuPDF
14
14
  Requires-Dist: unidecode
15
15
  Requires-Dist: crossrefapi
16
- Requires-Dist: bibtexparser
16
+ Requires-Dist: bibtexparser>=2.0.0b2
17
17
  Requires-Dist: scholarly
18
18
  Requires-Dist: rapidfuzz
19
19
  Requires-Dist: python-slugify
@@ -21,7 +21,8 @@ Requires-Dist: bs4
21
21
  Dynamic: license-file
22
22
 
23
23
  [![pypi](https://img.shields.io/pypi/v/papers-cli)](https://pypi.org/project/papers-cli)
24
- [![python](https://img.shields.io/badge/python-3.13-blue.svg)]()
24
+ [![python](https://img.shields.io/badge/python-3.9-blue.svg)]()
25
+ [![python](https://img.shields.io/badge/python-3.14t-blue.svg)]()
25
26
  [![test](https://github.com/perrette/papers/workflows/CI/badge.svg?query=branch%3Amaster)](https://github.com/perrette/papers/actions)
26
27
 
27
28
  # papers
@@ -100,6 +101,12 @@ That is equivalent to doing:
100
101
 
101
102
  See [Control fields when renaming file](#control-fields-when-renaming-file) for how to specify file naming patterns.
102
103
 
104
+ It is also possible to do this on a full directory of files, recursively:
105
+
106
+ papers add --rename --recursive /home/perette/playground/papers/papers_test
107
+
108
+ where, above, the `papers_test` directory contains a few PDF files. For each PDF, `papers` will attempt to extract the metadata and add the relevant file to the bibliography and renamed files to the files directory.
109
+
103
110
  ### Add library entry from its DOI
104
111
 
105
112
  If you already know the DOI of a PDF, and don't want to gamble the fulltext search and match, you can indicate it via `--doi`:
@@ -1,5 +1,6 @@
1
1
  [![pypi](https://img.shields.io/pypi/v/papers-cli)](https://pypi.org/project/papers-cli)
2
- [![python](https://img.shields.io/badge/python-3.13-blue.svg)]()
2
+ [![python](https://img.shields.io/badge/python-3.9-blue.svg)]()
3
+ [![python](https://img.shields.io/badge/python-3.14t-blue.svg)]()
3
4
  [![test](https://github.com/perrette/papers/workflows/CI/badge.svg?query=branch%3Amaster)](https://github.com/perrette/papers/actions)
4
5
 
5
6
  # papers
@@ -78,6 +79,12 @@ That is equivalent to doing:
78
79
 
79
80
  See [Control fields when renaming file](#control-fields-when-renaming-file) for how to specify file naming patterns.
80
81
 
82
+ It is also possible to do this on a full directory of files, recursively:
83
+
84
+ papers add --rename --recursive /home/perette/playground/papers/papers_test
85
+
86
+ where, above, the `papers_test` directory contains a few PDF files. For each PDF, `papers` will attempt to extract the metadata and add the relevant file to the bibliography and renamed files to the files directory.
87
+
81
88
  ### Add library entry from its DOI
82
89
 
83
90
  If you already know the DOI of a PDF, and don't want to gamble the fulltext search and match, you can indicate it via `--doi`:
@@ -19,8 +19,10 @@ from papers.extract import fetch_bibtex_by_doi, fetch_bibtex_by_fulltext_crossre
19
19
  from papers.encoding import parse_file, format_file, family_names, format_entries, standard_name, format_entry, parse_keywords, format_key
20
20
  from papers.config import bcolors, Config, search_config, CONFIG_FILE, CONFIG_FILE_LOCAL, DATA_DIR, CONFIG_FILE_LEGACY, BACKUP_DIR
21
21
  from papers.duplicate import list_duplicates, list_uniques, edit_entries
22
+ from papers.entries import get_entry_val
22
23
  from papers.bib import (Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck,
23
- backupfile as backupfile_func, isvalidkey, DuplicateKeyError, clean_filesdir)
24
+ backupfile as backupfile_func, isvalidkey, DuplicateKeyError, clean_filesdir,
25
+ are_duplicates)
24
26
  from papers.utils import move, checksum, view_pdf, open_folder
25
27
  from papers import __version__
26
28
 
@@ -149,10 +151,10 @@ def _restore_from_backupdir_wrapped(config, restore_files=False):
149
151
  assert len(biblio.entries) == len(biblio_clean.entries)
150
152
 
151
153
  for e, e_clean in zip(biblio.entries, biblio_clean.entries):
152
- assert e['ID'] == e_clean['ID'], f"{e['ID']} != {e_clean['ID']})"
154
+ assert get_entry_val(e, 'ID', '') == get_entry_val(e_clean, 'ID', ''), f"{get_entry_val(e, 'ID', '')} != {get_entry_val(e_clean, 'ID', '')})"
153
155
  files = biblio.get_files(e)
154
156
  files_clean = biblio_clean.get_files(e_clean)
155
- assert len(files) == len(files_clean), f"{e['ID']} :: files {len(files)} != {len(files_clean)})"
157
+ assert len(files) == len(files_clean), f"{get_entry_val(e, 'ID', '')} :: files {len(files)} != {len(files_clean)})"
156
158
 
157
159
  new_files = []
158
160
 
@@ -162,7 +164,7 @@ def _restore_from_backupdir_wrapped(config, restore_files=False):
162
164
  logger.debug(f"BACKUP FILE DOES NOT EXISTS: {f_clean} ")
163
165
  logger.debug(f"BACKUP ENTRY: {e_clean['file']} ")
164
166
  logger.debug(f"BROKEN ENTRY: {e['file']} ")
165
- logger.warning(f"{e['ID']} :: file link broken => {f} ")
167
+ logger.warning(f"{get_entry_val(e, 'ID', '')} :: file link broken => {f} ")
166
168
  new_files.append(f)
167
169
  continue
168
170
 
@@ -176,7 +178,7 @@ def _restore_from_backupdir_wrapped(config, restore_files=False):
176
178
  new_files.append(f)
177
179
 
178
180
  else:
179
- logger.warning(f"{e['ID']} :: file found but does not match backup (keep pointer to backup): {f} != {f_clean}")
181
+ logger.warning(f"{get_entry_val(e, 'ID', '')} :: file found but does not match backup (keep pointer to backup): {f} != {f_clean}")
180
182
  new_files.append(f_clean)
181
183
 
182
184
  # original bibtex link is broken, --restore-file is active
@@ -187,7 +189,7 @@ def _restore_from_backupdir_wrapped(config, restore_files=False):
187
189
 
188
190
  except Exception as error:
189
191
  logger.error(f"{error}")
190
- logger.error(f"{e['ID']} :: failed to restore file (keep pointer to backup)")
192
+ logger.error(f"{get_entry_val(e, 'ID', '')} :: failed to restore file (keep pointer to backup)")
191
193
  new_files.append(f_clean)
192
194
  pass
193
195
 
@@ -661,11 +663,11 @@ def addcmd(parser, o, config):
661
663
  # compare entries to inform user
662
664
  # this is not very efficient but I have yet to hear a complaint about speed
663
665
 
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))
666
+ old_set = set(get_entry_val(e, 'ID', '') for e in biblio_init.entries)
667
+ old_entries_by_key = {get_entry_val(e, 'ID', ''): e for e in biblio_init.db.entries}
668
+ new_set = set(get_entry_val(e, 'ID', '') for e in biblio.entries)
669
+ new_entries_by_key = {get_entry_val(e, 'ID', ''): e for e in biblio.db.entries}
670
+ modified_set = set(get_entry_val(e, 'ID', '') for e in entries).intersection(set.intersection(old_set, new_set))
669
671
 
670
672
  for ID in sorted(old_set - new_set):
671
673
  print(format_entry(biblio_init, old_entries_by_key[ID], prefix="Removed"))
@@ -699,7 +701,7 @@ def checkcmd(parser, o, config):
699
701
  # o.fix_key = True
700
702
 
701
703
  for e in biblio.entries:
702
- if o.keys and e.get('ID','') not in o.keys:
704
+ if o.keys and get_entry_val(e, 'ID', '') not in o.keys:
703
705
  continue
704
706
  biblio.fix_entry(e, fix_doi=o.fix_doi, fetch=o.fetch, fetch_all=o.fetch_all, fix_key=o.fix_key,
705
707
  auto_key=o.auto_key, format_name=o.format_name, encoding=o.encoding,
@@ -816,11 +818,11 @@ def listcmd(parser, o, config):
816
818
  return _match(word, target, fuzzy=o.fuzzy, substring=not o.strict)
817
819
 
818
820
  def _nfiles(e):
819
- return len(parse_file(e.get('file',''), relative_to=biblio.relative_to))
821
+ return len(parse_file(get_entry_val(e, 'file', ''), relative_to=biblio.relative_to))
820
822
 
821
823
 
822
824
  def _requiresreview(e):
823
- if not isvalidkey(e.get('ID','')): return True
825
+ if not isvalidkey(get_entry_val(e, 'ID', '')): return True
824
826
  if 'doi' in e and not isvaliddoi(e['doi']): return True
825
827
  if 'author' not in e: return True
826
828
  if 'title' not in e: return True
@@ -844,17 +846,17 @@ def listcmd(parser, o, config):
844
846
  if 'doi' in e and not isvaliddoi(e['doi']):
845
847
  e['doi'] = bcolors.FAIL + e['doi'] + bcolors.ENDC
846
848
  if o.has_file:
847
- entries = [e for e in entries if e.get('file','')]
849
+ entries = [e for e in entries if get_entry_val(e, 'file', '')]
848
850
  if o.no_file:
849
- entries = [e for e in entries if not e.get('file','')]
851
+ entries = [e for e in entries if not get_entry_val(e, 'file', '')]
850
852
  if o.broken_file:
851
- entries = [e for e in entries if e.get('file','') and any([not os.path.exists(f) for f in parse_file(e['file'], relative_to=biblio.relative_to)])]
853
+ entries = [e for e in entries if get_entry_val(e, 'file', '') and any([not os.path.exists(f) for f in parse_file(e['file'], relative_to=biblio.relative_to)])]
852
854
 
853
855
 
854
856
  if o.doi:
855
857
  entries = [e for e in entries if 'doi' in e and _longmatch(e['doi'], o.doi)]
856
858
  if o.key:
857
- entries = [e for e in entries if 'ID' in e and _longmatch(e['ID'], o.key)]
859
+ entries = [e for e in entries if _longmatch(get_entry_val(e, 'ID', ''), o.key)]
858
860
  if o.year:
859
861
  entries = [e for e in entries if 'year' in e and _longmatch(e['year'], o.year)]
860
862
  if o.first_author:
@@ -881,7 +883,7 @@ def listcmd(parser, o, config):
881
883
  if o.duplicates_key:
882
884
  entries = list_dup(entries, key=biblio.key, issorted=True)
883
885
  if o.duplicates_doi:
884
- entries = list_dup(entries, key=lambda e:e.get('doi',''), filter_key=isvaliddoi)
886
+ entries = list_dup(entries, key=lambda e: get_entry_val(e, 'doi', ''), filter_key=isvaliddoi)
885
887
  if o.duplicates_tit:
886
888
  entries = list_dup(entries, key=title_id)
887
889
  if o.duplicates:
@@ -889,7 +891,7 @@ def listcmd(parser, o, config):
889
891
  # I think that's because we might need to be inclusive here, whereas the default is conservative (parameter used for several functions with possibly differing requirements).
890
892
  # (otherwise we'd have used the command-line option o.similarity, or possibly DEFAULT_SIMILARITY)
891
893
  # Might need to revise later (the question mark is from a review after a long time without use)
892
- eq = lambda a, b: a['ID'] == b['ID'] or are_duplicates(a, b, similarity="PARTIAL", fuzzy_ratio=o.fuzzy_ratio)
894
+ eq = lambda a, b: get_entry_val(a, 'ID', '') == get_entry_val(b, 'ID', '') or are_duplicates(a, b, similarity="PARTIAL", fuzzy_ratio=o.fuzzy_ratio)
893
895
  entries = list_dup(entries, eq=eq)
894
896
 
895
897
  if o.add_keywords:
@@ -919,7 +921,7 @@ def listcmd(parser, o, config):
919
921
  otherentries = [e for e in biblio.db.entries if e not in entries]
920
922
  try:
921
923
  entries = edit_entries(entries)
922
- biblio.db.entries = otherentries + entries
924
+ biblio.entries = otherentries + entries
923
925
  except Exception as error:
924
926
  logger.error(str(error))
925
927
  return
@@ -933,7 +935,7 @@ def listcmd(parser, o, config):
933
935
 
934
936
  elif o.delete:
935
937
  for e in entries:
936
- biblio.db.entries.remove(e)
938
+ biblio.db.remove(e)
937
939
  savebib(biblio, config)
938
940
 
939
941
  elif o.open:
@@ -943,10 +945,10 @@ def listcmd(parser, o, config):
943
945
  elif o.field:
944
946
  # entries = [{k:e[k] for k in e if k in o.field+['ID','ENTRYTYPE']} for e in entries]
945
947
  for e in entries:
946
- print(format_key(e, no_key=o.no_key),*[e.get(k, "") for k in o.field])
948
+ print(format_key(e, no_key=o.no_key),*[get_entry_val(e, k, "") for k in o.field])
947
949
  elif o.key_only:
948
950
  for e in entries:
949
- print(e['ID'])
951
+ print(get_entry_val(e, 'ID', ''))
950
952
  elif o.one_liner:
951
953
  for e in entries:
952
954
  print(format_entry(biblio, e, no_key=o.no_key))
@@ -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.5'
32
- __version_tuple__ = version_tuple = (2, 15, 5)
31
+ __version__ = version = '2.16'
32
+ __version_tuple__ = version_tuple = (2, 16)
33
33
 
34
- __commit_id__ = commit_id = 'gbad573f0d'
34
+ __commit_id__ = commit_id = 'g8b22ae064'