CacheToolsUtils 10.1__tar.gz → 10.3__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: CacheToolsUtils
3
- Version: 10.1
3
+ Version: 10.3
4
4
  Summary: Cachetools Utilities
5
5
  Author-email: Fabien Coelho <cachetools.utils@coelho.net>
6
6
  License: CC0
@@ -53,7 +53,7 @@ Classes to add key prefix and stats to
53
53
  and other cache-related utils.
54
54
 
55
55
  ![Status](https://github.com/zx80/cachetools-utils/actions/workflows/cachetools-utils.yml/badge.svg?branch=main&style=flat)
56
- ![Tests](https://img.shields.io/badge/tests-22%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-23%20✓-success)
57
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
58
58
  ![Issues](https://img.shields.io/github/issues/zx80/cachetools-utils?style=flat)
59
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -81,6 +81,7 @@ with `cachetools`.
81
81
 
82
82
  - `PrefixedCache`, `PrefixedMemCached` and `PrefixedRedisCache` add a prefix to
83
83
  distinguish sources on a shared cache.
84
+ - `AutoPrefixedCache` add a counter-based prefix.
84
85
  - `StatsCache`, `MemCached` and `RedisCache` add a `hits` method
85
86
  to report the cache hit rate, `stats` to report statistics and
86
87
  `reset` to reset statistics.
@@ -226,6 +226,39 @@ class PrefixedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
226
226
  return self._prefix + self._cast(key) # type: ignore
227
227
 
228
228
 
229
+ class AutoPrefixedCache(PrefixedCache):
230
+ """Cache class with an automatic counter-based string prefix.
231
+
232
+ :param cache: actual cache.
233
+ :param sep: prefix separator, default is ".".
234
+ :param method: encoding method in "b64", "b64u", "b32", "b32x", "b16",
235
+ "a85" and "b85". Default is "b64".
236
+ """
237
+
238
+ _COUNTER: int = 0
239
+
240
+ _METHODS: dict[str, Callable[[Any], bytes]] = {
241
+ "b64": base64.b64encode,
242
+ "b64u": base64.urlsafe_b64encode,
243
+ "b32": base64.b32encode,
244
+ "b32x": base64.b32hexencode,
245
+ "b16": base64.b16encode,
246
+ "a85": base64.a85encode,
247
+ "b85": base64.b85encode,
248
+ # "z85": base64.z85encode, # Python 3.13
249
+ }
250
+
251
+ def __init__(self, cache: MutableMapping, sep: str = ".", method: str = "b64"):
252
+ if method not in AutoPrefixedCache._METHODS:
253
+ raise Exception(f"invalid conversion method: {method}")
254
+ encoder = AutoPrefixedCache._METHODS[method]
255
+ length = max(1, (AutoPrefixedCache._COUNTER.bit_length() + 7) // 8)
256
+ # NOTE byteorder default added to 3.11
257
+ prefix = encoder(AutoPrefixedCache._COUNTER.to_bytes(length, byteorder="big")).decode("ASCII")
258
+ AutoPrefixedCache._COUNTER += 1
259
+ super().__init__(cache, prefix + sep)
260
+
261
+
229
262
  class StatsCache(_MutMapMix, MutableMapping):
230
263
  """Cache class to keep stats.
231
264
 
@@ -367,6 +400,48 @@ class TwoLevelCache(_MutMapMix, MutableMapping):
367
400
  self._cache2.reset() # type: ignore
368
401
 
369
402
 
403
+ # NOTE this is quite costly, a Cipher object is created to encrypt/decrypt each value.
404
+ # another design could keep a persistant cipher.
405
+ class _Cipher:
406
+ """Not so convincing internal class to abstract cipher algorithms."""
407
+
408
+ def __init__(self, name):
409
+ self._name = name
410
+ if name == "Salsa20":
411
+ from Crypto.Cipher import Salsa20 as cipher
412
+ # NOTE hash and nonce may overlap, which is not an issue
413
+ params = {"key": (32, 64), "nonce": (24, 32)}
414
+ kwargs = {}
415
+ size = 0
416
+ elif name == "ChaCha20":
417
+ from Crypto.Cipher import ChaCha20 as cipher
418
+ # NOTE hash and nonce may overlap, which is not an issue
419
+ params = {"key": (32, 64), "nonce": (24, 32)}
420
+ kwargs = {}
421
+ size = 0
422
+ elif name in ("AES", "AES-128", "AES-128-CBC"):
423
+ from Crypto.Cipher import AES as cipher
424
+ params = {"key": (48, 64), "iv": (32, 48)}
425
+ kwargs = {"mode": cipher.MODE_CBC}
426
+ size = 16
427
+ else:
428
+ raise Exception(f"unexpected cipher: {name}")
429
+ self._cipher = cipher
430
+ self._params = params
431
+ self._kwargs = kwargs
432
+ if size:
433
+ from Crypto.Util.Padding import pad, unpad
434
+ self._pad = lambda s: pad(s, size)
435
+ self._unpad = lambda s: unpad(s, size)
436
+ else:
437
+ self._pad = lambda s: s
438
+ self._unpad = lambda s: s
439
+
440
+ def new(self, derived: bytes):
441
+ kwargs = {k: derived[v[0]: v[1]] for k, v in self._params.items()}
442
+ return self._cipher.new(**kwargs, **self._kwargs) # type: ignore
443
+
444
+
370
445
  #
371
446
  # Encrypted Cache
372
447
  #
@@ -376,6 +451,7 @@ class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
376
451
  :param secret: bytes of secret, at least 16 bytes.
377
452
  :param hsize: size of hashed key, default is *16*.
378
453
  :param csize: value checksum size, default is *0*.
454
+ :param cipher: chose cipher from "Salsa20", "AES-128-CBC" or "ChaCha20".
379
455
 
380
456
  The key is *not* encrypted but simply hashed, thus they are
381
457
  fixed size with a very low collision probability.
@@ -384,11 +460,12 @@ class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
384
460
  as each value is encrypted with its own key and nonce.
385
461
 
386
462
  Algorithms:
463
+
387
464
  - SHA3: hash/key/nonce derivation and checksum.
388
- - Salsa20: value encryption.
465
+ - Salsa20, AES-128-CBC or ChaCha20: value encryption.
389
466
  """
390
467
 
391
- def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0):
468
+ def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0, cipher: str = "Salsa20"):
392
469
  self._cache = cache
393
470
  assert len(secret) >= 16
394
471
  self._secret = secret
@@ -396,34 +473,31 @@ class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
396
473
  self._hsize = hsize
397
474
  assert 0 <= csize <= 32
398
475
  self._csize = csize
399
- from Crypto.Cipher import Salsa20
400
- self._cipher = Salsa20
476
+ self._cipher = _Cipher(cipher)
401
477
 
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])
478
+ def _derive(self, key) -> tuple[bytes, bytes]:
479
+ """Derive hash and stuff from initial key and secret."""
480
+ derived = hashlib.sha3_512(key + self._secret).digest()
481
+ return (derived[:self._hsize], derived)
407
482
 
408
483
  def _key(self, key):
409
- return self._keydev(key)[0]
484
+ return self._derive(key)[0]
410
485
 
411
486
  def __setitem__(self, key, val):
412
- hkey, vkey, vnonce = self._keydev(key)
413
- xval = self._cipher.new(key=vkey, nonce=vnonce).encrypt(val)
487
+ hkey, derived = self._derive(key)
488
+ xval = self._cipher.new(derived).encrypt(self._cipher._pad(val))
414
489
  if self._csize:
415
- cs = hashlib.sha3_256(val).digest()[:self._csize]
416
- xval = cs + xval
490
+ xval = hashlib.sha3_256(val).digest()[:self._csize] + xval
417
491
  self._cache[hkey] = xval
418
492
 
419
493
  def __getitem__(self, key):
420
- hkey, vkey, vnonce = self._keydev(key)
494
+ hkey, derived = self._derive(key)
421
495
  xval = self._cache[hkey]
422
496
  if self._csize: # split cs from encrypted value
423
497
  cs, xval = xval[:self._csize], xval[self._csize:]
424
498
  else:
425
499
  cs = None
426
- val = self._cipher.new(key=vkey, nonce=vnonce).decrypt(xval)
500
+ val = self._cipher._unpad(self._cipher.new(derived).decrypt(xval))
427
501
  if self._csize and cs != hashlib.sha3_256(val).digest()[:self._csize]:
428
502
  raise KeyError(f"invalid encrypted value for key {key}")
429
503
  return val
@@ -597,7 +671,9 @@ class _HashJsonKey:
597
671
  return self._key
598
672
 
599
673
 
674
+ # NOTE not very useful, behaves as _HashJsonKey.
600
675
  def hash_json_key(*args, **kwargs):
676
+ """Function handling of above class, to keep it hidden."""
601
677
  return _HashJsonKey(*args, **kwargs)
602
678
 
603
679
 
@@ -639,7 +715,7 @@ class JsonSerde:
639
715
  elif flag == 3:
640
716
  return json.loads(value.decode("utf-8"))
641
717
  else:
642
- raise Exception("Unknown serialization format")
718
+ raise Exception(f"Unknown serialization format: {flag}")
643
719
 
644
720
 
645
721
  class MemCached(_KeyMutMapMix, MutableMapping):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: CacheToolsUtils
3
- Version: 10.1
3
+ Version: 10.3
4
4
  Summary: Cachetools Utilities
5
5
  Author-email: Fabien Coelho <cachetools.utils@coelho.net>
6
6
  License: CC0
@@ -53,7 +53,7 @@ Classes to add key prefix and stats to
53
53
  and other cache-related utils.
54
54
 
55
55
  ![Status](https://github.com/zx80/cachetools-utils/actions/workflows/cachetools-utils.yml/badge.svg?branch=main&style=flat)
56
- ![Tests](https://img.shields.io/badge/tests-22%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-23%20✓-success)
57
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
58
58
  ![Issues](https://img.shields.io/github/issues/zx80/cachetools-utils?style=flat)
59
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -81,6 +81,7 @@ with `cachetools`.
81
81
 
82
82
  - `PrefixedCache`, `PrefixedMemCached` and `PrefixedRedisCache` add a prefix to
83
83
  distinguish sources on a shared cache.
84
+ - `AutoPrefixedCache` add a counter-based prefix.
84
85
  - `StatsCache`, `MemCached` and `RedisCache` add a `hits` method
85
86
  to report the cache hit rate, `stats` to report statistics and
86
87
  `reset` to reset statistics.
@@ -7,7 +7,7 @@ Classes to add key prefix and stats to
7
7
  and other cache-related utils.
8
8
 
9
9
  ![Status](https://github.com/zx80/cachetools-utils/actions/workflows/cachetools-utils.yml/badge.svg?branch=main&style=flat)
10
- ![Tests](https://img.shields.io/badge/tests-22%20✓-success)
10
+ ![Tests](https://img.shields.io/badge/tests-23%20✓-success)
11
11
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
12
12
  ![Issues](https://img.shields.io/github/issues/zx80/cachetools-utils?style=flat)
13
13
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -35,6 +35,7 @@ with `cachetools`.
35
35
 
36
36
  - `PrefixedCache`, `PrefixedMemCached` and `PrefixedRedisCache` add a prefix to
37
37
  distinguish sources on a shared cache.
38
+ - `AutoPrefixedCache` add a counter-based prefix.
38
39
  - `StatsCache`, `MemCached` and `RedisCache` add a `hits` method
39
40
  to report the cache hit rate, `stats` to report statistics and
40
41
  `reset` to reset statistics.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CacheToolsUtils"
7
- version = "10.1"
7
+ version = "10.3"
8
8
  authors = [ { name = "Fabien Coelho", email = "cachetools.utils@coelho.net" } ]
9
9
  description = "Cachetools Utilities"
10
10
  readme = "README.md"
File without changes
File without changes