CacheToolsUtils 9.3__tar.gz → 10.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.
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.egg-info/PKG-INFO +8 -3
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.egg-info/requires.txt +3 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.py +111 -22
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/PKG-INFO +8 -3
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/README.md +5 -2
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/pyproject.toml +2 -1
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.egg-info/SOURCES.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.egg-info/dependency_links.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/CacheToolsUtils.egg-info/top_level.txt +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/LICENSE +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/MANIFEST.in +0 -0
- {cachetoolsutils-9.3 → cachetoolsutils-10.0}/setup.cfg +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
|
|
|
@@ -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
|
|
|
@@ -465,7 +467,9 @@ def cacheFunctions(
|
|
|
465
467
|
|
|
466
468
|
# JSON-based key function
|
|
467
469
|
# NOTE obviously this only works if parameters are json-serializable…
|
|
468
|
-
def json_key(*args, **kwargs):
|
|
470
|
+
def json_key(*args, **kwargs) -> str:
|
|
471
|
+
"""JSON serialization of arguments."""
|
|
472
|
+
val: Any
|
|
469
473
|
if kwargs: # object
|
|
470
474
|
if args:
|
|
471
475
|
val = {"*": args, "**": kwargs}
|
|
@@ -476,19 +480,17 @@ def json_key(*args, **kwargs):
|
|
|
476
480
|
return json.dumps(val, sort_keys=True, separators=(",", ":"))
|
|
477
481
|
|
|
478
482
|
|
|
479
|
-
# Hmmm…
|
|
483
|
+
# Hmmm… is this useful?
|
|
480
484
|
class _HashJsonKey:
|
|
481
485
|
"""A cache key with a persistant hash value."""
|
|
482
486
|
|
|
483
487
|
def __init__(self, *args, **kwargs):
|
|
484
|
-
self._hashed = None
|
|
485
488
|
self._args = args
|
|
486
489
|
self._kwargs = kwargs
|
|
487
490
|
self._key = json_key(*args, **kwargs)
|
|
491
|
+
self._hashed = self._key.__hash__()
|
|
488
492
|
|
|
489
493
|
def __hash__(self):
|
|
490
|
-
if self._hashed is None:
|
|
491
|
-
self._hashed = self._key.__hash__()
|
|
492
494
|
return self._hashed
|
|
493
495
|
|
|
494
496
|
def __str__(self):
|
|
@@ -499,9 +501,100 @@ def hash_json_key(*args, **kwargs):
|
|
|
499
501
|
return _HashJsonKey(*args, **kwargs)
|
|
500
502
|
|
|
501
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
|
+
|
|
502
594
|
#
|
|
503
595
|
# MEMCACHED
|
|
504
596
|
#
|
|
597
|
+
# FIXME what about bytes?
|
|
505
598
|
class JsonSerde:
|
|
506
599
|
"""JSON serialize/deserialize class for MemCached (``pymemcache``).
|
|
507
600
|
|
|
@@ -516,8 +609,10 @@ class JsonSerde:
|
|
|
516
609
|
def serialize(self, key, value):
|
|
517
610
|
if isinstance(value, str):
|
|
518
611
|
return value.encode("utf-8"), 1
|
|
612
|
+
elif isinstance(value, bytes):
|
|
613
|
+
return value, 3
|
|
519
614
|
else:
|
|
520
|
-
return json.dumps(value, sort_keys=True).encode("utf-8"), 2
|
|
615
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 2
|
|
521
616
|
|
|
522
617
|
# reverse previous serialization
|
|
523
618
|
def deserialize(self, key, value, flag):
|
|
@@ -525,6 +620,8 @@ class JsonSerde:
|
|
|
525
620
|
return value.decode("utf-8")
|
|
526
621
|
elif flag == 2:
|
|
527
622
|
return json.loads(value.decode("utf-8"))
|
|
623
|
+
elif flag == 3:
|
|
624
|
+
return value
|
|
528
625
|
else:
|
|
529
626
|
raise Exception("Unknown serialization format")
|
|
530
627
|
|
|
@@ -598,25 +695,15 @@ class PrefixedMemCached(MemCached):
|
|
|
598
695
|
return self._prefix + base64.b85encode(str(key).encode("utf-8"))
|
|
599
696
|
|
|
600
697
|
|
|
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
698
|
#
|
|
611
699
|
# REDIS
|
|
612
700
|
#
|
|
613
|
-
|
|
614
|
-
|
|
615
701
|
class RedisCache(MutableMapping):
|
|
616
702
|
"""Redis TTL-ed wrapper for cachetools (``redis``).
|
|
617
703
|
|
|
618
704
|
:param cache: actual redis cache.
|
|
619
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*.
|
|
620
707
|
|
|
621
708
|
Keys and values are serialized in *JSON*.
|
|
622
709
|
|
|
@@ -628,24 +715,25 @@ class RedisCache(MutableMapping):
|
|
|
628
715
|
cache = ctu.RedisCache(redis.Redis(host="localhost"), 3600)
|
|
629
716
|
"""
|
|
630
717
|
|
|
631
|
-
def __init__(self, cache, ttl=600):
|
|
718
|
+
def __init__(self, cache, ttl=600, raw=False):
|
|
632
719
|
# import redis
|
|
633
720
|
# assert isinstance(cache, redis.Redis)
|
|
634
721
|
self._cache = cache
|
|
635
722
|
self._ttl = ttl
|
|
723
|
+
self._raw = raw
|
|
636
724
|
|
|
637
725
|
def clear(self): # pragma: no cover
|
|
638
726
|
"""Flush Redis contents."""
|
|
639
727
|
return self._cache.flushdb()
|
|
640
728
|
|
|
641
729
|
def _serialize(self, s):
|
|
642
|
-
return json.dumps(s, sort_keys=True)
|
|
730
|
+
return s if self._raw else json.dumps(s, sort_keys=True, separators=(",", ":"))
|
|
643
731
|
|
|
644
732
|
def _deserialize(self, s):
|
|
645
|
-
return json.loads(s)
|
|
733
|
+
return s if self._raw else json.loads(s)
|
|
646
734
|
|
|
647
735
|
def _key(self, key):
|
|
648
|
-
return json.dumps(key, sort_keys=True)
|
|
736
|
+
return key if self._raw else json.dumps(key, sort_keys=True, separators=(",", ":"))
|
|
649
737
|
|
|
650
738
|
def __getitem__(self, index):
|
|
651
739
|
val = self._cache.get(self._key(index))
|
|
@@ -725,10 +813,11 @@ class PrefixedRedisCache(RedisCache):
|
|
|
725
813
|
return self._prefix + str(key)
|
|
726
814
|
|
|
727
815
|
|
|
816
|
+
# FIXME should it be removed?
|
|
728
817
|
class StatsRedisCache(PrefixedRedisCache):
|
|
729
818
|
"""TTL-ed Redis wrapper class for cachetools.
|
|
730
819
|
|
|
731
|
-
This class is empty
|
|
820
|
+
This class is nearly empty.
|
|
732
821
|
"""
|
|
733
822
|
|
|
734
823
|
def flushdb(self):
|
|
@@ -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
|
|
|
@@ -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,16 @@ 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.
|
|
44
46
|
|
|
45
47
|
### Cache utilities
|
|
46
48
|
|
|
47
49
|
- `cached` decorator: a cachetools replacement which allows to test if a
|
|
48
50
|
function result is in cache, and to delete such an entry.
|
|
49
51
|
- `cacheFunctions` and `cacheMethods`: add caching to functions or methods.
|
|
50
|
-
- `json_key`
|
|
52
|
+
- `json_key`, `hash_json_key`, `full_hash_key`: convenient JSON-based cache key
|
|
53
|
+
serialization functions for `cached`.
|
|
51
54
|
|
|
52
55
|
## License
|
|
53
56
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "CacheToolsUtils"
|
|
7
|
-
version = "
|
|
7
|
+
version = "10.0"
|
|
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
|