CacheToolsUtils 10.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 10.0
3
+ Version: 10.1
4
4
  Summary: Cachetools Utilities
5
5
  Author-email: Fabien Coelho <cachetools.utils@coelho.net>
6
6
  License: CC0
@@ -89,6 +89,7 @@ with `cachetools`.
89
89
  - `DebugCache` to trace cache calls using `logging`.
90
90
  - `EncryptedCache` a cache with key hashing and value encryption.
91
91
  - `ToBytesCache` map keys and values to bytes.
92
+ - `BytesCache` map bytes keys and values to strings.
92
93
 
93
94
  ### Cache utilities
94
95
 
@@ -367,6 +367,106 @@ class TwoLevelCache(_MutMapMix, MutableMapping):
367
367
  self._cache2.reset() # type: ignore
368
368
 
369
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
+ #
370
470
  def cached(cache, *args, **kwargs):
371
471
  """Extended decorator with delete and exists.
372
472
 
@@ -508,93 +608,9 @@ def full_hash_key(*args, **kwargs) -> str:
508
608
  return base64.b85encode(hkey).decode("ASCII")
509
609
 
510
610
 
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
-
594
611
  #
595
612
  # MEMCACHED
596
613
  #
597
- # FIXME what about bytes?
598
614
  class JsonSerde:
599
615
  """JSON serialize/deserialize class for MemCached (``pymemcache``).
600
616
 
@@ -607,21 +623,21 @@ class JsonSerde:
607
623
 
608
624
  # keep strings, else json
609
625
  def serialize(self, key, value):
610
- if isinstance(value, str):
611
- return value.encode("utf-8"), 1
612
- elif isinstance(value, bytes):
613
- return value, 3
626
+ if isinstance(value, bytes):
627
+ return value, 1
628
+ elif isinstance(value, str):
629
+ return value.encode("utf-8"), 2
614
630
  else:
615
- return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 2
631
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 3
616
632
 
617
633
  # reverse previous serialization
618
634
  def deserialize(self, key, value, flag):
619
635
  if flag == 1:
620
- return value.decode("utf-8")
636
+ return value
621
637
  elif flag == 2:
622
- return json.loads(value.decode("utf-8"))
638
+ return value.decode("utf-8")
623
639
  elif flag == 3:
624
- return value
640
+ return json.loads(value.decode("utf-8"))
625
641
  else:
626
642
  raise Exception("Unknown serialization format")
627
643
 
@@ -650,8 +666,6 @@ class MemCached(_KeyMutMapMix, MutableMapping):
650
666
  # short (250 bytes) ASCII without control chars nor spaces
651
667
  # we do not use hashing which might be costly or induce collisions
652
668
  def _key(self, key):
653
- import base64
654
-
655
669
  return base64.b85encode(str(key).encode("utf-8"))
656
670
 
657
671
  def __len__(self):
@@ -690,8 +704,6 @@ class PrefixedMemCached(MemCached):
690
704
  self._prefix = bytes(prefix, "utf-8")
691
705
 
692
706
  def _key(self, key):
693
- import base64
694
-
695
707
  return self._prefix + base64.b85encode(str(key).encode("utf-8"))
696
708
 
697
709
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 10.0
3
+ Version: 10.1
4
4
  Summary: Cachetools Utilities
5
5
  Author-email: Fabien Coelho <cachetools.utils@coelho.net>
6
6
  License: CC0
@@ -89,6 +89,7 @@ with `cachetools`.
89
89
  - `DebugCache` to trace cache calls using `logging`.
90
90
  - `EncryptedCache` a cache with key hashing and value encryption.
91
91
  - `ToBytesCache` map keys and values to bytes.
92
+ - `BytesCache` map bytes keys and values to strings.
92
93
 
93
94
  ### Cache utilities
94
95
 
@@ -43,6 +43,7 @@ with `cachetools`.
43
43
  - `DebugCache` to trace cache calls using `logging`.
44
44
  - `EncryptedCache` a cache with key hashing and value encryption.
45
45
  - `ToBytesCache` map keys and values to bytes.
46
+ - `BytesCache` map bytes keys and values to strings.
46
47
 
47
48
  ### Cache utilities
48
49
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "CacheToolsUtils"
7
- version = "10.0"
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"
File without changes
File without changes