cachier 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
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.
- cachier/__init__.py +2 -2
- cachier/__main__.py +1 -1
- cachier/_types.py +9 -0
- cachier/config.py +103 -0
- cachier/core.py +66 -132
- cachier/cores/base.py +40 -25
- cachier/cores/memory.py +16 -13
- cachier/cores/mongo.py +47 -35
- cachier/cores/pickle.py +79 -109
- cachier/version.info +1 -1
- {cachier-2.3.0.dist-info → cachier-3.0.1.dist-info}/METADATA +39 -22
- cachier-3.0.1.dist-info/RECORD +19 -0
- {cachier-2.3.0.dist-info → cachier-3.0.1.dist-info}/WHEEL +1 -1
- cachier-3.0.1.dist-info/entry_points.txt +2 -0
- cachier-2.3.0.dist-info/RECORD +0 -17
- cachier-2.3.0.dist-info/entry_points.txt +0 -2
- {cachier-2.3.0.dist-info → cachier-3.0.1.dist-info}/LICENSE +0 -0
- {cachier-2.3.0.dist-info → cachier-3.0.1.dist-info}/top_level.txt +0 -0
cachier/__init__.py
CHANGED
cachier/__main__.py
CHANGED
cachier/_types.py
ADDED
cachier/config.py
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
import datetime
|
2
|
+
import hashlib
|
3
|
+
import os
|
4
|
+
import pickle
|
5
|
+
from typing import Optional, TypedDict, Union
|
6
|
+
|
7
|
+
from ._types import Backend, HashFunc, Mongetter
|
8
|
+
|
9
|
+
|
10
|
+
def _default_hash_func(args, kwds):
|
11
|
+
# Sort the kwargs to ensure consistent ordering
|
12
|
+
sorted_kwargs = sorted(kwds.items())
|
13
|
+
# Serialize args and sorted_kwargs using pickle or similar
|
14
|
+
serialized = pickle.dumps((args, sorted_kwargs))
|
15
|
+
# Create a hash of the serialized data
|
16
|
+
return hashlib.sha256(serialized).hexdigest()
|
17
|
+
|
18
|
+
|
19
|
+
class Params(TypedDict):
|
20
|
+
"""Type definition for cachier parameters."""
|
21
|
+
|
22
|
+
caching_enabled: bool
|
23
|
+
hash_func: HashFunc
|
24
|
+
backend: Backend
|
25
|
+
mongetter: Optional[Mongetter]
|
26
|
+
stale_after: datetime.timedelta
|
27
|
+
next_time: bool
|
28
|
+
cache_dir: Union[str, os.PathLike]
|
29
|
+
pickle_reload: bool
|
30
|
+
separate_files: bool
|
31
|
+
wait_for_calc_timeout: int
|
32
|
+
allow_none: bool
|
33
|
+
|
34
|
+
|
35
|
+
_default_params: Params = {
|
36
|
+
"caching_enabled": True,
|
37
|
+
"hash_func": _default_hash_func,
|
38
|
+
"backend": "pickle",
|
39
|
+
"mongetter": None,
|
40
|
+
"stale_after": datetime.timedelta.max,
|
41
|
+
"next_time": False,
|
42
|
+
"cache_dir": "~/.cachier/",
|
43
|
+
"pickle_reload": True,
|
44
|
+
"separate_files": False,
|
45
|
+
"wait_for_calc_timeout": 0,
|
46
|
+
"allow_none": False,
|
47
|
+
}
|
48
|
+
|
49
|
+
|
50
|
+
def _update_with_defaults(
|
51
|
+
param, name: str, func_kwargs: Optional[dict] = None
|
52
|
+
):
|
53
|
+
import cachier
|
54
|
+
|
55
|
+
if func_kwargs:
|
56
|
+
kw_name = f"cachier__{name}"
|
57
|
+
if kw_name in func_kwargs:
|
58
|
+
return func_kwargs.pop(kw_name)
|
59
|
+
if param is None:
|
60
|
+
return cachier.config._default_params[name]
|
61
|
+
return param
|
62
|
+
|
63
|
+
|
64
|
+
def set_default_params(**params):
|
65
|
+
"""Configure global parameters applicable to all memoized functions.
|
66
|
+
|
67
|
+
This function takes the same keyword parameters as the ones defined in the
|
68
|
+
decorator, which can be passed all at once or with multiple calls.
|
69
|
+
Parameters given directly to a decorator take precedence over any values
|
70
|
+
set by this function.
|
71
|
+
|
72
|
+
Only 'stale_after', 'next_time', and 'wait_for_calc_timeout' can be changed
|
73
|
+
after the memoization decorator has been applied. Other parameters will
|
74
|
+
only have an effect on decorators applied after this function is run.
|
75
|
+
|
76
|
+
"""
|
77
|
+
import cachier
|
78
|
+
|
79
|
+
valid_params = (
|
80
|
+
p for p in params.items() if p[0] in cachier.config._default_params
|
81
|
+
)
|
82
|
+
_default_params.update(valid_params)
|
83
|
+
|
84
|
+
|
85
|
+
def get_default_params():
|
86
|
+
"""Get current set of default parameters."""
|
87
|
+
import cachier
|
88
|
+
|
89
|
+
return cachier.config._default_params
|
90
|
+
|
91
|
+
|
92
|
+
def enable_caching():
|
93
|
+
"""Enable caching globally."""
|
94
|
+
import cachier
|
95
|
+
|
96
|
+
cachier.config._default_params["caching_enabled"] = True
|
97
|
+
|
98
|
+
|
99
|
+
def disable_caching():
|
100
|
+
"""Disable caching globally."""
|
101
|
+
import cachier
|
102
|
+
|
103
|
+
cachier.config._default_params["caching_enabled"] = False
|
cachier/core.py
CHANGED
@@ -7,28 +7,28 @@
|
|
7
7
|
# http://www.opensource.org/licenses/MIT-license
|
8
8
|
# Copyright (c) 2016, Shay Palachy <shaypal5@gmail.com>
|
9
9
|
|
10
|
-
# python 2 compatibility
|
11
|
-
|
12
10
|
import datetime
|
13
|
-
import hashlib
|
14
11
|
import inspect
|
15
12
|
import os
|
16
|
-
import
|
13
|
+
import warnings
|
17
14
|
from collections import OrderedDict
|
18
15
|
from concurrent.futures import ThreadPoolExecutor
|
19
16
|
from functools import wraps
|
20
|
-
from typing import
|
17
|
+
from typing import Optional, Union
|
21
18
|
from warnings import warn
|
22
19
|
|
20
|
+
from .config import (
|
21
|
+
Backend,
|
22
|
+
HashFunc,
|
23
|
+
Mongetter,
|
24
|
+
_default_params,
|
25
|
+
_update_with_defaults,
|
26
|
+
)
|
23
27
|
from .cores.base import RecalculationNeeded, _BaseCore
|
24
28
|
from .cores.memory import _MemoryCore
|
25
29
|
from .cores.mongo import _MongoCore
|
26
30
|
from .cores.pickle import _PickleCore
|
27
31
|
|
28
|
-
if TYPE_CHECKING:
|
29
|
-
import pymongo.collection
|
30
|
-
|
31
|
-
|
32
32
|
MAX_WORKERS_ENVAR_NAME = "CACHIER_MAX_WORKERS"
|
33
33
|
DEFAULT_MAX_WORKERS = 8
|
34
34
|
|
@@ -68,15 +68,6 @@ def _calc_entry(core, key, func, args, kwds):
|
|
68
68
|
core.mark_entry_not_calculated(key)
|
69
69
|
|
70
70
|
|
71
|
-
def _default_hash_func(args, kwds):
|
72
|
-
# Sort the kwargs to ensure consistent ordering
|
73
|
-
sorted_kwargs = sorted(kwds.items())
|
74
|
-
# Serialize args and sorted_kwargs using pickle or similar
|
75
|
-
serialized = pickle.dumps((args, sorted_kwargs))
|
76
|
-
# Create a hash of the serialized data
|
77
|
-
return hashlib.sha256(serialized).hexdigest()
|
78
|
-
|
79
|
-
|
80
71
|
def _convert_args_kwargs(
|
81
72
|
func, _is_method: bool, args: tuple, kwds: dict
|
82
73
|
) -> dict:
|
@@ -84,7 +75,7 @@ def _convert_args_kwargs(
|
|
84
75
|
# unwrap if the function is functools.partial
|
85
76
|
if hasattr(func, "func"):
|
86
77
|
args = func.args + args
|
87
|
-
kwds
|
78
|
+
kwds.update({k: v for k, v in func.keywords.items() if k not in kwds})
|
88
79
|
func = func.func
|
89
80
|
func_params = list(inspect.signature(func).parameters)
|
90
81
|
args_as_kw = dict(
|
@@ -103,42 +94,15 @@ def _convert_args_kwargs(
|
|
103
94
|
return OrderedDict(sorted(kwargs.items()))
|
104
95
|
|
105
96
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
class Params(TypedDict):
|
116
|
-
caching_enabled: bool
|
117
|
-
hash_func: HashFunc
|
118
|
-
backend: Backend
|
119
|
-
mongetter: Optional[Mongetter]
|
120
|
-
stale_after: datetime.timedelta
|
121
|
-
next_time: bool
|
122
|
-
cache_dir: Union[str, os.PathLike]
|
123
|
-
pickle_reload: bool
|
124
|
-
separate_files: bool
|
125
|
-
wait_for_calc_timeout: int
|
126
|
-
allow_none: bool
|
127
|
-
|
128
|
-
|
129
|
-
_default_params: Params = {
|
130
|
-
"caching_enabled": True,
|
131
|
-
"hash_func": _default_hash_func,
|
132
|
-
"backend": "pickle",
|
133
|
-
"mongetter": None,
|
134
|
-
"stale_after": datetime.timedelta.max,
|
135
|
-
"next_time": False,
|
136
|
-
"cache_dir": "~/.cachier/",
|
137
|
-
"pickle_reload": True,
|
138
|
-
"separate_files": False,
|
139
|
-
"wait_for_calc_timeout": 0,
|
140
|
-
"allow_none": False,
|
141
|
-
}
|
97
|
+
def _pop_kwds_with_deprecation(kwds, name: str, default_value: bool):
|
98
|
+
if name in kwds:
|
99
|
+
warnings.warn(
|
100
|
+
f"`{name}` is deprecated and will be removed in a future release,"
|
101
|
+
" use `cachier__` alternative instead.",
|
102
|
+
DeprecationWarning,
|
103
|
+
stacklevel=2,
|
104
|
+
)
|
105
|
+
return kwds.pop(name, default_value)
|
142
106
|
|
143
107
|
|
144
108
|
def cachier(
|
@@ -154,7 +118,7 @@ def cachier(
|
|
154
118
|
wait_for_calc_timeout: Optional[int] = None,
|
155
119
|
allow_none: Optional[bool] = None,
|
156
120
|
):
|
157
|
-
"""
|
121
|
+
"""Wrap as a persistent, stale-free memoization decorator.
|
158
122
|
|
159
123
|
The positional and keyword arguments to the wrapped function must be
|
160
124
|
hashable (i.e. Python's immutable built-in objects, not mutable
|
@@ -163,13 +127,14 @@ def cachier(
|
|
163
127
|
value is their id), equal objects across different sessions will not yield
|
164
128
|
identical keys.
|
165
129
|
|
166
|
-
Arguments
|
130
|
+
Arguments:
|
167
131
|
---------
|
168
132
|
hash_func : callable, optional
|
169
133
|
A callable that gets the args and kwargs from the decorated function
|
170
134
|
and returns a hash key for them. This parameter can be used to enable
|
171
135
|
the use of cachier with functions that get arguments that are not
|
172
136
|
automatically hashable by Python.
|
137
|
+
hash_params : callable, optional
|
173
138
|
backend : str, optional
|
174
139
|
The name of the backend to use. Valid options currently include
|
175
140
|
'pickle', 'mongo' and 'memory'. If not provided, defaults to
|
@@ -219,13 +184,12 @@ def cachier(
|
|
219
184
|
)
|
220
185
|
warn(message, DeprecationWarning, stacklevel=2)
|
221
186
|
hash_func = hash_params
|
187
|
+
# Update parameters with defaults if input is None
|
188
|
+
backend = _update_with_defaults(backend, "backend")
|
189
|
+
mongetter = _update_with_defaults(mongetter, "mongetter")
|
222
190
|
# Override the backend parameter if a mongetter is provided.
|
223
|
-
if mongetter is None:
|
224
|
-
mongetter = _default_params["mongetter"]
|
225
191
|
if callable(mongetter):
|
226
192
|
backend = "mongo"
|
227
|
-
if backend is None:
|
228
|
-
backend = _default_params["backend"]
|
229
193
|
core: _BaseCore
|
230
194
|
if backend == "pickle":
|
231
195
|
core = _PickleCore(
|
@@ -234,23 +198,16 @@ def cachier(
|
|
234
198
|
cache_dir=cache_dir,
|
235
199
|
separate_files=separate_files,
|
236
200
|
wait_for_calc_timeout=wait_for_calc_timeout,
|
237
|
-
default_params=_default_params,
|
238
201
|
)
|
239
202
|
elif backend == "mongo":
|
240
|
-
if mongetter is None:
|
241
|
-
raise MissingMongetter(
|
242
|
-
"must specify ``mongetter`` when using the mongo core"
|
243
|
-
)
|
244
203
|
core = _MongoCore(
|
245
|
-
mongetter=mongetter,
|
246
204
|
hash_func=hash_func,
|
205
|
+
mongetter=mongetter,
|
247
206
|
wait_for_calc_timeout=wait_for_calc_timeout,
|
248
|
-
default_params=_default_params,
|
249
207
|
)
|
250
208
|
elif backend == "memory":
|
251
209
|
core = _MemoryCore(
|
252
|
-
hash_func=hash_func,
|
253
|
-
default_params=_default_params,
|
210
|
+
hash_func=hash_func, wait_for_calc_timeout=wait_for_calc_timeout
|
254
211
|
)
|
255
212
|
else:
|
256
213
|
raise ValueError("specified an invalid core: %s" % backend)
|
@@ -261,26 +218,39 @@ def cachier(
|
|
261
218
|
@wraps(func)
|
262
219
|
def func_wrapper(*args, **kwds):
|
263
220
|
nonlocal allow_none
|
264
|
-
_allow_none = (
|
265
|
-
allow_none
|
266
|
-
if allow_none is not None
|
267
|
-
else _default_params["allow_none"]
|
268
|
-
)
|
221
|
+
_allow_none = _update_with_defaults(allow_none, "allow_none", kwds)
|
269
222
|
# print('Inside general wrapper for {}.'.format(func.__name__))
|
270
|
-
ignore_cache =
|
271
|
-
|
272
|
-
|
223
|
+
ignore_cache = _pop_kwds_with_deprecation(
|
224
|
+
kwds, "ignore_cache", False
|
225
|
+
)
|
226
|
+
overwrite_cache = _pop_kwds_with_deprecation(
|
227
|
+
kwds, "overwrite_cache", False
|
228
|
+
)
|
229
|
+
verbose = _pop_kwds_with_deprecation(kwds, "verbose_cache", False)
|
230
|
+
ignore_cache = kwds.pop("cachier__skip_cache", ignore_cache)
|
231
|
+
overwrite_cache = kwds.pop(
|
232
|
+
"cachier__overwrite_cache", overwrite_cache
|
233
|
+
)
|
234
|
+
verbose = kwds.pop("cachier__verbose", verbose)
|
235
|
+
_stale_after = _update_with_defaults(
|
236
|
+
stale_after, "stale_after", kwds
|
237
|
+
)
|
238
|
+
_next_time = _update_with_defaults(next_time, "next_time", kwds)
|
273
239
|
# merge args expanded as kwargs and the original kwds
|
274
240
|
kwargs = _convert_args_kwargs(
|
275
241
|
func, _is_method=core.func_is_method, args=args, kwds=kwds
|
276
242
|
)
|
277
243
|
|
278
244
|
_print = lambda x: None # noqa: E731
|
279
|
-
if
|
245
|
+
if verbose:
|
280
246
|
_print = print
|
281
247
|
if ignore_cache or not _default_params["caching_enabled"]:
|
282
|
-
return
|
283
|
-
|
248
|
+
return (
|
249
|
+
func(args[0], **kwargs)
|
250
|
+
if core.func_is_method
|
251
|
+
else func(**kwargs)
|
252
|
+
)
|
253
|
+
key, entry = core.get_entry((), kwargs)
|
284
254
|
if overwrite_cache:
|
285
255
|
return _calc_entry(core, key, func, args, kwds)
|
286
256
|
if entry is None:
|
@@ -289,17 +259,13 @@ def cachier(
|
|
289
259
|
_print("Entry found.")
|
290
260
|
if _allow_none or entry.get("value", None) is not None:
|
291
261
|
_print("Cached result found.")
|
292
|
-
local_stale_after = (
|
293
|
-
stale_after or _default_params["stale_after"]
|
294
|
-
)
|
295
|
-
local_next_time = next_time or _default_params["next_time"] # noqa: E501
|
296
262
|
now = datetime.datetime.now()
|
297
|
-
if now - entry["time"] <=
|
263
|
+
if now - entry["time"] <= _stale_after:
|
298
264
|
_print("And it is fresh!")
|
299
265
|
return entry["value"]
|
300
266
|
_print("But it is stale... :(")
|
301
267
|
if entry["being_calculated"]:
|
302
|
-
if
|
268
|
+
if _next_time:
|
303
269
|
_print("Returning stale.")
|
304
270
|
return entry["value"] # return stale val
|
305
271
|
_print("Already calc. Waiting on change.")
|
@@ -307,7 +273,7 @@ def cachier(
|
|
307
273
|
return core.wait_on_entry_calc(key)
|
308
274
|
except RecalculationNeeded:
|
309
275
|
return _calc_entry(core, key, func, args, kwds)
|
310
|
-
if
|
276
|
+
if _next_time:
|
311
277
|
_print("Async calc and return stale")
|
312
278
|
try:
|
313
279
|
core.mark_entry_being_calculated(key)
|
@@ -328,22 +294,22 @@ def cachier(
|
|
328
294
|
_print("No entry found. No current calc. Calling like a boss.")
|
329
295
|
return _calc_entry(core, key, func, args, kwds)
|
330
296
|
|
331
|
-
def
|
297
|
+
def _clear_cache():
|
332
298
|
"""Clear the cache."""
|
333
299
|
core.clear_cache()
|
334
300
|
|
335
|
-
def
|
336
|
-
"""
|
301
|
+
def _clear_being_calculated():
|
302
|
+
"""Mark all entries in this cache as not being calculated."""
|
337
303
|
core.clear_being_calculated()
|
338
304
|
|
339
|
-
def
|
340
|
-
"""
|
305
|
+
def _cache_dpath():
|
306
|
+
"""Return the path to the cache dir, if exists; None if not."""
|
341
307
|
return getattr(core, "cache_dir", None)
|
342
308
|
|
343
|
-
def
|
309
|
+
def _precache_value(*args, value_to_cache, **kwds): # noqa: D417
|
344
310
|
"""Add an initial value to the cache.
|
345
311
|
|
346
|
-
Arguments
|
312
|
+
Arguments:
|
347
313
|
---------
|
348
314
|
value_to_cache : any
|
349
315
|
entry to be written into the cache
|
@@ -353,44 +319,12 @@ def cachier(
|
|
353
319
|
kwargs = _convert_args_kwargs(
|
354
320
|
func, _is_method=core.func_is_method, args=args, kwds=kwds
|
355
321
|
)
|
356
|
-
return core.precache_value(
|
322
|
+
return core.precache_value((), kwargs, value_to_cache)
|
357
323
|
|
358
|
-
func_wrapper.clear_cache =
|
359
|
-
func_wrapper.clear_being_calculated =
|
360
|
-
func_wrapper.cache_dpath =
|
361
|
-
func_wrapper.precache_value =
|
324
|
+
func_wrapper.clear_cache = _clear_cache
|
325
|
+
func_wrapper.clear_being_calculated = _clear_being_calculated
|
326
|
+
func_wrapper.cache_dpath = _cache_dpath
|
327
|
+
func_wrapper.precache_value = _precache_value
|
362
328
|
return func_wrapper
|
363
329
|
|
364
330
|
return _cachier_decorator
|
365
|
-
|
366
|
-
|
367
|
-
def set_default_params(**params):
|
368
|
-
"""Configure global parameters applicable to all memoized functions.
|
369
|
-
|
370
|
-
This function takes the same keyword parameters as the ones defined in the
|
371
|
-
decorator, which can be passed all at once or with multiple calls.
|
372
|
-
Parameters given directly to a decorator take precedence over any values
|
373
|
-
set by this function.
|
374
|
-
|
375
|
-
Only 'stale_after', 'next_time', and 'wait_for_calc_timeout' can be changed
|
376
|
-
after the memoization decorator has been applied. Other parameters will
|
377
|
-
only have an effect on decorators applied after this function is run.
|
378
|
-
|
379
|
-
"""
|
380
|
-
valid_params = (p for p in params.items() if p[0] in _default_params)
|
381
|
-
_default_params.update(valid_params)
|
382
|
-
|
383
|
-
|
384
|
-
def get_default_params():
|
385
|
-
"""Get current set of default parameters."""
|
386
|
-
return _default_params
|
387
|
-
|
388
|
-
|
389
|
-
def enable_caching():
|
390
|
-
"""Enable caching globally."""
|
391
|
-
_default_params["caching_enabled"] = True
|
392
|
-
|
393
|
-
|
394
|
-
def disable_caching():
|
395
|
-
"""Disable caching globally."""
|
396
|
-
_default_params["caching_enabled"] = False
|
cachier/cores/base.py
CHANGED
@@ -8,21 +8,33 @@
|
|
8
8
|
|
9
9
|
import abc # for the _BaseCore abstract base class
|
10
10
|
import inspect
|
11
|
+
import threading
|
12
|
+
from typing import Callable
|
13
|
+
|
14
|
+
from .._types import HashFunc
|
15
|
+
from ..config import _update_with_defaults
|
11
16
|
|
12
17
|
|
13
18
|
class RecalculationNeeded(Exception):
|
19
|
+
"""Exception raised when a recalculation is needed."""
|
20
|
+
|
14
21
|
pass
|
15
22
|
|
16
23
|
|
24
|
+
def _get_func_str(func: Callable) -> str:
|
25
|
+
return f".{func.__module__}.{func.__name__}"
|
26
|
+
|
27
|
+
|
17
28
|
class _BaseCore:
|
18
29
|
__metaclass__ = abc.ABCMeta
|
19
30
|
|
20
|
-
def __init__(self, hash_func,
|
21
|
-
self.
|
22
|
-
self.
|
31
|
+
def __init__(self, hash_func: HashFunc, wait_for_calc_timeout: int):
|
32
|
+
self.hash_func = _update_with_defaults(hash_func, "hash_func")
|
33
|
+
self.wait_for_calc_timeout = wait_for_calc_timeout
|
34
|
+
self.lock = threading.RLock()
|
23
35
|
|
24
36
|
def set_func(self, func):
|
25
|
-
"""
|
37
|
+
"""Set the function this core will use.
|
26
38
|
|
27
39
|
This has to be set before any method is called. Also determine if the
|
28
40
|
function is an object method.
|
@@ -36,59 +48,62 @@ class _BaseCore:
|
|
36
48
|
self.func = func
|
37
49
|
|
38
50
|
def get_key(self, args, kwds):
|
39
|
-
"""
|
40
|
-
|
41
|
-
return self.hash_func(args, kwds)
|
42
|
-
else:
|
43
|
-
return self.default_params["hash_func"](args, kwds)
|
51
|
+
"""Return a unique key based on the arguments provided."""
|
52
|
+
return self.hash_func(args, kwds)
|
44
53
|
|
45
54
|
def get_entry(self, args, kwds):
|
46
|
-
"""
|
47
|
-
|
55
|
+
"""Get entry based on given arguments.
|
56
|
+
|
57
|
+
Return the result mapped to the given arguments in this core's cache,
|
58
|
+
if such a mapping exists.
|
59
|
+
|
60
|
+
"""
|
48
61
|
key = self.get_key(args, kwds)
|
49
62
|
return self.get_entry_by_key(key)
|
50
63
|
|
51
64
|
def precache_value(self, args, kwds, value_to_cache):
|
52
|
-
"""
|
65
|
+
"""Write a precomputed value into the cache."""
|
53
66
|
key = self.get_key(args, kwds)
|
54
67
|
self.set_entry(key, value_to_cache)
|
55
68
|
return value_to_cache
|
56
69
|
|
57
70
|
def check_calc_timeout(self, time_spent):
|
58
71
|
"""Raise an exception if a recalculation is needed."""
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
calc_timeout = self.default_params["wait_for_calc_timeout"]
|
72
|
+
calc_timeout = _update_with_defaults(
|
73
|
+
self.wait_for_calc_timeout, "wait_for_calc_timeout"
|
74
|
+
)
|
63
75
|
if calc_timeout > 0 and (time_spent >= calc_timeout):
|
64
76
|
raise RecalculationNeeded()
|
65
77
|
|
66
78
|
@abc.abstractmethod
|
67
79
|
def get_entry_by_key(self, key):
|
68
|
-
"""
|
69
|
-
|
80
|
+
"""Get entry based on given key.
|
81
|
+
|
82
|
+
Return the result mapped to the given key in this core's cache, if such
|
83
|
+
a mapping exists.
|
84
|
+
|
85
|
+
"""
|
70
86
|
|
71
87
|
@abc.abstractmethod
|
72
88
|
def set_entry(self, key, func_res):
|
73
|
-
"""
|
89
|
+
"""Map the given result to the given key in this core's cache."""
|
74
90
|
|
75
91
|
@abc.abstractmethod
|
76
92
|
def mark_entry_being_calculated(self, key):
|
77
|
-
"""
|
93
|
+
"""Mark the entry mapped by the given key as being calculated."""
|
78
94
|
|
79
95
|
@abc.abstractmethod
|
80
96
|
def mark_entry_not_calculated(self, key):
|
81
|
-
"""
|
97
|
+
"""Mark the entry mapped by the given key as not being calculated."""
|
82
98
|
|
83
99
|
@abc.abstractmethod
|
84
100
|
def wait_on_entry_calc(self, key):
|
85
|
-
"""
|
86
|
-
result."""
|
101
|
+
"""Wait on the entry with keys being calculated and returns result."""
|
87
102
|
|
88
103
|
@abc.abstractmethod
|
89
104
|
def clear_cache(self):
|
90
|
-
"""
|
105
|
+
"""Clear the cache of this core."""
|
91
106
|
|
92
107
|
@abc.abstractmethod
|
93
108
|
def clear_being_calculated(self):
|
94
|
-
"""
|
109
|
+
"""Mark all entries in this cache as not being calculated."""
|
cachier/cores/memory.py
CHANGED
@@ -3,20 +3,23 @@
|
|
3
3
|
import threading
|
4
4
|
from datetime import datetime
|
5
5
|
|
6
|
-
from
|
6
|
+
from .._types import HashFunc
|
7
|
+
from .base import _BaseCore, _get_func_str
|
7
8
|
|
8
9
|
|
9
10
|
class _MemoryCore(_BaseCore):
|
10
11
|
"""The memory core class for cachier."""
|
11
12
|
|
12
|
-
def __init__(self, hash_func,
|
13
|
-
super().__init__(hash_func,
|
13
|
+
def __init__(self, hash_func: HashFunc, wait_for_calc_timeout: int):
|
14
|
+
super().__init__(hash_func, wait_for_calc_timeout)
|
14
15
|
self.cache = {}
|
15
|
-
|
16
|
+
|
17
|
+
def _hash_func_key(self, key):
|
18
|
+
return f"{_get_func_str(self.func)}:{key}"
|
16
19
|
|
17
20
|
def get_entry_by_key(self, key, reload=False):
|
18
21
|
with self.lock:
|
19
|
-
return key, self.cache.get(key, None)
|
22
|
+
return key, self.cache.get(self._hash_func_key(key), None)
|
20
23
|
|
21
24
|
def set_entry(self, key, func_res):
|
22
25
|
with self.lock:
|
@@ -24,10 +27,10 @@ class _MemoryCore(_BaseCore):
|
|
24
27
|
# we need to retain the existing condition so that
|
25
28
|
# mark_entry_not_calculated can notify all possibly-waiting
|
26
29
|
# threads about it
|
27
|
-
cond = self.cache[key]["condition"]
|
30
|
+
cond = self.cache[self._hash_func_key(key)]["condition"]
|
28
31
|
except KeyError: # pragma: no cover
|
29
32
|
cond = None
|
30
|
-
self.cache[key] = {
|
33
|
+
self.cache[self._hash_func_key(key)] = {
|
31
34
|
"value": func_res,
|
32
35
|
"time": datetime.now(),
|
33
36
|
"stale": False,
|
@@ -40,10 +43,10 @@ class _MemoryCore(_BaseCore):
|
|
40
43
|
condition = threading.Condition()
|
41
44
|
# condition.acquire()
|
42
45
|
try:
|
43
|
-
self.cache[key]["being_calculated"] = True
|
44
|
-
self.cache[key]["condition"] = condition
|
46
|
+
self.cache[self._hash_func_key(key)]["being_calculated"] = True
|
47
|
+
self.cache[self._hash_func_key(key)]["condition"] = condition
|
45
48
|
except KeyError:
|
46
|
-
self.cache[key] = {
|
49
|
+
self.cache[self._hash_func_key(key)] = {
|
47
50
|
"value": None,
|
48
51
|
"time": datetime.now(),
|
49
52
|
"stale": False,
|
@@ -54,7 +57,7 @@ class _MemoryCore(_BaseCore):
|
|
54
57
|
def mark_entry_not_calculated(self, key):
|
55
58
|
with self.lock:
|
56
59
|
try:
|
57
|
-
entry = self.cache[key]
|
60
|
+
entry = self.cache[self._hash_func_key(key)]
|
58
61
|
except KeyError: # pragma: no cover
|
59
62
|
return # that's ok, we don't need an entry in that case
|
60
63
|
entry["being_calculated"] = False
|
@@ -67,13 +70,13 @@ class _MemoryCore(_BaseCore):
|
|
67
70
|
|
68
71
|
def wait_on_entry_calc(self, key):
|
69
72
|
with self.lock: # pragma: no cover
|
70
|
-
entry = self.cache[key]
|
73
|
+
entry = self.cache[self._hash_func_key(key)]
|
71
74
|
if not entry["being_calculated"]:
|
72
75
|
return entry["value"]
|
73
76
|
entry["condition"].acquire()
|
74
77
|
entry["condition"].wait()
|
75
78
|
entry["condition"].release()
|
76
|
-
return self.cache[key]["value"]
|
79
|
+
return self.cache[self._hash_func_key(key)]["value"]
|
77
80
|
|
78
81
|
def clear_cache(self):
|
79
82
|
with self.lock:
|
cachier/cores/mongo.py
CHANGED
@@ -14,31 +14,45 @@ import warnings # to warn if pymongo is missing
|
|
14
14
|
from contextlib import suppress
|
15
15
|
from datetime import datetime
|
16
16
|
|
17
|
+
from .._types import HashFunc, Mongetter
|
18
|
+
|
17
19
|
with suppress(ImportError):
|
18
20
|
from bson.binary import Binary # to save binary data to mongodb
|
19
21
|
from pymongo import ASCENDING, IndexModel
|
20
22
|
from pymongo.errors import OperationFailure
|
21
23
|
|
22
|
-
from .base import RecalculationNeeded, _BaseCore
|
24
|
+
from .base import RecalculationNeeded, _BaseCore, _get_func_str
|
23
25
|
|
24
26
|
MONGO_SLEEP_DURATION_IN_SEC = 1
|
25
27
|
|
26
28
|
|
29
|
+
class MissingMongetter(ValueError):
|
30
|
+
"""Thrown when the mongetter keyword argument is missing."""
|
31
|
+
|
32
|
+
|
27
33
|
class _MongoCore(_BaseCore):
|
28
34
|
_INDEX_NAME = "func_1_key_1"
|
29
35
|
|
30
36
|
def __init__(
|
31
|
-
self,
|
37
|
+
self,
|
38
|
+
hash_func: HashFunc,
|
39
|
+
mongetter: Mongetter,
|
40
|
+
wait_for_calc_timeout: int,
|
32
41
|
):
|
33
42
|
if "pymongo" not in sys.modules:
|
34
43
|
warnings.warn(
|
35
|
-
"
|
36
|
-
|
44
|
+
"`pymongo` was not found. MongoDB cores will not function.",
|
45
|
+
ImportWarning,
|
46
|
+
stacklevel=2,
|
37
47
|
) # pragma: no cover
|
38
|
-
|
48
|
+
|
49
|
+
super().__init__(hash_func, wait_for_calc_timeout)
|
50
|
+
if mongetter is None:
|
51
|
+
raise MissingMongetter(
|
52
|
+
"must specify ``mongetter`` when using the mongo core"
|
53
|
+
)
|
39
54
|
self.mongetter = mongetter
|
40
55
|
self.mongo_collection = self.mongetter()
|
41
|
-
self.wait_for_calc_timeout = wait_for_calc_timeout
|
42
56
|
index_inf = self.mongo_collection.index_information()
|
43
57
|
if _MongoCore._INDEX_NAME not in index_inf:
|
44
58
|
func1key1 = IndexModel(
|
@@ -47,39 +61,39 @@ class _MongoCore(_BaseCore):
|
|
47
61
|
)
|
48
62
|
self.mongo_collection.create_indexes([func1key1])
|
49
63
|
|
50
|
-
@
|
51
|
-
def
|
52
|
-
return
|
64
|
+
@property
|
65
|
+
def _func_str(self) -> str:
|
66
|
+
return _get_func_str(self.func)
|
53
67
|
|
54
68
|
def get_entry_by_key(self, key):
|
55
69
|
res = self.mongo_collection.find_one(
|
56
|
-
{"func":
|
70
|
+
{"func": self._func_str, "key": key}
|
57
71
|
)
|
58
|
-
if res:
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
return key,
|
72
|
+
if not res:
|
73
|
+
return key, None
|
74
|
+
try:
|
75
|
+
entry = {
|
76
|
+
"value": pickle.loads(res["value"]), # noqa: S301
|
77
|
+
"time": res.get("time", None),
|
78
|
+
"stale": res.get("stale", False),
|
79
|
+
"being_calculated": res.get("being_calculated", False),
|
80
|
+
}
|
81
|
+
except KeyError:
|
82
|
+
entry = {
|
83
|
+
"value": None,
|
84
|
+
"time": res.get("time", None),
|
85
|
+
"stale": res.get("stale", False),
|
86
|
+
"being_calculated": res.get("being_calculated", False),
|
87
|
+
}
|
88
|
+
return key, entry
|
75
89
|
|
76
90
|
def set_entry(self, key, func_res):
|
77
91
|
thebytes = pickle.dumps(func_res)
|
78
92
|
self.mongo_collection.update_one(
|
79
|
-
filter={"func":
|
93
|
+
filter={"func": self._func_str, "key": key},
|
80
94
|
update={
|
81
95
|
"$set": {
|
82
|
-
"func":
|
96
|
+
"func": self._func_str,
|
83
97
|
"key": key,
|
84
98
|
"value": Binary(thebytes),
|
85
99
|
"time": datetime.now(),
|
@@ -92,7 +106,7 @@ class _MongoCore(_BaseCore):
|
|
92
106
|
|
93
107
|
def mark_entry_being_calculated(self, key):
|
94
108
|
self.mongo_collection.update_one(
|
95
|
-
filter={"func":
|
109
|
+
filter={"func": self._func_str, "key": key},
|
96
110
|
update={"$set": {"being_calculated": True}},
|
97
111
|
upsert=True,
|
98
112
|
)
|
@@ -101,7 +115,7 @@ class _MongoCore(_BaseCore):
|
|
101
115
|
with suppress(OperationFailure): # don't care in this case
|
102
116
|
self.mongo_collection.update_one(
|
103
117
|
filter={
|
104
|
-
"func":
|
118
|
+
"func": self._func_str,
|
105
119
|
"key": key,
|
106
120
|
},
|
107
121
|
update={"$set": {"being_calculated": False}},
|
@@ -121,14 +135,12 @@ class _MongoCore(_BaseCore):
|
|
121
135
|
self.check_calc_timeout(time_spent)
|
122
136
|
|
123
137
|
def clear_cache(self):
|
124
|
-
self.mongo_collection.delete_many(
|
125
|
-
filter={"func": _MongoCore._get_func_str(self.func)}
|
126
|
-
)
|
138
|
+
self.mongo_collection.delete_many(filter={"func": self._func_str})
|
127
139
|
|
128
140
|
def clear_being_calculated(self):
|
129
141
|
self.mongo_collection.update_many(
|
130
142
|
filter={
|
131
|
-
"func":
|
143
|
+
"func": self._func_str,
|
132
144
|
"being_calculated": True,
|
133
145
|
},
|
134
146
|
update={"$set": {"being_calculated": False}},
|
cachier/cores/pickle.py
CHANGED
@@ -8,7 +8,6 @@
|
|
8
8
|
# Copyright (c) 2016, Shay Palachy <shaypal5@gmail.com>
|
9
9
|
import os
|
10
10
|
import pickle # for local caching
|
11
|
-
import threading
|
12
11
|
from contextlib import suppress
|
13
12
|
from datetime import datetime
|
14
13
|
|
@@ -16,21 +15,15 @@ import portalocker # to lock on pickle cache IO
|
|
16
15
|
from watchdog.events import PatternMatchingEventHandler
|
17
16
|
from watchdog.observers import Observer
|
18
17
|
|
18
|
+
from .._types import HashFunc
|
19
|
+
from ..config import _update_with_defaults
|
20
|
+
|
19
21
|
# Alternative: https://github.com/WoLpH/portalocker
|
20
22
|
from .base import _BaseCore
|
21
23
|
|
22
24
|
|
23
25
|
class _PickleCore(_BaseCore):
|
24
|
-
"""The pickle core class for cachier.
|
25
|
-
|
26
|
-
Parameters
|
27
|
-
----------
|
28
|
-
pickle_reload : bool, optional
|
29
|
-
See core.cachier() documentation.
|
30
|
-
cache_dir : str, optional.
|
31
|
-
See core.cachier() documentation.
|
32
|
-
|
33
|
-
"""
|
26
|
+
"""The pickle core class for cachier."""
|
34
27
|
|
35
28
|
class CacheChangeHandler(PatternMatchingEventHandler):
|
36
29
|
"""Handles cache-file modification events."""
|
@@ -69,75 +62,51 @@ class _PickleCore(_BaseCore):
|
|
69
62
|
self.observer.stop()
|
70
63
|
|
71
64
|
def on_created(self, event):
|
72
|
-
"""A Watchdog Event Handler method."""
|
65
|
+
"""A Watchdog Event Handler method.""" # noqa: D401
|
73
66
|
self._check_calculation() # pragma: no cover
|
74
67
|
|
75
68
|
def on_modified(self, event):
|
76
|
-
"""A Watchdog Event Handler method."""
|
69
|
+
"""A Watchdog Event Handler method.""" # noqa: D401
|
77
70
|
self._check_calculation()
|
78
71
|
|
79
72
|
def __init__(
|
80
73
|
self,
|
81
|
-
hash_func,
|
82
|
-
pickle_reload,
|
83
|
-
cache_dir,
|
84
|
-
separate_files,
|
85
|
-
wait_for_calc_timeout,
|
86
|
-
default_params,
|
74
|
+
hash_func: HashFunc,
|
75
|
+
pickle_reload: bool,
|
76
|
+
cache_dir: str,
|
77
|
+
separate_files: bool,
|
78
|
+
wait_for_calc_timeout: int,
|
87
79
|
):
|
88
|
-
super().__init__(hash_func,
|
80
|
+
super().__init__(hash_func, wait_for_calc_timeout)
|
89
81
|
self.cache = None
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
self.
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
def _cache_fname(self):
|
110
|
-
if self.cache_fname is None:
|
111
|
-
self.cache_fname = (
|
112
|
-
f".{self.func.__module__}.{self.func.__qualname__}"
|
113
|
-
)
|
114
|
-
self.cache_fname = self.cache_fname.replace("<", "_").replace(
|
115
|
-
">", "_"
|
116
|
-
)
|
117
|
-
return self.cache_fname
|
118
|
-
|
119
|
-
def _cache_fpath(self):
|
120
|
-
if self.cache_fpath is None:
|
121
|
-
if not os.path.exists(self.cache_dir):
|
122
|
-
os.makedirs(self.cache_dir)
|
123
|
-
self.cache_fpath = os.path.abspath(
|
124
|
-
os.path.join(
|
125
|
-
os.path.realpath(self.cache_dir),
|
126
|
-
self._cache_fname(),
|
127
|
-
)
|
128
|
-
)
|
129
|
-
return self.cache_fpath
|
82
|
+
self.reload = _update_with_defaults(pickle_reload, "pickle_reload")
|
83
|
+
self.cache_dir = os.path.expanduser(
|
84
|
+
_update_with_defaults(cache_dir, "cache_dir")
|
85
|
+
)
|
86
|
+
self.separate_files = _update_with_defaults(
|
87
|
+
separate_files, "separate_files"
|
88
|
+
)
|
89
|
+
|
90
|
+
@property
|
91
|
+
def cache_fname(self) -> str:
|
92
|
+
fname = f".{self.func.__module__}.{self.func.__qualname__}"
|
93
|
+
return fname.replace("<", "_").replace(">", "_")
|
94
|
+
|
95
|
+
@property
|
96
|
+
def cache_fpath(self) -> str:
|
97
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
98
|
+
return os.path.abspath(
|
99
|
+
os.path.join(os.path.realpath(self.cache_dir), self.cache_fname)
|
100
|
+
)
|
130
101
|
|
131
102
|
def _reload_cache(self):
|
132
103
|
with self.lock:
|
133
|
-
fpath = self._cache_fpath()
|
134
104
|
try:
|
135
|
-
with portalocker.Lock(
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
except FileNotFoundError:
|
105
|
+
with portalocker.Lock(
|
106
|
+
self.cache_fpath, mode="rb"
|
107
|
+
) as cache_file:
|
108
|
+
self.cache = pickle.load(cache_file) # noqa: S301
|
109
|
+
except (FileNotFoundError, EOFError):
|
141
110
|
self.cache = {}
|
142
111
|
|
143
112
|
def _get_cache(self):
|
@@ -146,9 +115,9 @@ class _PickleCore(_BaseCore):
|
|
146
115
|
self._reload_cache()
|
147
116
|
return self.cache
|
148
117
|
|
149
|
-
def _get_cache_by_key(self, key=None,
|
150
|
-
fpath = self.
|
151
|
-
fpath += f"_{
|
118
|
+
def _get_cache_by_key(self, key=None, hash_str=None):
|
119
|
+
fpath = self.cache_fpath
|
120
|
+
fpath += f"_{hash_str or key}"
|
152
121
|
try:
|
153
122
|
with portalocker.Lock(fpath, mode="rb") as cache_file:
|
154
123
|
return pickle.load(cache_file) # noqa: S301
|
@@ -156,30 +125,28 @@ class _PickleCore(_BaseCore):
|
|
156
125
|
return None
|
157
126
|
|
158
127
|
def _clear_all_cache_files(self):
|
159
|
-
|
160
|
-
path, name = os.path.split(fpath)
|
128
|
+
path, name = os.path.split(self.cache_fpath)
|
161
129
|
for subpath in os.listdir(path):
|
162
130
|
if subpath.startswith(f"{name}_"):
|
163
131
|
os.remove(os.path.join(path, subpath))
|
164
132
|
|
165
133
|
def _clear_being_calculated_all_cache_files(self):
|
166
|
-
|
167
|
-
path, name = os.path.split(fpath)
|
134
|
+
path, name = os.path.split(self.cache_fpath)
|
168
135
|
for subpath in os.listdir(path):
|
169
136
|
if subpath.startswith(name):
|
170
|
-
entry = self._get_cache_by_key(
|
137
|
+
entry = self._get_cache_by_key(hash_str=subpath.split("_")[-1])
|
171
138
|
if entry is not None:
|
172
139
|
entry["being_calculated"] = False
|
173
|
-
self._save_cache(entry,
|
174
|
-
|
175
|
-
def _save_cache(self, cache, key=None,
|
140
|
+
self._save_cache(entry, hash_str=subpath.split("_")[-1])
|
141
|
+
|
142
|
+
def _save_cache(self, cache, key=None, hash_str=None):
|
143
|
+
fpath = self.cache_fpath
|
144
|
+
if key is not None:
|
145
|
+
fpath += f"_{key}"
|
146
|
+
elif hash_str is not None:
|
147
|
+
fpath += f"_{hash_str}"
|
176
148
|
with self.lock:
|
177
149
|
self.cache = cache
|
178
|
-
fpath = self._cache_fpath()
|
179
|
-
if key is not None:
|
180
|
-
fpath += f"_{key}"
|
181
|
-
elif hash is not None:
|
182
|
-
fpath += f"_{hash}"
|
183
150
|
with portalocker.Lock(fpath, mode="wb") as cache_file:
|
184
151
|
pickle.dump(cache, cache_file, protocol=4)
|
185
152
|
if key is None:
|
@@ -202,11 +169,12 @@ class _PickleCore(_BaseCore):
|
|
202
169
|
}
|
203
170
|
if self.separate_files:
|
204
171
|
self._save_cache(key_data, key)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
172
|
+
return # pragma: no cover
|
173
|
+
|
174
|
+
with self.lock:
|
175
|
+
cache = self._get_cache()
|
176
|
+
cache[key] = key_data
|
177
|
+
self._save_cache(cache)
|
210
178
|
|
211
179
|
def mark_entry_being_calculated_separate_files(self, key):
|
212
180
|
self._save_cache(
|
@@ -227,19 +195,20 @@ class _PickleCore(_BaseCore):
|
|
227
195
|
def mark_entry_being_calculated(self, key):
|
228
196
|
if self.separate_files:
|
229
197
|
self.mark_entry_being_calculated_separate_files(key)
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
198
|
+
return # pragma: no cover
|
199
|
+
|
200
|
+
with self.lock:
|
201
|
+
cache = self._get_cache()
|
202
|
+
try:
|
203
|
+
cache[key]["being_calculated"] = True
|
204
|
+
except KeyError:
|
205
|
+
cache[key] = {
|
206
|
+
"value": None,
|
207
|
+
"time": datetime.now(),
|
208
|
+
"stale": False,
|
209
|
+
"being_calculated": True,
|
210
|
+
}
|
211
|
+
self._save_cache(cache)
|
243
212
|
|
244
213
|
def mark_entry_not_calculated(self, key):
|
245
214
|
if self.separate_files:
|
@@ -254,12 +223,12 @@ class _PickleCore(_BaseCore):
|
|
254
223
|
def wait_on_entry_calc(self, key):
|
255
224
|
if self.separate_files:
|
256
225
|
entry = self._get_cache_by_key(key)
|
257
|
-
filename = f"{self.
|
226
|
+
filename = f"{self.cache_fname}_{key}"
|
258
227
|
else:
|
259
228
|
with self.lock:
|
260
229
|
self._reload_cache()
|
261
230
|
entry = self._get_cache()[key]
|
262
|
-
filename = self.
|
231
|
+
filename = self.cache_fname
|
263
232
|
if not entry["being_calculated"]:
|
264
233
|
return entry["value"]
|
265
234
|
event_handler = _PickleCore.CacheChangeHandler(
|
@@ -285,9 +254,10 @@ class _PickleCore(_BaseCore):
|
|
285
254
|
def clear_being_calculated(self):
|
286
255
|
if self.separate_files:
|
287
256
|
self._clear_being_calculated_all_cache_files()
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
257
|
+
return # pragma: no cover
|
258
|
+
|
259
|
+
with self.lock:
|
260
|
+
cache = self._get_cache()
|
261
|
+
for key in cache:
|
262
|
+
cache[key]["being_calculated"] = False
|
263
|
+
self._save_cache(cache)
|
cachier/version.info
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.1
|
@@ -1,33 +1,50 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cachier
|
3
|
-
Version:
|
3
|
+
Version: 3.0.1
|
4
4
|
Summary: Persistent, stale-free, local and cross-machine caching for Python functions.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
Author-email: Shay Palachy Affek <shay.palachy@gmail.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2016 Shay Palachy
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: Source, https://github.com/python-cachier/cachier
|
29
|
+
Keywords: cache,caching,cross-machine,decorator,local,memoization,mongo,persistent
|
13
30
|
Classifier: Development Status :: 4 - Beta
|
31
|
+
Classifier: Intended Audience :: Developers
|
14
32
|
Classifier: License :: OSI Approved :: MIT License
|
15
33
|
Classifier: Programming Language :: Python
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
17
35
|
Classifier: Programming Language :: Python :: 3.8
|
18
36
|
Classifier: Programming Language :: Python :: 3.9
|
19
37
|
Classifier: Programming Language :: Python :: 3.10
|
20
38
|
Classifier: Programming Language :: Python :: 3.11
|
21
39
|
Classifier: Programming Language :: Python :: 3.12
|
40
|
+
Classifier: Topic :: Other/Nonlisted Topic
|
22
41
|
Classifier: Topic :: Software Development :: Libraries
|
23
42
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
24
43
|
Classifier: Topic :: Utilities
|
25
|
-
|
26
|
-
Classifier: Intended Audience :: Developers
|
44
|
+
Description-Content-Type: text/x-rst
|
27
45
|
License-File: LICENSE
|
28
|
-
Requires-Dist:
|
29
|
-
Requires-Dist:
|
30
|
-
Requires-Dist: setuptools >=67.6.0
|
46
|
+
Requires-Dist: portalocker>=2.3.2
|
47
|
+
Requires-Dist: watchdog>=2.3.1
|
31
48
|
|
32
49
|
Cachier
|
33
50
|
#######
|
@@ -267,7 +284,7 @@ Cachier also accepts several keyword arguments in the calls of the function it w
|
|
267
284
|
Ignore Cache
|
268
285
|
~~~~~~~~~~~~
|
269
286
|
|
270
|
-
You can have ``cachier`` ignore any existing cache for a specific function call by passing ``
|
287
|
+
You can have ``cachier`` ignore any existing cache for a specific function call by passing ``cachier__skip_cache=True`` to the function call. The cache will neither be checked nor updated with the new return value.
|
271
288
|
|
272
289
|
.. code-block:: python
|
273
290
|
|
@@ -276,17 +293,17 @@ You can have ``cachier`` ignore any existing cache for a specific function call
|
|
276
293
|
return first_num + second_num
|
277
294
|
|
278
295
|
def main():
|
279
|
-
print(sum(5, 3,
|
296
|
+
print(sum(5, 3, cachier__skip_cache=True))
|
280
297
|
|
281
298
|
Overwrite Cache
|
282
299
|
~~~~~~~~~~~~~~~
|
283
300
|
|
284
|
-
You can have ``cachier`` overwrite an existing cache entry - if one exists - for a specific function call by passing ``
|
301
|
+
You can have ``cachier`` overwrite an existing cache entry - if one exists - for a specific function call by passing ``cachier__overwrite_cache=True`` to the function call. The cache will not be checked but will be updated with the new return value.
|
285
302
|
|
286
303
|
Verbose Cache Call
|
287
304
|
~~~~~~~~~~~~~~~~~~
|
288
305
|
|
289
|
-
You can have ``cachier`` print out a detailed explanation of the logic of a specific call by passing ``
|
306
|
+
You can have ``cachier`` print out a detailed explanation of the logic of a specific call by passing ``cachier__verbose=True`` to the function call. This can be useful if you are not sure why a certain function result is, or is not, returned.
|
290
307
|
|
291
308
|
Cache `None` Values
|
292
309
|
~~~~~~~~~~~~~~~~~~~
|
@@ -435,7 +452,7 @@ To run all tests EXCEPT memory core AND MongoDB core related tests, use:
|
|
435
452
|
Running MongoDB tests against a live MongoDB instance
|
436
453
|
-----------------------------------------------------
|
437
454
|
|
438
|
-
**Note to developers:** By default, all MongoDB tests are run against a mocked MongoDB instance, provided by the ``pymongo_inmemory`` package. To run them against a live MongoDB instance, the ``
|
455
|
+
**Note to developers:** By default, all MongoDB tests are run against a mocked MongoDB instance, provided by the ``pymongo_inmemory`` package. To run them against a live MongoDB instance, the ``CACHIER_TEST_VS_DOCKERIZED_MONGO`` environment variable is set to ``True`` in the ``test`` environment of this repository (and additional environment variables are populated with the appropriate credentials), used by the GitHub Action running tests on every commit and pull request.
|
439
456
|
|
440
457
|
Contributors are not expected to run these tests against a live MongoDB instance when developing, as credentials for the testing instance used will NOT be shared, but rather use the testing against the in-memory MongoDB instance as a good proxy.
|
441
458
|
|
@@ -492,8 +509,8 @@ Notable bugfixers:
|
|
492
509
|
.. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/cachier.svg
|
493
510
|
:target: https://pypi.python.org/pypi/cachier
|
494
511
|
|
495
|
-
.. |Build-Status| image:: https://github.com/python-cachier/cachier/actions/workflows/test.yml/badge.svg
|
496
|
-
:target: https://github.com/python-cachier/cachier/actions/workflows/test.yml
|
512
|
+
.. |Build-Status| image:: https://github.com/python-cachier/cachier/actions/workflows/ci-test.yml/badge.svg
|
513
|
+
:target: https://github.com/python-cachier/cachier/actions/workflows/ci-test.yml
|
497
514
|
|
498
515
|
.. |LICENCE| image:: https://img.shields.io/pypi/l/cachier.svg
|
499
516
|
:target: https://pypi.python.org/pypi/cachier
|
@@ -0,0 +1,19 @@
|
|
1
|
+
cachier/__init__.py,sha256=6i43oFM_ZTp47b6R8Ws2vvf2JXLvbn-TKzWESwBDb-M,304
|
2
|
+
cachier/__main__.py,sha256=upg-TlHs1vngKYvkjoPpl3Pvl6xOx4ut-M1mElMiAo0,443
|
3
|
+
cachier/_types.py,sha256=EGJMiw-oCIC_cDLyzw7YC40lfo8jnD3zMmoJpA9Y8Iw,238
|
4
|
+
cachier/_version.py,sha256=yE6UYwvdoIRpw3HmNifiGwV3fqVea5PwZj_EvyosiZ8,1079
|
5
|
+
cachier/config.py,sha256=JJutAeOlNjDobNWKteTvcvdj_Pfemk6_dMKIGFtW6UM,2754
|
6
|
+
cachier/core.py,sha256=x0I-Ot2dt7wThu2j8iLXPYvv8exwp4xXMUQIQyImr64,13261
|
7
|
+
cachier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
cachier/version.info,sha256=BuP-SnoYhNINgcxwZcqM_Uw5TsYCxNGA3cObvU99pSE,6
|
9
|
+
cachier/cores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
cachier/cores/base.py,sha256=i6xlR3Em_s6rVER5Uyf1kK86KcaWtb84iKwS9bgDZGI,3465
|
11
|
+
cachier/cores/memory.py,sha256=JFXnXBAeP0nOXOf_vNgpcEuRNjxre8WQjiUBa7yEYmY,3128
|
12
|
+
cachier/cores/mongo.py,sha256=Ez3SNZzAzELO9c_SrGlOX855-5UEPBViabWPjkQnFc0,4917
|
13
|
+
cachier/cores/pickle.py,sha256=fVajLzlhUkQ9ochLWAZnDQQZJPXj0lWl7mgalI-z2x0,8903
|
14
|
+
cachier-3.0.1.dist-info/LICENSE,sha256=-2WrMJkIa0gVP6YQHXXDT7ws-S3M2NEVEF4XF3K8qrY,1069
|
15
|
+
cachier-3.0.1.dist-info/METADATA,sha256=JZCfPStwVwwhYrdvT_Vkmm2FrfqYc2rUGtExGh86DGI,20102
|
16
|
+
cachier-3.0.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
17
|
+
cachier-3.0.1.dist-info/entry_points.txt,sha256=x4Y7t6Y0Qev_3fgG-Jv7TrsvVdJty3FnGAdkT8-_5mY,49
|
18
|
+
cachier-3.0.1.dist-info/top_level.txt,sha256=_rW_HiJumDCch67YT-WAgzcyvKg5RiYDMZq9d-0ZpaE,8
|
19
|
+
cachier-3.0.1.dist-info/RECORD,,
|
cachier-2.3.0.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
cachier/__init__.py,sha256=OvhjKkhTHuE9ESuBWLks0g6eGto9jKsJOr6FMFKQzXM,289
|
2
|
-
cachier/__main__.py,sha256=g8dgovKdDgOMgNIpnmzVRuI5Dj5xODTav45TpZlH_iA,429
|
3
|
-
cachier/_version.py,sha256=yE6UYwvdoIRpw3HmNifiGwV3fqVea5PwZj_EvyosiZ8,1079
|
4
|
-
cachier/core.py,sha256=xB0kWRey5wL5XeTMTRe4lMpIukJmc2xWsOgUQGr8Q_c,14965
|
5
|
-
cachier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
cachier/version.info,sha256=B-GHcA-4RPUOEZ4BIL3emPJuZ5J9E1aQPTmIXKecyHE,7
|
7
|
-
cachier/cores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
cachier/cores/base.py,sha256=Sy0Tzi2We-op31bsickPiSM1U0neND1hvgjAkjKEUKg,3194
|
9
|
-
cachier/cores/memory.py,sha256=sbit0EQNqZLqIZMKLJ7sNXfTkG63s68vKJpTl49E6Kc,2816
|
10
|
-
cachier/cores/mongo.py,sha256=LMfLawErv7WJQVMwgca1jg6Qu7rK7tO7_9Zi1BHrodM,4829
|
11
|
-
cachier/cores/pickle.py,sha256=tNrOQ61n2i6KC-xAw5K3Ewz_6Cegz4n3NtvRl_lqLnM,9903
|
12
|
-
cachier-2.3.0.dist-info/LICENSE,sha256=-2WrMJkIa0gVP6YQHXXDT7ws-S3M2NEVEF4XF3K8qrY,1069
|
13
|
-
cachier-2.3.0.dist-info/METADATA,sha256=06wPKvqCL4K1ujV0dFz6GLU4t75GFpVq8QMZHm2X2PQ,18839
|
14
|
-
cachier-2.3.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
15
|
-
cachier-2.3.0.dist-info/entry_points.txt,sha256=16JU8wc6a62BLuOXVNjAiXJRp0AB5LtEwiKGYMjZjv0,49
|
16
|
-
cachier-2.3.0.dist-info/top_level.txt,sha256=_rW_HiJumDCch67YT-WAgzcyvKg5RiYDMZq9d-0ZpaE,8
|
17
|
-
cachier-2.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|