CacheToolsUtils 9.3__tar.gz → 10.1__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.
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.egg-info/PKG-INFO +9 -3
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.egg-info/requires.txt +3 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.py +130 -29
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/PKG-INFO +9 -3
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/README.md +6 -2
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/pyproject.toml +2 -1
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.egg-info/SOURCES.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.egg-info/dependency_links.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/CacheToolsUtils.egg-info/top_level.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/LICENSE +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/MANIFEST.in +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CacheToolsUtils
|
|
3
|
-
Version:
|
|
3
|
+
Version: 10.1
|
|
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,17 @@ 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.
|
|
92
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
88
93
|
|
|
89
94
|
### Cache utilities
|
|
90
95
|
|
|
91
96
|
- `cached` decorator: a cachetools replacement which allows to test if a
|
|
92
97
|
function result is in cache, and to delete such an entry.
|
|
93
98
|
- `cacheFunctions` and `cacheMethods`: add caching to functions or methods.
|
|
94
|
-
- `json_key`
|
|
99
|
+
- `json_key`, `hash_json_key`, `full_hash_key`: convenient JSON-based cache key
|
|
100
|
+
serialization functions for `cached`.
|
|
95
101
|
|
|
96
102
|
## License
|
|
97
103
|
|
|
@@ -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
|
|
|
@@ -365,6 +367,106 @@ class TwoLevelCache(_MutMapMix, MutableMapping):
|
|
|
365
367
|
self._cache2.reset() # type: ignore
|
|
366
368
|
|
|
367
369
|
|
|
370
|
+
#
|
|
371
|
+
# Encrypted Cache
|
|
372
|
+
#
|
|
373
|
+
class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
374
|
+
"""Encrypted Bytes Key-Value Cache.
|
|
375
|
+
|
|
376
|
+
:param secret: bytes of secret, at least 16 bytes.
|
|
377
|
+
:param hsize: size of hashed key, default is *16*.
|
|
378
|
+
:param csize: value checksum size, default is *0*.
|
|
379
|
+
|
|
380
|
+
The key is *not* encrypted but simply hashed, thus they are
|
|
381
|
+
fixed size with a very low collision probability.
|
|
382
|
+
|
|
383
|
+
By design, the clear-text key is needed to recover the value,
|
|
384
|
+
as each value is encrypted with its own key and nonce.
|
|
385
|
+
|
|
386
|
+
Algorithms:
|
|
387
|
+
- SHA3: hash/key/nonce derivation and checksum.
|
|
388
|
+
- Salsa20: value encryption.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0):
|
|
392
|
+
self._cache = cache
|
|
393
|
+
assert len(secret) >= 16
|
|
394
|
+
self._secret = secret
|
|
395
|
+
assert 8 <= hsize <= 32
|
|
396
|
+
self._hsize = hsize
|
|
397
|
+
assert 0 <= csize <= 32
|
|
398
|
+
self._csize = csize
|
|
399
|
+
from Crypto.Cipher import Salsa20
|
|
400
|
+
self._cipher = Salsa20
|
|
401
|
+
|
|
402
|
+
def _keydev(self, key) -> tuple[bytes, bytes, bytes]:
|
|
403
|
+
"""Compute hash, key and nonce from initial key."""
|
|
404
|
+
hkey = hashlib.sha3_512(key + self._secret).digest()
|
|
405
|
+
# NOTE hash and nonce may overlap, which is not an issue
|
|
406
|
+
return (hkey[:self._hsize], hkey[32:], hkey[24:32])
|
|
407
|
+
|
|
408
|
+
def _key(self, key):
|
|
409
|
+
return self._keydev(key)[0]
|
|
410
|
+
|
|
411
|
+
def __setitem__(self, key, val):
|
|
412
|
+
hkey, vkey, vnonce = self._keydev(key)
|
|
413
|
+
xval = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
|
|
414
|
+
if self._csize:
|
|
415
|
+
cs = hashlib.sha3_256(val).digest()[:self._csize]
|
|
416
|
+
xval = cs + xval
|
|
417
|
+
self._cache[hkey] = xval
|
|
418
|
+
|
|
419
|
+
def __getitem__(self, key):
|
|
420
|
+
hkey, vkey, vnonce = self._keydev(key)
|
|
421
|
+
xval = self._cache[hkey]
|
|
422
|
+
if self._csize: # split cs from encrypted value
|
|
423
|
+
cs, xval = xval[:self._csize], xval[self._csize:]
|
|
424
|
+
else:
|
|
425
|
+
cs = None
|
|
426
|
+
val = self._cipher.new(key=vkey, nonce=vnonce).decrypt(xval)
|
|
427
|
+
if self._csize and cs != hashlib.sha3_256(val).digest()[:self._csize]:
|
|
428
|
+
raise KeyError(f"invalid encrypted value for key {key}")
|
|
429
|
+
return val
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
433
|
+
"""Map bytes to strings."""
|
|
434
|
+
|
|
435
|
+
def __init__(self, cache):
|
|
436
|
+
self._cache = cache
|
|
437
|
+
|
|
438
|
+
def _key(self, key):
|
|
439
|
+
# assert isinstance(key, bytes)
|
|
440
|
+
return base64.b85encode(key).decode("ASCII")
|
|
441
|
+
|
|
442
|
+
def __setitem__(self, key, val):
|
|
443
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
444
|
+
|
|
445
|
+
def __getitem__(self, key):
|
|
446
|
+
val = self._cache.__getitem__(self._key(key))
|
|
447
|
+
# assert isinstance(val, str)
|
|
448
|
+
return base64.b85decode(val)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class ToBytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
|
|
452
|
+
"""Map (JSON-serializable) cache keys and values to bytes."""
|
|
453
|
+
|
|
454
|
+
def __init__(self, cache):
|
|
455
|
+
self._cache = cache
|
|
456
|
+
|
|
457
|
+
def _key(self, key):
|
|
458
|
+
return json.dumps(key, sort_keys=True, separators=(",", ":")).encode("UTF-8")
|
|
459
|
+
|
|
460
|
+
def __setitem__(self, key, val):
|
|
461
|
+
self._cache.__setitem__(self._key(key), self._key(val))
|
|
462
|
+
|
|
463
|
+
def __getitem__(self, key):
|
|
464
|
+
return json.loads(self._cache.__getitem__(self._key(key)).decode("UTF-8"))
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
#
|
|
468
|
+
# CACHED DECORATOR AND HELPERS
|
|
469
|
+
#
|
|
368
470
|
def cached(cache, *args, **kwargs):
|
|
369
471
|
"""Extended decorator with delete and exists.
|
|
370
472
|
|
|
@@ -465,7 +567,9 @@ def cacheFunctions(
|
|
|
465
567
|
|
|
466
568
|
# JSON-based key function
|
|
467
569
|
# NOTE obviously this only works if parameters are json-serializable…
|
|
468
|
-
def json_key(*args, **kwargs):
|
|
570
|
+
def json_key(*args, **kwargs) -> str:
|
|
571
|
+
"""JSON serialization of arguments."""
|
|
572
|
+
val: Any
|
|
469
573
|
if kwargs: # object
|
|
470
574
|
if args:
|
|
471
575
|
val = {"*": args, "**": kwargs}
|
|
@@ -476,19 +580,17 @@ def json_key(*args, **kwargs):
|
|
|
476
580
|
return json.dumps(val, sort_keys=True, separators=(",", ":"))
|
|
477
581
|
|
|
478
582
|
|
|
479
|
-
# Hmmm…
|
|
583
|
+
# Hmmm… is this useful?
|
|
480
584
|
class _HashJsonKey:
|
|
481
585
|
"""A cache key with a persistant hash value."""
|
|
482
586
|
|
|
483
587
|
def __init__(self, *args, **kwargs):
|
|
484
|
-
self._hashed = None
|
|
485
588
|
self._args = args
|
|
486
589
|
self._kwargs = kwargs
|
|
487
590
|
self._key = json_key(*args, **kwargs)
|
|
591
|
+
self._hashed = self._key.__hash__()
|
|
488
592
|
|
|
489
593
|
def __hash__(self):
|
|
490
|
-
if self._hashed is None:
|
|
491
|
-
self._hashed = self._key.__hash__()
|
|
492
594
|
return self._hashed
|
|
493
595
|
|
|
494
596
|
def __str__(self):
|
|
@@ -499,6 +601,13 @@ def hash_json_key(*args, **kwargs):
|
|
|
499
601
|
return _HashJsonKey(*args, **kwargs)
|
|
500
602
|
|
|
501
603
|
|
|
604
|
+
def full_hash_key(*args, **kwargs) -> str:
|
|
605
|
+
"""Reduce arguments to a single 128-bits hash."""
|
|
606
|
+
skey = json_key(*args, **kwargs)
|
|
607
|
+
hkey = hashlib.sha3_256(skey.encode("UTF-8")).digest()[:16]
|
|
608
|
+
return base64.b85encode(hkey).decode("ASCII")
|
|
609
|
+
|
|
610
|
+
|
|
502
611
|
#
|
|
503
612
|
# MEMCACHED
|
|
504
613
|
#
|
|
@@ -514,16 +623,20 @@ class JsonSerde:
|
|
|
514
623
|
|
|
515
624
|
# keep strings, else json
|
|
516
625
|
def serialize(self, key, value):
|
|
517
|
-
if isinstance(value,
|
|
518
|
-
return value
|
|
626
|
+
if isinstance(value, bytes):
|
|
627
|
+
return value, 1
|
|
628
|
+
elif isinstance(value, str):
|
|
629
|
+
return value.encode("utf-8"), 2
|
|
519
630
|
else:
|
|
520
|
-
return json.dumps(value, sort_keys=True).encode("utf-8"),
|
|
631
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 3
|
|
521
632
|
|
|
522
633
|
# reverse previous serialization
|
|
523
634
|
def deserialize(self, key, value, flag):
|
|
524
635
|
if flag == 1:
|
|
525
|
-
return value
|
|
636
|
+
return value
|
|
526
637
|
elif flag == 2:
|
|
638
|
+
return value.decode("utf-8")
|
|
639
|
+
elif flag == 3:
|
|
527
640
|
return json.loads(value.decode("utf-8"))
|
|
528
641
|
else:
|
|
529
642
|
raise Exception("Unknown serialization format")
|
|
@@ -553,8 +666,6 @@ class MemCached(_KeyMutMapMix, MutableMapping):
|
|
|
553
666
|
# short (250 bytes) ASCII without control chars nor spaces
|
|
554
667
|
# we do not use hashing which might be costly or induce collisions
|
|
555
668
|
def _key(self, key):
|
|
556
|
-
import base64
|
|
557
|
-
|
|
558
669
|
return base64.b85encode(str(key).encode("utf-8"))
|
|
559
670
|
|
|
560
671
|
def __len__(self):
|
|
@@ -593,30 +704,18 @@ class PrefixedMemCached(MemCached):
|
|
|
593
704
|
self._prefix = bytes(prefix, "utf-8")
|
|
594
705
|
|
|
595
706
|
def _key(self, key):
|
|
596
|
-
import base64
|
|
597
|
-
|
|
598
707
|
return self._prefix + base64.b85encode(str(key).encode("utf-8"))
|
|
599
708
|
|
|
600
709
|
|
|
601
|
-
class StatsMemCached(MemCached):
|
|
602
|
-
"""Cache MemCached-compatible class with stats.
|
|
603
|
-
|
|
604
|
-
This class is empty and only kept for compatibility.
|
|
605
|
-
"""
|
|
606
|
-
|
|
607
|
-
pass
|
|
608
|
-
|
|
609
|
-
|
|
610
710
|
#
|
|
611
711
|
# REDIS
|
|
612
712
|
#
|
|
613
|
-
|
|
614
|
-
|
|
615
713
|
class RedisCache(MutableMapping):
|
|
616
714
|
"""Redis TTL-ed wrapper for cachetools (``redis``).
|
|
617
715
|
|
|
618
716
|
:param cache: actual redis cache.
|
|
619
717
|
:param ttl: time-to-live in seconds, used as default expiration (``ex``), default is 600.
|
|
718
|
+
:param raw: whether to serialize keys and values, default is *False*.
|
|
620
719
|
|
|
621
720
|
Keys and values are serialized in *JSON*.
|
|
622
721
|
|
|
@@ -628,24 +727,25 @@ class RedisCache(MutableMapping):
|
|
|
628
727
|
cache = ctu.RedisCache(redis.Redis(host="localhost"), 3600)
|
|
629
728
|
"""
|
|
630
729
|
|
|
631
|
-
def __init__(self, cache, ttl=600):
|
|
730
|
+
def __init__(self, cache, ttl=600, raw=False):
|
|
632
731
|
# import redis
|
|
633
732
|
# assert isinstance(cache, redis.Redis)
|
|
634
733
|
self._cache = cache
|
|
635
734
|
self._ttl = ttl
|
|
735
|
+
self._raw = raw
|
|
636
736
|
|
|
637
737
|
def clear(self): # pragma: no cover
|
|
638
738
|
"""Flush Redis contents."""
|
|
639
739
|
return self._cache.flushdb()
|
|
640
740
|
|
|
641
741
|
def _serialize(self, s):
|
|
642
|
-
return json.dumps(s, sort_keys=True)
|
|
742
|
+
return s if self._raw else json.dumps(s, sort_keys=True, separators=(",", ":"))
|
|
643
743
|
|
|
644
744
|
def _deserialize(self, s):
|
|
645
|
-
return json.loads(s)
|
|
745
|
+
return s if self._raw else json.loads(s)
|
|
646
746
|
|
|
647
747
|
def _key(self, key):
|
|
648
|
-
return json.dumps(key, sort_keys=True)
|
|
748
|
+
return key if self._raw else json.dumps(key, sort_keys=True, separators=(",", ":"))
|
|
649
749
|
|
|
650
750
|
def __getitem__(self, index):
|
|
651
751
|
val = self._cache.get(self._key(index))
|
|
@@ -725,10 +825,11 @@ class PrefixedRedisCache(RedisCache):
|
|
|
725
825
|
return self._prefix + str(key)
|
|
726
826
|
|
|
727
827
|
|
|
828
|
+
# FIXME should it be removed?
|
|
728
829
|
class StatsRedisCache(PrefixedRedisCache):
|
|
729
830
|
"""TTL-ed Redis wrapper class for cachetools.
|
|
730
831
|
|
|
731
|
-
This class is empty
|
|
832
|
+
This class is nearly empty.
|
|
732
833
|
"""
|
|
733
834
|
|
|
734
835
|
def flushdb(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CacheToolsUtils
|
|
3
|
-
Version:
|
|
3
|
+
Version: 10.1
|
|
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,17 @@ 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.
|
|
92
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
88
93
|
|
|
89
94
|
### Cache utilities
|
|
90
95
|
|
|
91
96
|
- `cached` decorator: a cachetools replacement which allows to test if a
|
|
92
97
|
function result is in cache, and to delete such an entry.
|
|
93
98
|
- `cacheFunctions` and `cacheMethods`: add caching to functions or methods.
|
|
94
|
-
- `json_key`
|
|
99
|
+
- `json_key`, `hash_json_key`, `full_hash_key`: convenient JSON-based cache key
|
|
100
|
+
serialization functions for `cached`.
|
|
95
101
|
|
|
96
102
|
## License
|
|
97
103
|
|
|
@@ -7,7 +7,7 @@ Classes to add key prefix and stats to
|
|
|
7
7
|
and other cache-related utils.
|
|
8
8
|
|
|
9
9
|

|
|
10
|
-

|
|
11
11
|

|
|
12
12
|

|
|
13
13
|

|
|
@@ -41,13 +41,17 @@ with `cachetools`.
|
|
|
41
41
|
- `LockedCache` use a (thread) lock to control cache accesses.
|
|
42
42
|
- `TwoLevelCache` allows to combine two caches.
|
|
43
43
|
- `DebugCache` to trace cache calls using `logging`.
|
|
44
|
+
- `EncryptedCache` a cache with key hashing and value encryption.
|
|
45
|
+
- `ToBytesCache` map keys and values to bytes.
|
|
46
|
+
- `BytesCache` map bytes keys and values to strings.
|
|
44
47
|
|
|
45
48
|
### Cache utilities
|
|
46
49
|
|
|
47
50
|
- `cached` decorator: a cachetools replacement which allows to test if a
|
|
48
51
|
function result is in cache, and to delete such an entry.
|
|
49
52
|
- `cacheFunctions` and `cacheMethods`: add caching to functions or methods.
|
|
50
|
-
- `json_key`
|
|
53
|
+
- `json_key`, `hash_json_key`, `full_hash_key`: convenient JSON-based cache key
|
|
54
|
+
serialization functions for `cached`.
|
|
51
55
|
|
|
52
56
|
## License
|
|
53
57
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "CacheToolsUtils"
|
|
7
|
-
version = "
|
|
7
|
+
version = "10.1"
|
|
8
8
|
authors = [ { name = "Fabien Coelho", email = "cachetools.utils@coelho.net" } ]
|
|
9
9
|
description = "Cachetools Utilities"
|
|
10
10
|
readme = "README.md"
|
|
@@ -32,5 +32,6 @@ py-modules = [ "CacheToolsUtils" ]
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
dev = [ "mypy", "pyright", "black", "flake8", "ruff", "pytest", "coverage", "pymarkdownlnt", "types-redis", "types-cachetools" ]
|
|
34
34
|
doc = [ "sphinx", "sphinx_rtd_theme", "sphinx-autoapi", "sphinx-lint", "myst_parser" ]
|
|
35
|
+
crypt = [ "pycryptodome" ]
|
|
35
36
|
tests = [ "pymemcache", "redis" ]
|
|
36
37
|
pub = [ "build", "wheel", "twine" ]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|