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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 9.3
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
  ![Status](https://github.com/zx80/cachetools-utils/actions/workflows/cachetools-utils.yml/badge.svg?branch=main&style=flat)
54
- ![Tests](https://img.shields.io/badge/tests-21%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-22%20✓-success)
55
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
56
58
  ![Issues](https://img.shields.io/github/issues/zx80/cachetools-utils?style=flat)
57
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -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` and `hash_json_key`: JSON-based cache key serialization functions
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
 
@@ -1,5 +1,8 @@
1
1
  cachetools
2
2
 
3
+ [crypt]
4
+ pycryptodome
5
+
3
6
  [dev]
4
7
  mypy
5
8
  pyright
@@ -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, str):
518
- return value.encode("utf-8"), 1
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"), 2
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.decode("utf-8")
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 and only kept for compatibility.
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: 9.3
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
  ![Status](https://github.com/zx80/cachetools-utils/actions/workflows/cachetools-utils.yml/badge.svg?branch=main&style=flat)
54
- ![Tests](https://img.shields.io/badge/tests-21%20✓-success)
56
+ ![Tests](https://img.shields.io/badge/tests-22%20✓-success)
55
57
  ![Coverage](https://img.shields.io/badge/coverage-100%25-success)
56
58
  ![Issues](https://img.shields.io/github/issues/zx80/cachetools-utils?style=flat)
57
59
  ![Python](https://img.shields.io/badge/python-3-informational)
@@ -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` and `hash_json_key`: JSON-based cache key serialization functions
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
  ![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-21%20✓-success)
10
+ ![Tests](https://img.shields.io/badge/tests-22%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)
@@ -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` and `hash_json_key`: JSON-based cache key serialization functions
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 = "9.3"
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