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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 9.3
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
  ![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,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` and `hash_json_key`: JSON-based cache key serialization functions
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
 
@@ -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
 
@@ -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 and only kept for compatibility.
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: 9.3
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
  ![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,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` and `hash_json_key`: JSON-based cache key serialization functions
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
  ![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,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` and `hash_json_key`: JSON-based cache key serialization functions
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 = "9.3"
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