CacheToolsUtils 10.0__py3-none-any.whl → 10.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 10.0
3
+ Version: 10.2
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
 
@@ -0,0 +1,6 @@
1
+ CacheToolsUtils.py,sha256=etJrt34OkZtSMXK1wGJIg9VcDYxH3pXzpJy6OowAMnY,26120
2
+ CacheToolsUtils-10.2.dist-info/LICENSE,sha256=y_yFW1bmBAUFCF-ZDyDiq10Ykns_19If5pFnZUzYgeU,33
3
+ CacheToolsUtils-10.2.dist-info/METADATA,sha256=rEY3PJdfjL-E-QVqAt36vb7voJqxZYaeqze-Lk7YuQ0,4759
4
+ CacheToolsUtils-10.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
5
+ CacheToolsUtils-10.2.dist-info/top_level.txt,sha256=qCyE4jGot5mOmvIQrZueCnPtbYtRe2hqke4sGojm3zI,16
6
+ CacheToolsUtils-10.2.dist-info/RECORD,,
CacheToolsUtils.py CHANGED
@@ -367,6 +367,147 @@ class TwoLevelCache(_MutMapMix, MutableMapping):
367
367
  self._cache2.reset() # type: ignore
368
368
 
369
369
 
370
+ # NOTE this is quite costly, a Cipher object is created to encrypt/decrypt each value.
371
+ # another design could keep a persistant cipher.
372
+ class _Cipher:
373
+ """Not so convincing internal class to abstract cipher algorithms."""
374
+
375
+ def __init__(self, name):
376
+ self._name = name
377
+ if name == "Salsa20":
378
+ from Crypto.Cipher import Salsa20 as cipher
379
+ # NOTE hash and nonce may overlap, which is not an issue
380
+ params = {"key": (32, 64), "nonce": (24, 32)}
381
+ kwargs = {}
382
+ size = 0
383
+ elif name == "ChaCha20":
384
+ from Crypto.Cipher import ChaCha20 as cipher
385
+ # NOTE hash and nonce may overlap, which is not an issue
386
+ params = {"key": (32, 64), "nonce": (24, 32)}
387
+ kwargs = {}
388
+ size = 0
389
+ elif name in ("AES", "AES-128", "AES-128-CBC"):
390
+ from Crypto.Cipher import AES as cipher
391
+ params = {"key": (48, 64), "iv": (32, 48)}
392
+ kwargs = {"mode": cipher.MODE_CBC}
393
+ size = 16
394
+ else:
395
+ raise Exception(f"unexpected cipher: {name}")
396
+ self._cipher = cipher
397
+ self._params = params
398
+ self._kwargs = kwargs
399
+ if size:
400
+ from Crypto.Util.Padding import pad, unpad
401
+ self._pad = lambda s: pad(s, size)
402
+ self._unpad = lambda s: unpad(s, size)
403
+ else:
404
+ self._pad = lambda s: s
405
+ self._unpad = lambda s: s
406
+
407
+ def new(self, derived: bytes):
408
+ kwargs = {k: derived[v[0]: v[1]] for k, v in self._params.items()}
409
+ return self._cipher.new(**kwargs, **self._kwargs) # type: ignore
410
+
411
+
412
+ #
413
+ # Encrypted Cache
414
+ #
415
+ class EncryptedCache(_KeyMutMapMix, _StatsMix, MutableMapping):
416
+ """Encrypted Bytes Key-Value Cache.
417
+
418
+ :param secret: bytes of secret, at least 16 bytes.
419
+ :param hsize: size of hashed key, default is *16*.
420
+ :param csize: value checksum size, default is *0*.
421
+ :param cipher: chose cipher from "Salsa20", "AES-128-CBC" or "ChaCha20".
422
+
423
+ The key is *not* encrypted but simply hashed, thus they are
424
+ fixed size with a very low collision probability.
425
+
426
+ By design, the clear-text key is needed to recover the value,
427
+ as each value is encrypted with its own key and nonce.
428
+
429
+ Algorithms:
430
+
431
+ - SHA3: hash/key/nonce derivation and checksum.
432
+ - Salsa20, AES-128-CBC or ChaCha20: value encryption.
433
+ """
434
+
435
+ def __init__(self, cache: MutableMapping, secret: bytes, hsize: int = 16, csize: int = 0, cipher: str = "Salsa20"):
436
+ self._cache = cache
437
+ assert len(secret) >= 16
438
+ self._secret = secret
439
+ assert 8 <= hsize <= 32
440
+ self._hsize = hsize
441
+ assert 0 <= csize <= 32
442
+ self._csize = csize
443
+ self._cipher = _Cipher(cipher)
444
+
445
+ def _derive(self, key) -> tuple[bytes, bytes]:
446
+ """Derive hash and stuff from initial key and secret."""
447
+ derived = hashlib.sha3_512(key + self._secret).digest()
448
+ return (derived[:self._hsize], derived)
449
+
450
+ def _key(self, key):
451
+ return self._derive(key)[0]
452
+
453
+ def __setitem__(self, key, val):
454
+ hkey, derived = self._derive(key)
455
+ xval = self._cipher.new(derived).encrypt(self._cipher._pad(val))
456
+ if self._csize:
457
+ xval = hashlib.sha3_256(val).digest()[:self._csize] + xval
458
+ self._cache[hkey] = xval
459
+
460
+ def __getitem__(self, key):
461
+ hkey, derived = self._derive(key)
462
+ xval = self._cache[hkey]
463
+ if self._csize: # split cs from encrypted value
464
+ cs, xval = xval[:self._csize], xval[self._csize:]
465
+ else:
466
+ cs = None
467
+ val = self._cipher._unpad(self._cipher.new(derived).decrypt(xval))
468
+ if self._csize and cs != hashlib.sha3_256(val).digest()[:self._csize]:
469
+ raise KeyError(f"invalid encrypted value for key {key}")
470
+ return val
471
+
472
+
473
+ class BytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
474
+ """Map bytes to strings."""
475
+
476
+ def __init__(self, cache):
477
+ self._cache = cache
478
+
479
+ def _key(self, key):
480
+ # assert isinstance(key, bytes)
481
+ return base64.b85encode(key).decode("ASCII")
482
+
483
+ def __setitem__(self, key, val):
484
+ self._cache.__setitem__(self._key(key), self._key(val))
485
+
486
+ def __getitem__(self, key):
487
+ val = self._cache.__getitem__(self._key(key))
488
+ # assert isinstance(val, str)
489
+ return base64.b85decode(val)
490
+
491
+
492
+ class ToBytesCache(_KeyMutMapMix, _StatsMix, MutableMapping):
493
+ """Map (JSON-serializable) cache keys and values to bytes."""
494
+
495
+ def __init__(self, cache):
496
+ self._cache = cache
497
+
498
+ def _key(self, key):
499
+ return json.dumps(key, sort_keys=True, separators=(",", ":")).encode("UTF-8")
500
+
501
+ def __setitem__(self, key, val):
502
+ self._cache.__setitem__(self._key(key), self._key(val))
503
+
504
+ def __getitem__(self, key):
505
+ return json.loads(self._cache.__getitem__(self._key(key)).decode("UTF-8"))
506
+
507
+
508
+ #
509
+ # CACHED DECORATOR AND HELPERS
510
+ #
370
511
  def cached(cache, *args, **kwargs):
371
512
  """Extended decorator with delete and exists.
372
513
 
@@ -497,7 +638,9 @@ class _HashJsonKey:
497
638
  return self._key
498
639
 
499
640
 
641
+ # NOTE not very useful, behaves as _HashJsonKey.
500
642
  def hash_json_key(*args, **kwargs):
643
+ """Function handling of above class, to keep it hidden."""
501
644
  return _HashJsonKey(*args, **kwargs)
502
645
 
503
646
 
@@ -508,93 +651,9 @@ def full_hash_key(*args, **kwargs) -> str:
508
651
  return base64.b85encode(hkey).decode("ASCII")
509
652
 
510
653
 
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
654
  #
595
655
  # MEMCACHED
596
656
  #
597
- # FIXME what about bytes?
598
657
  class JsonSerde:
599
658
  """JSON serialize/deserialize class for MemCached (``pymemcache``).
600
659
 
@@ -607,23 +666,23 @@ class JsonSerde:
607
666
 
608
667
  # keep strings, else json
609
668
  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
669
+ if isinstance(value, bytes):
670
+ return value, 1
671
+ elif isinstance(value, str):
672
+ return value.encode("utf-8"), 2
614
673
  else:
615
- return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 2
674
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 3
616
675
 
617
676
  # reverse previous serialization
618
677
  def deserialize(self, key, value, flag):
619
678
  if flag == 1:
620
- return value.decode("utf-8")
679
+ return value
621
680
  elif flag == 2:
622
- return json.loads(value.decode("utf-8"))
681
+ return value.decode("utf-8")
623
682
  elif flag == 3:
624
- return value
683
+ return json.loads(value.decode("utf-8"))
625
684
  else:
626
- raise Exception("Unknown serialization format")
685
+ raise Exception(f"Unknown serialization format: {flag}")
627
686
 
628
687
 
629
688
  class MemCached(_KeyMutMapMix, MutableMapping):
@@ -650,8 +709,6 @@ class MemCached(_KeyMutMapMix, MutableMapping):
650
709
  # short (250 bytes) ASCII without control chars nor spaces
651
710
  # we do not use hashing which might be costly or induce collisions
652
711
  def _key(self, key):
653
- import base64
654
-
655
712
  return base64.b85encode(str(key).encode("utf-8"))
656
713
 
657
714
  def __len__(self):
@@ -690,8 +747,6 @@ class PrefixedMemCached(MemCached):
690
747
  self._prefix = bytes(prefix, "utf-8")
691
748
 
692
749
  def _key(self, key):
693
- import base64
694
-
695
750
  return self._prefix + base64.b85encode(str(key).encode("utf-8"))
696
751
 
697
752
 
@@ -1,6 +0,0 @@
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,,