CacheToolsUtils 9.2__py3-none-any.whl → 10.0__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.
- {CacheToolsUtils-9.2.dist-info → CacheToolsUtils-10.0.dist-info}/METADATA +8 -3
- CacheToolsUtils-10.0.dist-info/RECORD +6 -0
- CacheToolsUtils.py +129 -28
- CacheToolsUtils-9.2.dist-info/RECORD +0 -6
- {CacheToolsUtils-9.2.dist-info → CacheToolsUtils-10.0.dist-info}/LICENSE +0 -0
- {CacheToolsUtils-9.2.dist-info → CacheToolsUtils-10.0.dist-info}/WHEEL +0 -0
- {CacheToolsUtils-9.2.dist-info → CacheToolsUtils-10.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CacheToolsUtils
|
|
3
|
-
Version:
|
|
3
|
+
Version: 10.0
|
|
4
4
|
Summary: Cachetools Utilities
|
|
5
5
|
Author-email: Fabien Coelho <cachetools.utils@coelho.net>
|
|
6
6
|
License: CC0
|
|
@@ -34,6 +34,8 @@ Requires-Dist: sphinx_rtd_theme; extra == "doc"
|
|
|
34
34
|
Requires-Dist: sphinx-autoapi; extra == "doc"
|
|
35
35
|
Requires-Dist: sphinx-lint; extra == "doc"
|
|
36
36
|
Requires-Dist: myst_parser; extra == "doc"
|
|
37
|
+
Provides-Extra: crypt
|
|
38
|
+
Requires-Dist: pycryptodome; extra == "crypt"
|
|
37
39
|
Provides-Extra: tests
|
|
38
40
|
Requires-Dist: pymemcache; extra == "tests"
|
|
39
41
|
Requires-Dist: redis; extra == "tests"
|
|
@@ -51,7 +53,7 @@ Classes to add key prefix and stats to
|
|
|
51
53
|
and other cache-related utils.
|
|
52
54
|
|
|
53
55
|

|
|
54
|
-

|
|
55
57
|

|
|
56
58
|

|
|
57
59
|

|
|
@@ -85,13 +87,16 @@ with `cachetools`.
|
|
|
85
87
|
- `LockedCache` use a (thread) lock to control cache accesses.
|
|
86
88
|
- `TwoLevelCache` allows to combine two caches.
|
|
87
89
|
- `DebugCache` to trace cache calls using `logging`.
|
|
90
|
+
- `EncryptedCache` a cache with key hashing and value encryption.
|
|
91
|
+
- `ToBytesCache` map keys and values to bytes.
|
|
88
92
|
|
|
89
93
|
### Cache utilities
|
|
90
94
|
|
|
91
95
|
- `cached` decorator: a cachetools replacement which allows to test if a
|
|
92
96
|
function result is in cache, and to delete such an entry.
|
|
93
97
|
- `cacheFunctions` and `cacheMethods`: add caching to functions or methods.
|
|
94
|
-
- `json_key`
|
|
98
|
+
- `json_key`, `hash_json_key`, `full_hash_key`: convenient JSON-based cache key
|
|
99
|
+
serialization functions for `cached`.
|
|
95
100
|
|
|
96
101
|
## License
|
|
97
102
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
CacheToolsUtils.py,sha256=RSwc1mMDaQo_Sa-tE54q5iUoEVg_-74JU4ykvUHhHTY,23684
|
|
2
|
+
CacheToolsUtils-10.0.dist-info/LICENSE,sha256=y_yFW1bmBAUFCF-ZDyDiq10Ykns_19If5pFnZUzYgeU,33
|
|
3
|
+
CacheToolsUtils-10.0.dist-info/METADATA,sha256=-BR63iaEPl9rIzOilTkrNCtVgQF7S-nME_asJWnIQqM,4706
|
|
4
|
+
CacheToolsUtils-10.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
5
|
+
CacheToolsUtils-10.0.dist-info/top_level.txt,sha256=qCyE4jGot5mOmvIQrZueCnPtbYtRe2hqke4sGojm3zI,16
|
|
6
|
+
CacheToolsUtils-10.0.dist-info/RECORD,,
|
CacheToolsUtils.py
CHANGED
|
@@ -5,6 +5,8 @@ CacheTools Utilities. This code is public domain.
|
|
|
5
5
|
from typing import Any, Callable, MutableMapping
|
|
6
6
|
import abc
|
|
7
7
|
|
|
8
|
+
import base64
|
|
9
|
+
import hashlib
|
|
8
10
|
import cachetools
|
|
9
11
|
import json
|
|
10
12
|
|
|
@@ -380,10 +382,10 @@ def cached(cache, *args, **kwargs):
|
|
|
380
382
|
|
|
381
383
|
def decorate(fun: Callable):
|
|
382
384
|
|
|
383
|
-
# use cachetools
|
|
385
|
+
# use cachetools "cached" decorator
|
|
384
386
|
fun = cachetools.cached(cache, *args, **kwargs)(fun)
|
|
385
387
|
|
|
386
|
-
# extend
|
|
388
|
+
# extend it with two functions
|
|
387
389
|
def cache_in(*args, **kwargs) -> bool:
|
|
388
390
|
"""Tell whether key is already in cache."""
|
|
389
391
|
key = fun.cache_key(*args, **kwargs) # type: ignore
|
|
@@ -409,12 +411,19 @@ def cached(cache, *args, **kwargs):
|
|
|
409
411
|
MapGen = Callable[[MutableMapping, str], MutableMapping]
|
|
410
412
|
|
|
411
413
|
|
|
412
|
-
def cacheMethods(
|
|
414
|
+
def cacheMethods(
|
|
415
|
+
cache: MutableMapping,
|
|
416
|
+
obj: Any,
|
|
417
|
+
gen: MapGen = PrefixedCache,
|
|
418
|
+
opts: dict[str, Any] = {},
|
|
419
|
+
**funs
|
|
420
|
+
):
|
|
413
421
|
"""Cache some object methods.
|
|
414
422
|
|
|
415
423
|
:param cache: cache to use.
|
|
416
424
|
:param obj: object instance to be cached.
|
|
417
425
|
:param gen: generator of PrefixedCache.
|
|
426
|
+
:param opts: additional parameters when calling `cached`.
|
|
418
427
|
:param funs: name of methods and corresponding prefix
|
|
419
428
|
|
|
420
429
|
.. code-block:: python
|
|
@@ -426,17 +435,22 @@ def cacheMethods(cache: MutableMapping, obj: Any, gen: MapGen = PrefixedCache, *
|
|
|
426
435
|
f = getattr(obj, fun)
|
|
427
436
|
while hasattr(f, "__wrapped__"):
|
|
428
437
|
f = f.__wrapped__
|
|
429
|
-
setattr(obj, fun, cached(cache=gen(cache, prefix))(f))
|
|
438
|
+
setattr(obj, fun, cached(cache=gen(cache, prefix), **opts)(f))
|
|
430
439
|
|
|
431
440
|
|
|
432
441
|
def cacheFunctions(
|
|
433
|
-
cache: MutableMapping,
|
|
442
|
+
cache: MutableMapping,
|
|
443
|
+
globs: MutableMapping[str, Any],
|
|
444
|
+
gen: MapGen = PrefixedCache,
|
|
445
|
+
opts: dict[str, Any] = {},
|
|
446
|
+
**funs
|
|
434
447
|
):
|
|
435
448
|
"""Cache some global functions, with a prefix.
|
|
436
449
|
|
|
437
450
|
:param cache: cache to use.
|
|
438
451
|
:param globs: global object dictionary.
|
|
439
452
|
:param gen: generator of PrefixedCache.
|
|
453
|
+
:param opts: additional parameters when calling `cached`.
|
|
440
454
|
:param funs: name of functions and corresponding prefix
|
|
441
455
|
|
|
442
456
|
.. code-block:: python
|
|
@@ -448,12 +462,14 @@ def cacheFunctions(
|
|
|
448
462
|
f = globs[fun]
|
|
449
463
|
while hasattr(f, "__wrapped__"):
|
|
450
464
|
f = f.__wrapped__
|
|
451
|
-
globs[fun] = cached(cache=gen(cache, prefix))(f)
|
|
465
|
+
globs[fun] = cached(cache=gen(cache, prefix), **opts)(f)
|
|
452
466
|
|
|
453
467
|
|
|
454
468
|
# JSON-based key function
|
|
455
469
|
# NOTE obviously this only works if parameters are json-serializable…
|
|
456
|
-
def json_key(*args, **kwargs):
|
|
470
|
+
def json_key(*args, **kwargs) -> str:
|
|
471
|
+
"""JSON serialization of arguments."""
|
|
472
|
+
val: Any
|
|
457
473
|
if kwargs: # object
|
|
458
474
|
if args:
|
|
459
475
|
val = {"*": args, "**": kwargs}
|
|
@@ -464,19 +480,17 @@ def json_key(*args, **kwargs):
|
|
|
464
480
|
return json.dumps(val, sort_keys=True, separators=(",", ":"))
|
|
465
481
|
|
|
466
482
|
|
|
467
|
-
# Hmmm…
|
|
483
|
+
# Hmmm… is this useful?
|
|
468
484
|
class _HashJsonKey:
|
|
469
485
|
"""A cache key with a persistant hash value."""
|
|
470
486
|
|
|
471
487
|
def __init__(self, *args, **kwargs):
|
|
472
|
-
self._hashed = None
|
|
473
488
|
self._args = args
|
|
474
489
|
self._kwargs = kwargs
|
|
475
490
|
self._key = json_key(*args, **kwargs)
|
|
491
|
+
self._hashed = self._key.__hash__()
|
|
476
492
|
|
|
477
493
|
def __hash__(self):
|
|
478
|
-
if self._hashed is None:
|
|
479
|
-
self._hashed = self._key.__hash__()
|
|
480
494
|
return self._hashed
|
|
481
495
|
|
|
482
496
|
def __str__(self):
|
|
@@ -487,9 +501,100 @@ def hash_json_key(*args, **kwargs):
|
|
|
487
501
|
return _HashJsonKey(*args, **kwargs)
|
|
488
502
|
|
|
489
503
|
|
|
504
|
+
def full_hash_key(*args, **kwargs) -> str:
|
|
505
|
+
"""Reduce arguments to a single 128-bits hash."""
|
|
506
|
+
skey = json_key(*args, **kwargs)
|
|
507
|
+
hkey = hashlib.sha3_256(skey.encode("UTF-8")).digest()[:16]
|
|
508
|
+
return base64.b85encode(hkey).decode("ASCII")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
#
|
|
512
|
+
# Encrypted Cache
|
|
513
|
+
#
|
|
514
|
+
class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
515
|
+
"""Encrypted Bytes Key-Value Cache.
|
|
516
|
+
|
|
517
|
+
:param secret: bytes of secret, at least 16 bytes.
|
|
518
|
+
:param hsize: size of hashed key, default is 16.
|
|
519
|
+
|
|
520
|
+
The key is *not* encrypted but simply hashed, thus they are
|
|
521
|
+
fixed size with a very low collision probability.
|
|
522
|
+
|
|
523
|
+
By design, the clear-text key is needed to recover the value,
|
|
524
|
+
as each value is encrypted with its own key.
|
|
525
|
+
|
|
526
|
+
There is no integrity check on the value.
|
|
527
|
+
|
|
528
|
+
Algorithms:
|
|
529
|
+
- SHA3: hash/key/nonce derivation.
|
|
530
|
+
- Salsa20: value encryption.
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16):
|
|
534
|
+
self._cache = cache
|
|
535
|
+
assert len(secret) >= 16
|
|
536
|
+
self._secret = secret
|
|
537
|
+
assert 8 <= hsize <= 24
|
|
538
|
+
self._hsize = hsize
|
|
539
|
+
from Crypto.Cipher import Salsa20
|
|
540
|
+
self._cipher = Salsa20
|
|
541
|
+
|
|
542
|
+
def _keydev(self, key):
|
|
543
|
+
hkey = hashlib.sha3_512(key + self._secret).digest()
|
|
544
|
+
sz = self._hsize
|
|
545
|
+
return (hkey[:sz], hkey[sz:sz+32], hkey[sz+32:sz+40])
|
|
546
|
+
|
|
547
|
+
def _key(self, key):
|
|
548
|
+
return self._keydev(key)[0]
|
|
549
|
+
|
|
550
|
+
def __setitem__(self, key, val):
|
|
551
|
+
hkey, vkey, vnonce = self._keydev(key)
|
|
552
|
+
self._cache[hkey] = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
|
|
553
|
+
|
|
554
|
+
def __getitem__(self, key):
|
|
555
|
+
hkey, vkey, vnonce = self._keydev(key)
|
|
556
|
+
return self._cipher.new(key=vkey, nonce=vnonce).decrypt(self._cache[hkey])
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
560
|
+
"""Map bytes to strings."""
|
|
561
|
+
|
|
562
|
+
def __init__(self, cache):
|
|
563
|
+
self._cache = cache
|
|
564
|
+
|
|
565
|
+
def _key(self, key):
|
|
566
|
+
# assert isinstance(key, bytes)
|
|
567
|
+
return base64.b85encode(key).decode("ASCII")
|
|
568
|
+
|
|
569
|
+
def __setitem__(self, key, val):
|
|
570
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
571
|
+
|
|
572
|
+
def __getitem__(self, key):
|
|
573
|
+
val = self._cache.__getitem__(self._key(key))
|
|
574
|
+
# assert isinstance(val, str)
|
|
575
|
+
return base64.b85decode(val)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class ToBytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
579
|
+
"""Map (JSON-serializable) cache keys and values to bytes."""
|
|
580
|
+
|
|
581
|
+
def __init__(self, cache):
|
|
582
|
+
self._cache = cache
|
|
583
|
+
|
|
584
|
+
def _key(self, key):
|
|
585
|
+
return json.dumps(key, sort_keys=True, separators=(",", ":")).encode("UTF-8")
|
|
586
|
+
|
|
587
|
+
def __setitem__(self, key, val):
|
|
588
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
589
|
+
|
|
590
|
+
def __getitem__(self, key):
|
|
591
|
+
return json.loads(self._cache.__getitem__(self._key(key)).decode("UTF-8"))
|
|
592
|
+
|
|
593
|
+
|
|
490
594
|
#
|
|
491
595
|
# MEMCACHED
|
|
492
596
|
#
|
|
597
|
+
# FIXME what about bytes?
|
|
493
598
|
class JsonSerde:
|
|
494
599
|
"""JSON serialize/deserialize class for MemCached (``pymemcache``).
|
|
495
600
|
|
|
@@ -504,8 +609,10 @@ class JsonSerde:
|
|
|
504
609
|
def serialize(self, key, value):
|
|
505
610
|
if isinstance(value, str):
|
|
506
611
|
return value.encode("utf-8"), 1
|
|
612
|
+
elif isinstance(value, bytes):
|
|
613
|
+
return value, 3
|
|
507
614
|
else:
|
|
508
|
-
return json.dumps(value, sort_keys=True).encode("utf-8"), 2
|
|
615
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 2
|
|
509
616
|
|
|
510
617
|
# reverse previous serialization
|
|
511
618
|
def deserialize(self, key, value, flag):
|
|
@@ -513,6 +620,8 @@ class JsonSerde:
|
|
|
513
620
|
return value.decode("utf-8")
|
|
514
621
|
elif flag == 2:
|
|
515
622
|
return json.loads(value.decode("utf-8"))
|
|
623
|
+
elif flag == 3:
|
|
624
|
+
return value
|
|
516
625
|
else:
|
|
517
626
|
raise Exception("Unknown serialization format")
|
|
518
627
|
|
|
@@ -586,25 +695,15 @@ class PrefixedMemCached(MemCached):
|
|
|
586
695
|
return self._prefix + base64.b85encode(str(key).encode("utf-8"))
|
|
587
696
|
|
|
588
697
|
|
|
589
|
-
class StatsMemCached(MemCached):
|
|
590
|
-
"""Cache MemCached-compatible class with stats.
|
|
591
|
-
|
|
592
|
-
This class is empty and only kept for compatibility.
|
|
593
|
-
"""
|
|
594
|
-
|
|
595
|
-
pass
|
|
596
|
-
|
|
597
|
-
|
|
598
698
|
#
|
|
599
699
|
# REDIS
|
|
600
700
|
#
|
|
601
|
-
|
|
602
|
-
|
|
603
701
|
class RedisCache(MutableMapping):
|
|
604
702
|
"""Redis TTL-ed wrapper for cachetools (``redis``).
|
|
605
703
|
|
|
606
704
|
:param cache: actual redis cache.
|
|
607
705
|
:param ttl: time-to-live in seconds, used as default expiration (``ex``), default is 600.
|
|
706
|
+
:param raw: whether to serialize keys and values, default is *False*.
|
|
608
707
|
|
|
609
708
|
Keys and values are serialized in *JSON*.
|
|
610
709
|
|
|
@@ -616,24 +715,25 @@ class RedisCache(MutableMapping):
|
|
|
616
715
|
cache = ctu.RedisCache(redis.Redis(host="localhost"), 3600)
|
|
617
716
|
"""
|
|
618
717
|
|
|
619
|
-
def __init__(self, cache, ttl=600):
|
|
718
|
+
def __init__(self, cache, ttl=600, raw=False):
|
|
620
719
|
# import redis
|
|
621
720
|
# assert isinstance(cache, redis.Redis)
|
|
622
721
|
self._cache = cache
|
|
623
722
|
self._ttl = ttl
|
|
723
|
+
self._raw = raw
|
|
624
724
|
|
|
625
725
|
def clear(self): # pragma: no cover
|
|
626
726
|
"""Flush Redis contents."""
|
|
627
727
|
return self._cache.flushdb()
|
|
628
728
|
|
|
629
729
|
def _serialize(self, s):
|
|
630
|
-
return json.dumps(s, sort_keys=True)
|
|
730
|
+
return s if self._raw else json.dumps(s, sort_keys=True, separators=(",", ":"))
|
|
631
731
|
|
|
632
732
|
def _deserialize(self, s):
|
|
633
|
-
return json.loads(s)
|
|
733
|
+
return s if self._raw else json.loads(s)
|
|
634
734
|
|
|
635
735
|
def _key(self, key):
|
|
636
|
-
return json.dumps(key, sort_keys=True)
|
|
736
|
+
return key if self._raw else json.dumps(key, sort_keys=True, separators=(",", ":"))
|
|
637
737
|
|
|
638
738
|
def __getitem__(self, index):
|
|
639
739
|
val = self._cache.get(self._key(index))
|
|
@@ -713,10 +813,11 @@ class PrefixedRedisCache(RedisCache):
|
|
|
713
813
|
return self._prefix + str(key)
|
|
714
814
|
|
|
715
815
|
|
|
816
|
+
# FIXME should it be removed?
|
|
716
817
|
class StatsRedisCache(PrefixedRedisCache):
|
|
717
818
|
"""TTL-ed Redis wrapper class for cachetools.
|
|
718
819
|
|
|
719
|
-
This class is empty
|
|
820
|
+
This class is nearly empty.
|
|
720
821
|
"""
|
|
721
822
|
|
|
722
823
|
def flushdb(self):
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
CacheToolsUtils.py,sha256=2IzulFpw9WH-7-aJs7cztR-NPdLH3TVMxN2eFyZXRc0,20350
|
|
2
|
-
CacheToolsUtils-9.2.dist-info/LICENSE,sha256=y_yFW1bmBAUFCF-ZDyDiq10Ykns_19If5pFnZUzYgeU,33
|
|
3
|
-
CacheToolsUtils-9.2.dist-info/METADATA,sha256=jWWKCdp-ShOLRzX4DvYVziFAVdh-JDPA_nvGxK7ZKnE,4483
|
|
4
|
-
CacheToolsUtils-9.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
5
|
-
CacheToolsUtils-9.2.dist-info/top_level.txt,sha256=qCyE4jGot5mOmvIQrZueCnPtbYtRe2hqke4sGojm3zI,16
|
|
6
|
-
CacheToolsUtils-9.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|