CacheToolsUtils 9.2__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.2
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
 
@@ -380,10 +382,10 @@ def cached(cache, *args, **kwargs):
380
382
 
381
383
  def decorate(fun: Callable):
382
384
 
383
- # use cachetools
385
+ # use cachetools "cached" decorator
384
386
  fun = cachetools.cached(cache, *args, **kwargs)(fun)
385
387
 
386
- # extend
388
+ # extend it with two functions
387
389
  def cache_in(*args, **kwargs) -> bool:
388
390
  """Tell whether key is already in cache."""
389
391
  key = fun.cache_key(*args, **kwargs) # type: ignore
@@ -409,12 +411,19 @@ def cached(cache, *args, **kwargs):
409
411
  MapGen = Callable[[MutableMapping, str], MutableMapping]
410
412
 
411
413
 
412
- def cacheMethods(cache: MutableMapping, obj: Any, gen: MapGen = PrefixedCache, **funs):
414
+ def cacheMethods(
415
+ cache: MutableMapping,
416
+ obj: Any,
417
+ gen: MapGen = PrefixedCache,
418
+ opts: dict[str, Any] = {},
419
+ **funs
420
+ ):
413
421
  """Cache some object methods.
414
422
 
415
423
  :param cache: cache to use.
416
424
  :param obj: object instance to be cached.
417
425
  :param gen: generator of PrefixedCache.
426
+ :param opts: additional parameters when calling `cached`.
418
427
  :param funs: name of methods and corresponding prefix
419
428
 
420
429
  .. code-block:: python
@@ -426,17 +435,22 @@ def cacheMethods(cache: MutableMapping, obj: Any, gen: MapGen = PrefixedCache, *
426
435
  f = getattr(obj, fun)
427
436
  while hasattr(f, "__wrapped__"):
428
437
  f = f.__wrapped__
429
- setattr(obj, fun, cached(cache=gen(cache, prefix))(f))
438
+ setattr(obj, fun, cached(cache=gen(cache, prefix), **opts)(f))
430
439
 
431
440
 
432
441
  def cacheFunctions(
433
- cache: MutableMapping, globs: MutableMapping[str, Any], gen: MapGen = PrefixedCache, **funs
442
+ cache: MutableMapping,
443
+ globs: MutableMapping[str, Any],
444
+ gen: MapGen = PrefixedCache,
445
+ opts: dict[str, Any] = {},
446
+ **funs
434
447
  ):
435
448
  """Cache some global functions, with a prefix.
436
449
 
437
450
  :param cache: cache to use.
438
451
  :param globs: global object dictionary.
439
452
  :param gen: generator of PrefixedCache.
453
+ :param opts: additional parameters when calling `cached`.
440
454
  :param funs: name of functions and corresponding prefix
441
455
 
442
456
  .. code-block:: python
@@ -448,12 +462,14 @@ def cacheFunctions(
448
462
  f = globs[fun]
449
463
  while hasattr(f, "__wrapped__"):
450
464
  f = f.__wrapped__
451
- globs[fun] = cached(cache=gen(cache, prefix))(f)
465
+ globs[fun] = cached(cache=gen(cache, prefix), **opts)(f)
452
466
 
453
467
 
454
468
  # JSON-based key function
455
469
  # NOTE obviously this only works if parameters are json-serializable…
456
- def json_key(*args, **kwargs):
470
+ def json_key(*args, **kwargs) -> str:
471
+ """JSON serialization of arguments."""
472
+ val: Any
457
473
  if kwargs: # object
458
474
  if args:
459
475
  val = {"*": args, "**": kwargs}
@@ -464,19 +480,17 @@ def json_key(*args, **kwargs):
464
480
  return json.dumps(val, sort_keys=True, separators=(",", ":"))
465
481
 
466
482
 
467
- # Hmmm…
483
+ # Hmmm… is this useful?
468
484
  class _HashJsonKey:
469
485
  """A cache key with a persistant hash value."""
470
486
 
471
487
  def __init__(self, *args, **kwargs):
472
- self._hashed = None
473
488
  self._args = args
474
489
  self._kwargs = kwargs
475
490
  self._key = json_key(*args, **kwargs)
491
+ self._hashed = self._key.__hash__()
476
492
 
477
493
  def __hash__(self):
478
- if self._hashed is None:
479
- self._hashed = self._key.__hash__()
480
494
  return self._hashed
481
495
 
482
496
  def __str__(self):
@@ -487,9 +501,100 @@ def hash_json_key(*args, **kwargs):
487
501
  return _HashJsonKey(*args, **kwargs)
488
502
 
489
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
+
490
594
  #
491
595
  # MEMCACHED
492
596
  #
597
+ # FIXME what about bytes?
493
598
  class JsonSerde:
494
599
  """JSON serialize/deserialize class for MemCached (``pymemcache``).
495
600
 
@@ -504,8 +609,10 @@ class JsonSerde:
504
609
  def serialize(self, key, value):
505
610
  if isinstance(value, str):
506
611
  return value.encode("utf-8"), 1
612
+ elif isinstance(value, bytes):
613
+ return value, 3
507
614
  else:
508
- return json.dumps(value, sort_keys=True).encode("utf-8"), 2
615
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"), 2
509
616
 
510
617
  # reverse previous serialization
511
618
  def deserialize(self, key, value, flag):
@@ -513,6 +620,8 @@ class JsonSerde:
513
620
  return value.decode("utf-8")
514
621
  elif flag == 2:
515
622
  return json.loads(value.decode("utf-8"))
623
+ elif flag == 3:
624
+ return value
516
625
  else:
517
626
  raise Exception("Unknown serialization format")
518
627
 
@@ -586,25 +695,15 @@ class PrefixedMemCached(MemCached):
586
695
  return self._prefix + base64.b85encode(str(key).encode("utf-8"))
587
696
 
588
697
 
589
- class StatsMemCached(MemCached):
590
- """Cache MemCached-compatible class with stats.
591
-
592
- This class is empty and only kept for compatibility.
593
- """
594
-
595
- pass
596
-
597
-
598
698
  #
599
699
  # REDIS
600
700
  #
601
-
602
-
603
701
  class RedisCache(MutableMapping):
604
702
  """Redis TTL-ed wrapper for cachetools (``redis``).
605
703
 
606
704
  :param cache: actual redis cache.
607
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*.
608
707
 
609
708
  Keys and values are serialized in *JSON*.
610
709
 
@@ -616,24 +715,25 @@ class RedisCache(MutableMapping):
616
715
  cache = ctu.RedisCache(redis.Redis(host="localhost"), 3600)
617
716
  """
618
717
 
619
- def __init__(self, cache, ttl=600):
718
+ def __init__(self, cache, ttl=600, raw=False):
620
719
  # import redis
621
720
  # assert isinstance(cache, redis.Redis)
622
721
  self._cache = cache
623
722
  self._ttl = ttl
723
+ self._raw = raw
624
724
 
625
725
  def clear(self): # pragma: no cover
626
726
  """Flush Redis contents."""
627
727
  return self._cache.flushdb()
628
728
 
629
729
  def _serialize(self, s):
630
- return json.dumps(s, sort_keys=True)
730
+ return s if self._raw else json.dumps(s, sort_keys=True, separators=(",", ":"))
631
731
 
632
732
  def _deserialize(self, s):
633
- return json.loads(s)
733
+ return s if self._raw else json.loads(s)
634
734
 
635
735
  def _key(self, key):
636
- return json.dumps(key, sort_keys=True)
736
+ return key if self._raw else json.dumps(key, sort_keys=True, separators=(",", ":"))
637
737
 
638
738
  def __getitem__(self, index):
639
739
  val = self._cache.get(self._key(index))
@@ -713,10 +813,11 @@ class PrefixedRedisCache(RedisCache):
713
813
  return self._prefix + str(key)
714
814
 
715
815
 
816
+ # FIXME should it be removed?
716
817
  class StatsRedisCache(PrefixedRedisCache):
717
818
  """TTL-ed Redis wrapper class for cachetools.
718
819
 
719
- This class is empty and only kept for compatibility.
820
+ This class is nearly empty.
720
821
  """
721
822
 
722
823
  def flushdb(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CacheToolsUtils
3
- Version: 9.2
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.2"
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