papers-cli 2.17.6__tar.gz → 2.18.0__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.17.6 → papers_cli-2.18.0}/PKG-INFO +1 -2
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/configuration.md +4 -1
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/installation.md +1 -3
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/__main__.py +13 -2
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/_version.py +3 -3
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/bib.py +33 -24
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/config.py +69 -51
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/extract.py +193 -67
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/PKG-INFO +1 -2
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/requires.txt +0 -1
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/scm_version.json +2 -2
- {papers_cli-2.17.6 → papers_cli-2.18.0}/pyproject.toml +0 -1
- papers_cli-2.18.0/tests/test_config_unit.py +233 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_extract_unit.py +141 -0
- papers_cli-2.17.6/tests/test_config_unit.py +0 -207
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.gitattributes +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.github/FUNDING.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.github/workflows/docs.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.github/workflows/release.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.github/workflows/tests.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/.gitignore +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/LICENSE +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/README.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/adding-entries.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/comparison.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/features.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/git.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/index.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/library-management.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/listing.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/quickstart.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/renaming.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/docs/undo-redo.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/mkdocs.yml +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/__init__.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/backup.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/duplicate.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/encoding.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/entries.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/filename.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/install.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/latexenc.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers/utils.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/SOURCES.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/scm_file_list.json +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/requirements.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/scripts/README.md +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/scripts/benchmark_bibtexparser_load.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/scripts/generate_dummy_bib.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/setup.cfg +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/setup.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/__init__.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/common.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/config.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/download.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/downloadedpapers/bg-8-515-2011.pdf +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/downloadedpapers/esd-4-11-2013-supplement.pdf +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/downloadedpapers/esd-4-11-2013.pdf +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_add.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_backup_dirs.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_bib_unit.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_biblio.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_check.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_duplicates.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_encoding.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_extract.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_filecheck.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_filename_unit.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_install.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_latexenc.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_list.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_undo.py +0 -0
- {papers_cli-2.17.6 → papers_cli-2.18.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: papers-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.18.0
|
|
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,6 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: PyMuPDF<2
|
|
15
15
|
Requires-Dist: unidecode
|
|
16
|
-
Requires-Dist: crossrefapi
|
|
17
16
|
Requires-Dist: bibtexparser>=2.0.0b2
|
|
18
17
|
Requires-Dist: scholarly
|
|
19
18
|
Requires-Dist: rapidfuzz
|
|
@@ -28,7 +28,10 @@ Type `papers status -v` to check your configuration.
|
|
|
28
28
|
|
|
29
29
|
You also notice a cache directory. All internet requests such as crossref
|
|
30
30
|
requests are saved in the cache directory. This happens regardless of whether
|
|
31
|
-
`papers` is installed or not.
|
|
31
|
+
`papers` is installed or not. The metadata of each DOI is a json file under
|
|
32
|
+
`doi/`, e.g. `doi/10.1126/science.1166510.json` (DOIs that do not exist are
|
|
33
|
+
remembered for a day). Deleting the directory, or any file in it, is safe:
|
|
34
|
+
the requests are made again when needed.
|
|
32
35
|
|
|
33
36
|
## Local install
|
|
34
37
|
|
|
@@ -37,10 +37,8 @@ mitigations are built in:
|
|
|
37
37
|
- python 3.9+
|
|
38
38
|
- [PyMuPDF](https://github.com/pymupdf/PyMuPDF) (preferred) or
|
|
39
39
|
[poppler-utils](https://en.wikipedia.org/wiki/Poppler_(software))
|
|
40
|
-
(only `pdftotext`; deprecated): convert PDF to text for parsing
|
|
40
|
+
(only `pdftotext` and `pdfinfo`; deprecated): convert PDF to text for parsing
|
|
41
41
|
- [bibtexparser](https://bibtexparser.readthedocs.io): parse bibtex files
|
|
42
|
-
- [crossrefapi](https://github.com/fabiobatalha/crossrefapi): make polite
|
|
43
|
-
requests to the crossref API
|
|
44
42
|
- [scholarly](https://github.com/OrganicIrradiation/scholarly): interface for
|
|
45
43
|
google scholar
|
|
46
44
|
- [rapidfuzz](https://github.com/rhasspy/rapidfuzz): calculate score to sort
|
|
@@ -13,7 +13,7 @@ import fnmatch # unix-like match
|
|
|
13
13
|
|
|
14
14
|
import papers
|
|
15
15
|
from papers import logger
|
|
16
|
-
from papers.extract import extract_pdf_doi, isvaliddoi, extract_pdf_metadata
|
|
16
|
+
from papers.extract import extract_pdf_doi, isvaliddoi, extract_pdf_metadata, prefetch_pdf_metadata
|
|
17
17
|
from papers.extract import fetch_bibtex_by_doi, fetch_bibtex_by_fulltext_crossref, fetch_bibtex_by_fulltext_scholar
|
|
18
18
|
from papers.encoding import parse_file, format_file, family_names, format_entries, standard_name, format_entry, parse_keywords, format_key
|
|
19
19
|
from papers.config import (bcolors, Config, search_config, CONFIG_FILE, CONFIG_FILE_LOCAL,
|
|
@@ -22,7 +22,7 @@ from papers.duplicate import list_duplicates, list_uniques, edit_entries, title_
|
|
|
22
22
|
from papers.entries import get_entry_val, entry_content_equal
|
|
23
23
|
from papers.bib import (Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck,
|
|
24
24
|
backupfile as backupfile_func, isvalidkey, DuplicateKeyError, clean_filesdir,
|
|
25
|
-
are_duplicates, download_url, get_biblio)
|
|
25
|
+
are_duplicates, download_url, get_biblio, scan_dir_walk)
|
|
26
26
|
from papers.install import resolve_install, apply_install, InputAsker, DefaultAsker
|
|
27
27
|
from papers.utils import view_pdf, open_folder, PapersExit, file_uri_to_path, find_masked_viewer
|
|
28
28
|
from papers.backup import (silent_backup_bib, restore_from_backupdir,
|
|
@@ -332,6 +332,17 @@ def addcmd(parser, o, config):
|
|
|
332
332
|
entries.extend( biblio.insert_entry(metadata, **kw) )
|
|
333
333
|
|
|
334
334
|
|
|
335
|
+
# several PDFs: fetch their metadata concurrently first
|
|
336
|
+
if not o.doi and not o.scholar and not (o.no_query_doi and o.no_query_fulltext):
|
|
337
|
+
pdfs = []
|
|
338
|
+
for file in o.file:
|
|
339
|
+
if os.path.isdir(file) and o.recursive:
|
|
340
|
+
pdfs.extend(path for kind, path in scan_dir_walk(file) if kind == 'file' and path.lower().endswith('.pdf'))
|
|
341
|
+
elif os.path.isfile(file) and file.lower().endswith('.pdf'):
|
|
342
|
+
pdfs.append(file)
|
|
343
|
+
if len(pdfs) > 1:
|
|
344
|
+
prefetch_pdf_metadata(pdfs, search_doi=not o.no_query_doi, search_fulltext=not o.no_query_fulltext)
|
|
345
|
+
|
|
335
346
|
for file in o.file:
|
|
336
347
|
try:
|
|
337
348
|
if os.path.isdir(file):
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.18.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 18, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g278a27c01'
|
|
@@ -260,6 +260,26 @@ def backupfile(bibtex):
|
|
|
260
260
|
class DuplicateKeyError(ValueError):
|
|
261
261
|
pass
|
|
262
262
|
|
|
263
|
+
def scan_dir_walk(direc):
|
|
264
|
+
"""the files scanned by Biblio.scan_dir: yield ('entry', dir) for entry
|
|
265
|
+
directories (with a hidden bibtex), ('file', path) for other files"""
|
|
266
|
+
top = os.path.abspath(direc)
|
|
267
|
+
for root, direcs, files in os.walk(direc):
|
|
268
|
+
dirname = os.path.basename(root)
|
|
269
|
+
# never skip the scan root itself: basename('.') == '.' looks hidden
|
|
270
|
+
if os.path.abspath(root) != top:
|
|
271
|
+
if dirname.startswith('.'): continue
|
|
272
|
+
if dirname.startswith('_'): continue
|
|
273
|
+
|
|
274
|
+
if os.path.exists(hidden_bibtex(root)):
|
|
275
|
+
yield 'entry', root
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
for file in files:
|
|
279
|
+
if not file.startswith('.'):
|
|
280
|
+
yield 'file', os.path.join(root, file)
|
|
281
|
+
|
|
282
|
+
|
|
263
283
|
class Biblio:
|
|
264
284
|
"""
|
|
265
285
|
The bibtex object that we operate on, which is mainly used to read and write to dynamically, and can then send the changes to be stored in a specified bibtex file on disk.
|
|
@@ -575,36 +595,25 @@ class Biblio:
|
|
|
575
595
|
|
|
576
596
|
def scan_dir_iter(self, direc, search_doi=True, search_fulltext=True, **kw):
|
|
577
597
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
# never skip the scan root itself: basename('.') == '.' looks hidden
|
|
582
|
-
if os.path.abspath(root) != top:
|
|
583
|
-
if dirname.startswith('.'): continue
|
|
584
|
-
if dirname.startswith('_'): continue
|
|
585
|
-
|
|
586
|
-
# maybe a special entry directory?
|
|
587
|
-
if os.path.exists(hidden_bibtex(root)):
|
|
598
|
+
for kind, path in scan_dir_walk(direc):
|
|
599
|
+
# a special entry directory
|
|
600
|
+
if kind == 'entry':
|
|
588
601
|
logger.debug('read from hidden bibtex')
|
|
589
602
|
try:
|
|
590
|
-
entry = read_entry_dir(
|
|
603
|
+
entry = read_entry_dir(path, relative_to=self.relative_to)
|
|
591
604
|
yield from self.insert_entry(entry, **kw)
|
|
592
605
|
except Exception as error:
|
|
593
|
-
logger.warning(
|
|
606
|
+
logger.warning(path+'::'+str(error))
|
|
594
607
|
continue
|
|
595
608
|
|
|
596
|
-
|
|
597
|
-
if
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
yield from self.add_bibtex_file(path, **kw)
|
|
605
|
-
except Exception as error:
|
|
606
|
-
logger.warning(path+'::'+str(error))
|
|
607
|
-
continue
|
|
609
|
+
try:
|
|
610
|
+
if path.lower().endswith('.pdf'):
|
|
611
|
+
yield from self.add_pdf(path, search_doi=search_doi, search_fulltext=search_fulltext, **kw)
|
|
612
|
+
elif path.lower().endswith('.bib'):
|
|
613
|
+
yield from self.add_bibtex_file(path, **kw)
|
|
614
|
+
except Exception as error:
|
|
615
|
+
logger.warning(path+'::'+str(error))
|
|
616
|
+
continue
|
|
608
617
|
|
|
609
618
|
def scan_dir(self, direc, **kw):
|
|
610
619
|
" like scan_dir_iter but returns a list"
|
|
@@ -4,12 +4,10 @@ import copy
|
|
|
4
4
|
import tempfile
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import hashlib
|
|
7
|
+
import time
|
|
8
|
+
from urllib.parse import quote
|
|
7
9
|
import platformdirs
|
|
8
10
|
|
|
9
|
-
try:
|
|
10
|
-
import fcntl # not available on Windows
|
|
11
|
-
except ImportError:
|
|
12
|
-
fcntl = None
|
|
13
11
|
from papers.entries import parse_string
|
|
14
12
|
from papers import logger
|
|
15
13
|
from papers.filename import Format, NAMEFORMAT, KEYFORMAT
|
|
@@ -246,24 +244,17 @@ class Config:
|
|
|
246
244
|
return '\n'.join(lines)
|
|
247
245
|
|
|
248
246
|
|
|
249
|
-
def
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
261
|
-
with open(file + '.lock', 'w') as lockfile:
|
|
262
|
-
fcntl.flock(lockfile, fcntl.LOCK_EX)
|
|
263
|
-
try:
|
|
264
|
-
yield
|
|
265
|
-
finally:
|
|
266
|
-
fcntl.flock(lockfile, fcntl.LOCK_UN)
|
|
247
|
+
def _cache_path(folder, key):
|
|
248
|
+
"""one json file per key: <folder>/<prefix>/<rest>.json, split at the first
|
|
249
|
+
slash so that DOIs are grouped by registrant (10.1126/science.1166510 ->
|
|
250
|
+
10.1126/science.1166510.json); characters unsafe in file names are quoted"""
|
|
251
|
+
head, sep, tail = key.partition('/')
|
|
252
|
+
if not sep:
|
|
253
|
+
head, tail = key[:2], key
|
|
254
|
+
name = quote(tail, safe='')
|
|
255
|
+
if len(name) > 200: # file name length limit
|
|
256
|
+
name = hashlib.sha256(tail.encode('utf-8')).hexdigest()
|
|
257
|
+
return os.path.join(folder, quote(head, safe=''), name + '.json')
|
|
267
258
|
|
|
268
259
|
|
|
269
260
|
def _read_cache_file(file):
|
|
@@ -272,17 +263,18 @@ def _read_cache_file(file):
|
|
|
272
263
|
return json.load(open(file))
|
|
273
264
|
except (json.JSONDecodeError, OSError) as error:
|
|
274
265
|
# a corrupt cache file must not break every query
|
|
275
|
-
logger.warning(f"unreadable cache file {file}: {error} --
|
|
276
|
-
return
|
|
266
|
+
logger.warning(f"unreadable cache file {file}: {error} -- ignored")
|
|
267
|
+
return None
|
|
277
268
|
|
|
278
269
|
|
|
279
|
-
def _write_cache_file(
|
|
280
|
-
"""atomic replace, so an interrupted write cannot truncate the
|
|
281
|
-
|
|
270
|
+
def _write_cache_file(data, file):
|
|
271
|
+
"""atomic replace, so an interrupted write cannot truncate the file"""
|
|
272
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
273
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(file),
|
|
282
274
|
prefix=os.path.basename(file) + '.', suffix='.tmp')
|
|
283
275
|
try:
|
|
284
276
|
with os.fdopen(fd, 'w') as f:
|
|
285
|
-
json.dump(
|
|
277
|
+
json.dump(data, f)
|
|
286
278
|
os.replace(tmp, file)
|
|
287
279
|
except BaseException:
|
|
288
280
|
with contextlib.suppress(OSError):
|
|
@@ -290,33 +282,59 @@ def _write_cache_file(cache, file):
|
|
|
290
282
|
raise
|
|
291
283
|
|
|
292
284
|
|
|
293
|
-
def
|
|
285
|
+
def _migrate_legacy_cache(legacy, folder, migrate):
|
|
286
|
+
"""split a cache of previous versions (one json file for all keys) into
|
|
287
|
+
the per-key files; the legacy file is kept as <legacy>.bak"""
|
|
288
|
+
data = _read_cache_file(legacy)
|
|
289
|
+
if data:
|
|
290
|
+
logger.info(f'migrate cache {legacy} to {folder}')
|
|
291
|
+
for key, value in data.items():
|
|
292
|
+
if migrate:
|
|
293
|
+
value = migrate(value)
|
|
294
|
+
if value is not None:
|
|
295
|
+
# lower-case: DOIs are case-insensitive, and looked up in lower case
|
|
296
|
+
_write_cache_file({'value': value}, _cache_path(folder, key.lower()))
|
|
297
|
+
with contextlib.suppress(OSError): # another process may have migrated it
|
|
298
|
+
os.replace(legacy, legacy + '.bak')
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
NOT_FOUND_TTL = 24 * 3600 # seconds
|
|
294
302
|
|
|
295
|
-
file = os.path.join(CACHE_DIR, file)
|
|
296
303
|
|
|
304
|
+
def cached(name, hashed_key=False, legacy=None, migrate=None, trim=None, not_found=None):
|
|
305
|
+
"""cache the results of fun(key) as json files under CACHE_DIR/name/
|
|
306
|
+
|
|
307
|
+
legacy: cache file of previous versions, migrated on first use (with
|
|
308
|
+
migrate(value) -> value, or None to drop it)
|
|
309
|
+
trim: applied to results before they are stored
|
|
310
|
+
not_found: exception class raised by fun when the key does not exist:
|
|
311
|
+
it is remembered (and raised again) for NOT_FOUND_TTL seconds
|
|
312
|
+
"""
|
|
297
313
|
def decorator(fun):
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
_init_cache()
|
|
304
|
-
cache = _read_cache_file(file)
|
|
314
|
+
def decorated(arg):
|
|
315
|
+
key = arg
|
|
316
|
+
folder = os.path.join(CACHE_DIR, name)
|
|
317
|
+
if legacy and not DRYRUN and os.path.exists(os.path.join(CACHE_DIR, legacy)):
|
|
318
|
+
_migrate_legacy_cache(os.path.join(CACHE_DIR, legacy), folder, migrate)
|
|
305
319
|
if hashed_key: # use hashed parameter as key (for full text query)
|
|
306
|
-
key = hashlib.sha256(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if
|
|
310
|
-
logger.debug('load from cache: '+repr((
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
key = hashlib.sha256(arg.encode('utf-8')).hexdigest()[:16]
|
|
321
|
+
file = _cache_path(folder, key)
|
|
322
|
+
data = _read_cache_file(file)
|
|
323
|
+
if data and 'value' in data:
|
|
324
|
+
logger.debug('load from cache: '+repr((name, key)))
|
|
325
|
+
return data['value']
|
|
326
|
+
if data and not_found and time.time() - data.get('not_found', 0) < NOT_FOUND_TTL:
|
|
327
|
+
raise not_found(f'{key}: not found (cached until {time.ctime(data["not_found"] + NOT_FOUND_TTL)})')
|
|
328
|
+
try:
|
|
329
|
+
res = fun(arg)
|
|
330
|
+
except Exception as error:
|
|
331
|
+
if not_found and isinstance(error, not_found) and not DRYRUN:
|
|
332
|
+
_write_cache_file({'not_found': time.time()}, file)
|
|
333
|
+
raise
|
|
334
|
+
if trim:
|
|
335
|
+
res = trim(res)
|
|
336
|
+
if not DRYRUN:
|
|
337
|
+
_write_cache_file({'value': res}, file)
|
|
320
338
|
return res
|
|
321
339
|
return decorated
|
|
322
340
|
return decorator
|