redis-dict 2.7.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- redis_dict/__init__.py +17 -0
- redis_dict.py → redis_dict/core.py +229 -321
- redis_dict/py.typed +0 -0
- redis_dict/type_management.py +273 -0
- {redis_dict-2.7.0.dist-info → redis_dict-3.0.0.dist-info}/METADATA +74 -34
- redis_dict-3.0.0.dist-info/RECORD +9 -0
- redis_dict-2.7.0.dist-info/RECORD +0 -6
- {redis_dict-2.7.0.dist-info → redis_dict-3.0.0.dist-info}/LICENSE +0 -0
- {redis_dict-2.7.0.dist-info → redis_dict-3.0.0.dist-info}/WHEEL +0 -0
- {redis_dict-2.7.0.dist-info → redis_dict-3.0.0.dist-info}/top_level.txt +0 -0
redis_dict/__init__.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
"""__init__ module for redis dict."""
|
2
|
+
from importlib.metadata import version, PackageNotFoundError
|
3
|
+
|
4
|
+
from .core import RedisDict
|
5
|
+
from .type_management import decoding_registry, encoding_registry, RedisDictJSONEncoder, RedisDictJSONDecoder
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
'RedisDict',
|
9
|
+
'decoding_registry',
|
10
|
+
'encoding_registry',
|
11
|
+
'RedisDictJSONEncoder',
|
12
|
+
'RedisDictJSONDecoder',
|
13
|
+
]
|
14
|
+
try:
|
15
|
+
__version__ = version("redis-dict")
|
16
|
+
except PackageNotFoundError:
|
17
|
+
__version__ = "0.0.0"
|
@@ -1,174 +1,23 @@
|
|
1
|
-
"""
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
interacting with Redis as if it were a Python dictionary. The simple yet powerful library
|
6
|
-
enables you to manage key-value pairs in Redis using native Python syntax of dictionary. It supports
|
7
|
-
various data types, including strings, integers, floats, booleans, lists, and dictionaries,
|
8
|
-
and includes additional utility functions for more complex use cases.
|
9
|
-
|
10
|
-
By leveraging Redis for efficient key-value storage, RedisDict allows for high-performance
|
11
|
-
data management and is particularly useful for handling large datasets that may exceed local
|
12
|
-
memory capacity.
|
13
|
-
|
14
|
-
## Features
|
15
|
-
|
16
|
-
* **Dictionary-like interface**: Use familiar Python dictionary syntax to interact with Redis.
|
17
|
-
* **Data Type Support**: Comprehensive support for various data types, including strings,
|
18
|
-
integers, floats, booleans, lists, dictionaries, sets, and tuples.
|
19
|
-
* **Pipelining support**: Use pipelines for batch operations to improve performance.
|
20
|
-
* **Expiration Support**: Enables the setting of expiration times either globally or individually
|
21
|
-
per key, through the use of context managers.
|
22
|
-
* **Efficiency and Scalability**: RedisDict is designed for use with large datasets and is
|
23
|
-
optimized for efficiency. It retrieves only the data needed for a particular operation,
|
24
|
-
ensuring efficient memory usage and fast performance.
|
25
|
-
* **Namespace Management**: Provides simple and efficient namespace handling to help organize
|
26
|
-
and manage data in Redis, streamlining data access and manipulation.
|
27
|
-
* **Distributed Computing**: With its ability to seamlessly connect to other instances or
|
28
|
-
servers with access to the same Redis instance, RedisDict enables easy distributed computing.
|
29
|
-
* **Custom data types**: Add custom types and transformations to suit your specific needs.
|
30
|
-
|
31
|
-
New feature
|
32
|
-
|
33
|
-
Custom extendable Validity checks on keys, and values.to support redis-dict base exceptions with messages from
|
34
|
-
enabling detailed reporting on the reasons for specific validation failures. This refactor would allow users
|
35
|
-
to configure which validity checks to execute, integrate custom validation functions, and specify whether
|
36
|
-
to raise an error on validation failures or to drop the operation and log a warning.
|
37
|
-
|
38
|
-
For example, in a caching scenario, data should only be cached if it falls within defined minimum and
|
39
|
-
maximum size constraints. This approach enables straightforward dictionary set operations while ensuring
|
40
|
-
that only meaningful data is cached: values greater than 10 MB and less than 100 MB should be cached;
|
41
|
-
otherwise, they will be dropped.
|
42
|
-
|
43
|
-
>>> def my_custom_validity_check(value: str) -> None:
|
44
|
-
\"""
|
45
|
-
Validates the size of the input.
|
46
|
-
|
47
|
-
Args:
|
48
|
-
value (str): string to validate.
|
49
|
-
|
50
|
-
Raises:
|
51
|
-
RedisDictValidityException: If the length of the input is not within the allowed range.
|
52
|
-
\"""
|
53
|
-
min_size = 10 * 1024: # Minimum size: 10 KB
|
54
|
-
max_size = 10 * 1024 * 1024: # Maximum size: 10 MB
|
55
|
-
if len(value) < min_size
|
56
|
-
raise RedisDictValidityException(f"value is too small: {len(value)} bytes")
|
57
|
-
if len(value) > max_size
|
58
|
-
raise RedisDictValidityException(f"value is too large: {len(value)} bytes")
|
59
|
-
|
60
|
-
>>> cache = RedisDict(namespace='cache_valid_results_for_1_minute',
|
61
|
-
... expire=60,
|
62
|
-
... custom_valid_values_checks=[my_custom_validity_check], # extend with new valid check
|
63
|
-
... validity_exception_suppress=True) # when value is invalid, don't store, and don't raise an exception.
|
64
|
-
>>> too_small = "too small to cache"
|
65
|
-
>>> cache["1234"] = too_small # Since the value is below 10kb, thus there is no reason to cache the value.
|
66
|
-
>>> cache.get("1234") is None
|
67
|
-
>>> True
|
68
|
-
"""
|
69
|
-
# Types imports
|
70
|
-
import json
|
71
|
-
from datetime import datetime, time, timedelta, date
|
72
|
-
from decimal import Decimal
|
73
|
-
from uuid import UUID
|
74
|
-
from collections import OrderedDict, defaultdict
|
75
|
-
import base64
|
76
|
-
|
77
|
-
from typing import Any, Callable, Dict, Iterator, Set, List, Tuple, Union, Optional
|
1
|
+
"""Redis Dict module."""
|
2
|
+
from typing import Any, Dict, Iterator, List, Tuple, Union, Optional, Type
|
3
|
+
|
4
|
+
from datetime import timedelta
|
78
5
|
from contextlib import contextmanager
|
6
|
+
from collections.abc import Mapping
|
79
7
|
|
80
8
|
from redis import StrictRedis
|
81
9
|
|
82
|
-
SENTINEL
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
EncodeType = Dict[str, EncodeFuncType]
|
88
|
-
DecodeType = Dict[str, DecodeFuncType]
|
89
|
-
|
90
|
-
|
91
|
-
def _create_default_encode(custom_encode_method: str) -> EncodeFuncType:
|
92
|
-
def default_encode(obj: Any) -> str:
|
93
|
-
return getattr(obj, custom_encode_method)() # type: ignore[no-any-return]
|
94
|
-
return default_encode
|
95
|
-
|
96
|
-
|
97
|
-
def _create_default_decode(cls: type, custom_decode_method: str) -> DecodeFuncType:
|
98
|
-
def default_decode(encoded_str: str) -> Any:
|
99
|
-
return getattr(cls, custom_decode_method)(encoded_str)
|
100
|
-
return default_decode
|
101
|
-
|
102
|
-
|
103
|
-
def _decode_tuple(val: str) -> Tuple[Any, ...]:
|
104
|
-
"""
|
105
|
-
Deserialize a JSON-formatted string to a tuple.
|
106
|
-
|
107
|
-
This function takes a JSON-formatted string, deserializes it to a list, and
|
108
|
-
then converts the list to a tuple.
|
109
|
-
|
110
|
-
Args:
|
111
|
-
val (str): A JSON-formatted string representing a list.
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
Tuple[Any, ...]: A tuple with the deserialized values from the input string.
|
115
|
-
"""
|
116
|
-
return tuple(json.loads(val))
|
117
|
-
|
118
|
-
|
119
|
-
def _encode_tuple(val: Tuple[Any, ...]) -> str:
|
120
|
-
"""
|
121
|
-
Serialize a tuple to a JSON-formatted string.
|
122
|
-
|
123
|
-
This function takes a tuple, converts it to a list, and then serializes
|
124
|
-
the list to a JSON-formatted string.
|
125
|
-
|
126
|
-
Args:
|
127
|
-
val (Tuple[Any, ...]): A tuple with values to be serialized.
|
128
|
-
|
129
|
-
Returns:
|
130
|
-
str: A JSON-formatted string representing the input tuple.
|
131
|
-
"""
|
132
|
-
return json.dumps(list(val))
|
133
|
-
|
134
|
-
|
135
|
-
def _decode_set(val: str) -> Set[Any]:
|
136
|
-
"""
|
137
|
-
Deserialize a JSON-formatted string to a set.
|
138
|
-
|
139
|
-
This function takes a JSON-formatted string, deserializes it to a list, and
|
140
|
-
then converts the list to a set.
|
141
|
-
|
142
|
-
Args:
|
143
|
-
val (str): A JSON-formatted string representing a list.
|
144
|
-
|
145
|
-
Returns:
|
146
|
-
set[Any]: A set with the deserialized values from the input string.
|
147
|
-
"""
|
148
|
-
return set(json.loads(val))
|
149
|
-
|
150
|
-
|
151
|
-
def _encode_set(val: Set[Any]) -> str:
|
152
|
-
"""
|
153
|
-
Serialize a set to a JSON-formatted string.
|
154
|
-
|
155
|
-
This function takes a set, converts it to a list, and then serializes the
|
156
|
-
list to a JSON-formatted string.
|
157
|
-
|
158
|
-
Args:
|
159
|
-
val (set[Any]): A set with values to be serialized.
|
160
|
-
|
161
|
-
Returns:
|
162
|
-
str: A JSON-formatted string representing the input set.
|
163
|
-
"""
|
164
|
-
return json.dumps(list(val))
|
10
|
+
from redis_dict.type_management import SENTINEL, EncodeFuncType, DecodeFuncType, EncodeType, DecodeType
|
11
|
+
from redis_dict.type_management import _create_default_encode, _create_default_decode, _default_decoder
|
12
|
+
from redis_dict.type_management import encoding_registry as enc_reg
|
13
|
+
from redis_dict.type_management import decoding_registry as dec_reg
|
165
14
|
|
166
15
|
|
167
16
|
# pylint: disable=R0902, R0904
|
168
17
|
class RedisDict:
|
169
|
-
"""
|
170
|
-
|
171
|
-
custom data types, pipelining, and key expiration.
|
18
|
+
"""Python dictionary with Redis as backend.
|
19
|
+
|
20
|
+
With support for advanced features, such as custom data types, pipelining, and key expiration.
|
172
21
|
|
173
22
|
This class provides a dictionary-like interface that interacts with a Redis database, allowing
|
174
23
|
for efficient storage and retrieval of key-value pairs. It supports various data types, including
|
@@ -194,70 +43,35 @@ class RedisDict:
|
|
194
43
|
expire (Union[int, None]): An optional expiration time for keys, in seconds.
|
195
44
|
|
196
45
|
"""
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
type(0.1).__name__: float,
|
201
|
-
type(True).__name__: lambda x: x == "True",
|
202
|
-
type(None).__name__: lambda x: None,
|
203
|
-
|
204
|
-
"list": json.loads,
|
205
|
-
"dict": json.loads,
|
206
|
-
"tuple": _decode_tuple,
|
207
|
-
type(set()).__name__: _decode_set,
|
208
|
-
|
209
|
-
datetime.__name__: datetime.fromisoformat,
|
210
|
-
date.__name__: date.fromisoformat,
|
211
|
-
time.__name__: time.fromisoformat,
|
212
|
-
timedelta.__name__: lambda x: timedelta(seconds=float(x)),
|
213
|
-
|
214
|
-
Decimal.__name__: Decimal,
|
215
|
-
complex.__name__: lambda x: complex(*map(float, x.split(','))),
|
216
|
-
bytes.__name__: base64.b64decode,
|
217
|
-
|
218
|
-
UUID.__name__: UUID,
|
219
|
-
OrderedDict.__name__: lambda x: OrderedDict(json.loads(x)),
|
220
|
-
defaultdict.__name__: lambda x: defaultdict(type(None), json.loads(x)),
|
221
|
-
frozenset.__name__: lambda x: frozenset(json.loads(x)),
|
222
|
-
}
|
223
|
-
|
224
|
-
encoding_registry: EncodeType = {
|
225
|
-
"list": json.dumps,
|
226
|
-
"dict": json.dumps,
|
227
|
-
"tuple": _encode_tuple,
|
228
|
-
type(set()).__name__: _encode_set,
|
229
|
-
|
230
|
-
datetime.__name__: datetime.isoformat,
|
231
|
-
date.__name__: date.isoformat,
|
232
|
-
time.__name__: time.isoformat,
|
233
|
-
timedelta.__name__: lambda x: str(x.total_seconds()),
|
234
|
-
|
235
|
-
complex.__name__: lambda x: f"{x.real},{x.imag}",
|
236
|
-
bytes.__name__: lambda x: base64.b64encode(x).decode('ascii'),
|
237
|
-
OrderedDict.__name__: lambda x: json.dumps(list(x.items())),
|
238
|
-
defaultdict.__name__: lambda x: json.dumps(dict(x)),
|
239
|
-
frozenset.__name__: lambda x: json.dumps(list(x)),
|
240
|
-
}
|
46
|
+
|
47
|
+
encoding_registry: EncodeType = enc_reg
|
48
|
+
decoding_registry: DecodeType = dec_reg
|
241
49
|
|
242
50
|
def __init__(self,
|
243
51
|
namespace: str = 'main',
|
244
52
|
expire: Union[int, timedelta, None] = None,
|
245
53
|
preserve_expiration: Optional[bool] = False,
|
54
|
+
redis: "Optional[StrictRedis[Any]]" = None,
|
246
55
|
**redis_kwargs: Any) -> None:
|
247
|
-
"""
|
248
|
-
|
56
|
+
"""Initialize a RedisDict instance.
|
57
|
+
|
58
|
+
Init the RedisDict instance.
|
249
59
|
|
250
60
|
Args:
|
251
|
-
namespace (str
|
252
|
-
expire (int, timedelta, optional): Expiration time for keys
|
253
|
-
preserve_expiration (bool, optional): Preserve
|
254
|
-
|
61
|
+
namespace (str): A prefix for keys stored in Redis.
|
62
|
+
expire (Union[int, timedelta, None], optional): Expiration time for keys.
|
63
|
+
preserve_expiration (Optional[bool], optional): Preserve expiration on key updates.
|
64
|
+
redis (Optional[StrictRedis[Any]], optional): A Redis connection instance.
|
65
|
+
**redis_kwargs (Any): Additional kwargs for Redis connection if not provided.
|
255
66
|
"""
|
256
67
|
|
257
68
|
self.namespace: str = namespace
|
258
69
|
self.expire: Union[int, timedelta, None] = expire
|
259
70
|
self.preserve_expiration: Optional[bool] = preserve_expiration
|
260
|
-
|
71
|
+
if redis:
|
72
|
+
redis.connection_pool.connection_kwargs["decode_responses"] = True
|
73
|
+
|
74
|
+
self.redis: StrictRedis[Any] = redis or StrictRedis(decode_responses=True, **redis_kwargs)
|
261
75
|
self.get_redis: StrictRedis[Any] = self.redis
|
262
76
|
|
263
77
|
self.custom_encode_method = "encode"
|
@@ -288,7 +102,7 @@ class RedisDict:
|
|
288
102
|
length does not exceed the maximum allowed size (500 MB).
|
289
103
|
|
290
104
|
Args:
|
291
|
-
val (
|
105
|
+
val (Any): The input value to be validated.
|
292
106
|
val_type (str): The type of the input value ("str", "int", "float", or "bool").
|
293
107
|
|
294
108
|
Returns:
|
@@ -298,7 +112,19 @@ class RedisDict:
|
|
298
112
|
return len(val) < self._max_string_size
|
299
113
|
return True
|
300
114
|
|
301
|
-
def _format_value(self, key: str,
|
115
|
+
def _format_value(self, key: str, value: Any) -> str:
|
116
|
+
"""Format a valid value with the type and encoded representation of the value.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
key (str): The key of the value to be formatted.
|
120
|
+
value (Any): The value to be encoded and formatted.
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
ValueError: If the value or key fail validation.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
str: The formatted value with the type and encoded representation of the value.
|
127
|
+
"""
|
302
128
|
store_type, key = type(value).__name__, str(key)
|
303
129
|
if not self._valid_input(value, store_type) or not self._valid_input(key, "str"):
|
304
130
|
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
@@ -355,7 +181,7 @@ class RedisDict:
|
|
355
181
|
Any: The transformed Python object.
|
356
182
|
"""
|
357
183
|
type_, value = result.split(':', 1)
|
358
|
-
return self.decoding_registry.get(type_,
|
184
|
+
return self.decoding_registry.get(type_, _default_decoder)(value)
|
359
185
|
|
360
186
|
def new_type_compliance(
|
361
187
|
self,
|
@@ -363,8 +189,7 @@ class RedisDict:
|
|
363
189
|
encode_method_name: Optional[str] = None,
|
364
190
|
decode_method_name: Optional[str] = None,
|
365
191
|
) -> None:
|
366
|
-
"""
|
367
|
-
Checks if a class complies with the required encoding and decoding methods.
|
192
|
+
"""Check if a class complies with the required encoding and decoding methods.
|
368
193
|
|
369
194
|
Args:
|
370
195
|
class_type (type): The class to check for compliance.
|
@@ -386,6 +211,7 @@ class RedisDict:
|
|
386
211
|
raise NotImplementedError(
|
387
212
|
f"Class {class_type.__name__} does not implement the required {decode_method_name} class method.")
|
388
213
|
|
214
|
+
# pylint: disable=too-many-arguments
|
389
215
|
def extends_type(
|
390
216
|
self,
|
391
217
|
class_type: type,
|
@@ -394,31 +220,21 @@ class RedisDict:
|
|
394
220
|
encoding_method_name: Optional[str] = None,
|
395
221
|
decoding_method_name: Optional[str] = None,
|
396
222
|
) -> None:
|
397
|
-
"""
|
398
|
-
Extends RedisDict to support a custom type in the encode/decode mapping.
|
223
|
+
"""Extend RedisDict to support a custom type in the encode/decode mapping.
|
399
224
|
|
400
225
|
This method enables serialization of instances based on their type,
|
401
226
|
allowing for custom types, specialized storage formats, and more.
|
402
227
|
There are three ways to add custom types:
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
228
|
+
1. Have a class with an `encode` instance method and a `decode` class method.
|
229
|
+
2. Have a class and pass encoding and decoding functions, where
|
230
|
+
`encode` converts the class instance to a string, and
|
231
|
+
`decode` takes the string and recreates the class instance.
|
232
|
+
3. Have a class that already has serialization methods, that satisfies the:
|
233
|
+
EncodeFuncType = Callable[[Any], str]
|
234
|
+
DecodeFuncType = Callable[[str], Any]
|
410
235
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
Args:
|
415
|
-
class_type (Type[type]): The class `__name__` will become the key for the encoding and decoding functions.
|
416
|
-
encode (Optional[EncodeFuncType]): function that encodes an object into a storable string format.
|
417
|
-
This function should take an instance of `class_type` as input and return a string.
|
418
|
-
decode (Optional[DecodeFuncType]): function that decodes a string back into an object of `class_type`.
|
419
|
-
This function should take a string as input and return an instance of `class_type`.
|
420
|
-
encoding_method_name (str, optional): Name of encoding method of the class for redis-dict custom types.
|
421
|
-
decoding_method_name (str, optional): Name of decoding method of the class for redis-dict custom types.
|
236
|
+
`custom_encode_method`
|
237
|
+
`custom_decode_method`
|
422
238
|
|
423
239
|
If no encoding or decoding function is provided, default to use the `encode` and `decode` methods of the class.
|
424
240
|
|
@@ -445,11 +261,21 @@ class RedisDict:
|
|
445
261
|
|
446
262
|
redis_dict.extends_type(Person)
|
447
263
|
|
264
|
+
Args:
|
265
|
+
class_type (type): The class `__name__` will become the key for the encoding and decoding functions.
|
266
|
+
encode (Optional[EncodeFuncType]): function that encodes an object into a storable string format.
|
267
|
+
decode (Optional[DecodeFuncType]): function that decodes a string back into an object of `class_type`.
|
268
|
+
encoding_method_name (str, optional): Name of encoding method of the class for redis-dict custom types.
|
269
|
+
decoding_method_name (str, optional): Name of decoding method of the class for redis-dict custom types.
|
270
|
+
|
271
|
+
Raises:
|
272
|
+
NotImplementedError
|
273
|
+
|
448
274
|
Note:
|
449
|
-
|
275
|
+
You can check for compliance of a class separately using the `new_type_compliance` method:
|
450
276
|
|
451
|
-
|
452
|
-
|
277
|
+
This method raises a NotImplementedError if either `encode` or `decode` is `None`
|
278
|
+
and the class does not implement the corresponding method.
|
453
279
|
"""
|
454
280
|
|
455
281
|
if encode is None or decode is None:
|
@@ -460,7 +286,7 @@ class RedisDict:
|
|
460
286
|
|
461
287
|
if decode is None:
|
462
288
|
decode_method_name = decoding_method_name or self.custom_decode_method
|
463
|
-
self.new_type_compliance(class_type,
|
289
|
+
self.new_type_compliance(class_type, decode_method_name=decode_method_name)
|
464
290
|
decode = _create_default_decode(class_type, decode_method_name)
|
465
291
|
|
466
292
|
type_name = class_type.__name__
|
@@ -479,7 +305,7 @@ class RedisDict:
|
|
479
305
|
"""
|
480
306
|
if len(self) != len(other):
|
481
307
|
return False
|
482
|
-
for key, value in self.
|
308
|
+
for key, value in self.items():
|
483
309
|
if value != other.get(key, SENTINEL):
|
484
310
|
return False
|
485
311
|
return True
|
@@ -561,7 +387,7 @@ class RedisDict:
|
|
561
387
|
Returns:
|
562
388
|
Iterator[str]: An iterator over the keys of the RedisDict.
|
563
389
|
"""
|
564
|
-
self._iter = self.
|
390
|
+
self._iter = self.keys()
|
565
391
|
return self
|
566
392
|
|
567
393
|
def __repr__(self) -> str:
|
@@ -582,15 +408,102 @@ class RedisDict:
|
|
582
408
|
"""
|
583
409
|
return str(self.to_dict())
|
584
410
|
|
411
|
+
def __or__(self, other: Dict[str, Any]) -> Dict[str, Any]:
|
412
|
+
"""
|
413
|
+
Implements the | operator (dict union).
|
414
|
+
Returns a new dictionary with items from both dictionaries.
|
415
|
+
|
416
|
+
Args:
|
417
|
+
other (Dict[str, Any]): The dictionary to merge with.
|
418
|
+
|
419
|
+
Raises:
|
420
|
+
TypeError: If other does not adhere to Mapping.
|
421
|
+
|
422
|
+
Returns:
|
423
|
+
Dict[str, Any]: A new dictionary containing items from both dictionaries.
|
424
|
+
"""
|
425
|
+
if not isinstance(other, Mapping):
|
426
|
+
raise TypeError(f"unsupported operand type(s) for |: '{type(other).__name__}' and 'RedisDict'")
|
427
|
+
|
428
|
+
result = {}
|
429
|
+
result.update(self.to_dict())
|
430
|
+
result.update(other)
|
431
|
+
return result
|
432
|
+
|
433
|
+
def __ror__(self, other: Dict[str, Any]) -> Dict[str, Any]:
|
434
|
+
"""
|
435
|
+
Implements the reverse | operator.
|
436
|
+
Called when RedisDict is on the right side of |.
|
437
|
+
|
438
|
+
Args:
|
439
|
+
other (Dict[str, Any]): The dictionary to merge with.
|
440
|
+
|
441
|
+
Raises:
|
442
|
+
TypeError: If other does not adhere to Mapping.
|
443
|
+
|
444
|
+
Returns:
|
445
|
+
Dict[str, Any]: A new dictionary containing items from both dictionaries.
|
446
|
+
"""
|
447
|
+
if not isinstance(other, Mapping):
|
448
|
+
raise TypeError(f"unsupported operand type(s) for |: 'RedisDict' and '{type(other).__name__}'")
|
449
|
+
|
450
|
+
result = {}
|
451
|
+
result.update(other)
|
452
|
+
result.update(self.to_dict())
|
453
|
+
return result
|
454
|
+
|
455
|
+
def __ior__(self, other: Dict[str, Any]) -> 'RedisDict':
|
456
|
+
"""
|
457
|
+
Implements the |= operator (in-place union).
|
458
|
+
Modifies the current dictionary by adding items from other.
|
459
|
+
|
460
|
+
Args:
|
461
|
+
other (Dict[str, Any]): The dictionary to merge with.
|
462
|
+
|
463
|
+
Raises:
|
464
|
+
TypeError: If other does not adhere to Mapping.
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
RedisDict: The modified RedisDict instance.
|
468
|
+
"""
|
469
|
+
if not isinstance(other, Mapping):
|
470
|
+
raise TypeError(f"unsupported operand type(s) for |: '{type(other).__name__}' and 'RedisDict'")
|
471
|
+
|
472
|
+
self.update(other)
|
473
|
+
return self
|
474
|
+
|
475
|
+
@classmethod
|
476
|
+
def __class_getitem__(cls: Type['RedisDict'], key: Any) -> Type['RedisDict']:
|
477
|
+
"""
|
478
|
+
Enables type hinting support like RedisDict[str, Any].
|
479
|
+
|
480
|
+
Args:
|
481
|
+
key (Any): The type parameter(s) used in the type hint.
|
482
|
+
|
483
|
+
Returns:
|
484
|
+
Type[RedisDict]: The class itself, enabling type hint usage.
|
485
|
+
"""
|
486
|
+
return cls
|
487
|
+
|
488
|
+
def __reversed__(self) -> Iterator[str]:
|
489
|
+
"""
|
490
|
+
Implements reversed() built-in:
|
491
|
+
Returns an iterator over dictionary keys in reverse insertion order.
|
492
|
+
|
493
|
+
Warning:
|
494
|
+
RedisDict Currently does not support 'insertion order' as property thus also not reversed.
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
Iterator[str]: An iterator yielding the dictionary keys in reverse order.
|
498
|
+
"""
|
499
|
+
return reversed(list(self.keys()))
|
500
|
+
|
585
501
|
def __next__(self) -> str:
|
586
502
|
"""
|
587
503
|
Get the next item in the iterator.
|
588
504
|
|
589
505
|
Returns:
|
590
506
|
str: The next item in the iterator.
|
591
|
-
|
592
|
-
Raises:
|
593
|
-
StopIteration: If there are no more items.
|
594
507
|
"""
|
595
508
|
return next(self._iter)
|
596
509
|
|
@@ -601,8 +514,6 @@ class RedisDict:
|
|
601
514
|
Returns:
|
602
515
|
str: The next item in the iterator.
|
603
516
|
|
604
|
-
Raises:
|
605
|
-
StopIteration: If there are no more items.
|
606
517
|
"""
|
607
518
|
return next(self)
|
608
519
|
|
@@ -633,7 +544,7 @@ class RedisDict:
|
|
633
544
|
Scan for Redis keys matching the given search term.
|
634
545
|
|
635
546
|
Args:
|
636
|
-
search_term (str
|
547
|
+
search_term (str): A search term to filter keys. Defaults to ''.
|
637
548
|
|
638
549
|
Returns:
|
639
550
|
Iterator[str]: An iterator of matching Redis keys.
|
@@ -642,8 +553,8 @@ class RedisDict:
|
|
642
553
|
return self.get_redis.scan_iter(match=search_query)
|
643
554
|
|
644
555
|
def get(self, key: str, default: Optional[Any] = None) -> Any:
|
645
|
-
"""
|
646
|
-
|
556
|
+
"""Return the value for the given key if it exists, otherwise return the default value.
|
557
|
+
|
647
558
|
Analogous to a dictionary's get method.
|
648
559
|
|
649
560
|
Args:
|
@@ -651,23 +562,30 @@ class RedisDict:
|
|
651
562
|
default (Optional[Any], optional): The value to return if the key is not found.
|
652
563
|
|
653
564
|
Returns:
|
654
|
-
|
565
|
+
Any: The value associated with the key or the default value.
|
655
566
|
"""
|
656
567
|
found, item = self._load(key)
|
657
568
|
if not found:
|
658
569
|
return default
|
659
570
|
return item
|
660
571
|
|
661
|
-
def
|
662
|
-
"""
|
663
|
-
|
572
|
+
def keys(self) -> Iterator[str]:
|
573
|
+
"""Return an Iterator of keys in the RedisDict, analogous to a dictionary's keys method.
|
574
|
+
|
575
|
+
Returns:
|
576
|
+
Iterator[str]: A list of keys in the RedisDict.
|
664
577
|
"""
|
665
578
|
to_rm = len(self.namespace) + 1
|
666
579
|
return (str(item[to_rm:]) for item in self._scan_keys())
|
667
580
|
|
668
581
|
def key(self, search_term: str = '') -> Optional[str]:
|
669
|
-
"""
|
670
|
-
|
582
|
+
"""Return the first value for search_term if it exists, otherwise return None.
|
583
|
+
|
584
|
+
Args:
|
585
|
+
search_term (str): A search term to filter keys. Defaults to ''.
|
586
|
+
|
587
|
+
Returns:
|
588
|
+
str: The first key associated with the given search term.
|
671
589
|
"""
|
672
590
|
to_rm = len(self.namespace) + 1
|
673
591
|
search_query = self._create_iter_query(search_term)
|
@@ -677,18 +595,11 @@ class RedisDict:
|
|
677
595
|
|
678
596
|
return None
|
679
597
|
|
680
|
-
def
|
681
|
-
"""
|
682
|
-
Return a list of keys in the RedisDict, analogous to a dictionary's keys method.
|
598
|
+
def items(self) -> Iterator[Tuple[str, Any]]:
|
599
|
+
"""Return a list of key-value pairs (tuples) in the RedisDict, analogous to a dictionary's items method.
|
683
600
|
|
684
|
-
|
685
|
-
|
686
|
-
"""
|
687
|
-
return list(self.iterkeys())
|
688
|
-
|
689
|
-
def iteritems(self) -> Iterator[Tuple[str, Any]]:
|
690
|
-
"""
|
691
|
-
Note: for python2 str is needed
|
601
|
+
Yields:
|
602
|
+
Iterator[Tuple[str, Any]]: A list of key-value pairs in the RedisDict.
|
692
603
|
"""
|
693
604
|
to_rm = len(self.namespace) + 1
|
694
605
|
for item in self._scan_keys():
|
@@ -697,31 +608,14 @@ class RedisDict:
|
|
697
608
|
except KeyError:
|
698
609
|
pass
|
699
610
|
|
700
|
-
def
|
701
|
-
"""
|
702
|
-
Return a list of key-value pairs (tuples) in the RedisDict, analogous to a dictionary's items method.
|
611
|
+
def values(self) -> Iterator[Any]:
|
612
|
+
"""Analogous to a dictionary's values method.
|
703
613
|
|
704
|
-
|
705
|
-
List[Tuple[str, Any]]: A list of key-value pairs in the RedisDict.
|
706
|
-
"""
|
707
|
-
return list(self.iteritems())
|
614
|
+
Return a list of values in the RedisDict,
|
708
615
|
|
709
|
-
|
710
|
-
"""
|
711
|
-
Return a list of values in the RedisDict, analogous to a dictionary's values method.
|
712
|
-
|
713
|
-
Returns:
|
616
|
+
Yields:
|
714
617
|
List[Any]: A list of values in the RedisDict.
|
715
618
|
"""
|
716
|
-
return list(self.itervalues())
|
717
|
-
|
718
|
-
def itervalues(self) -> Iterator[Any]:
|
719
|
-
"""
|
720
|
-
Iterate over the values in the RedisDict.
|
721
|
-
|
722
|
-
Returns:
|
723
|
-
Iterator[Any]: An iterator of values in the RedisDict.
|
724
|
-
"""
|
725
619
|
to_rm = len(self.namespace) + 1
|
726
620
|
for item in self._scan_keys():
|
727
621
|
try:
|
@@ -730,8 +624,7 @@ class RedisDict:
|
|
730
624
|
pass
|
731
625
|
|
732
626
|
def to_dict(self) -> Dict[str, Any]:
|
733
|
-
"""
|
734
|
-
Convert the RedisDict to a Python dictionary.
|
627
|
+
"""Convert the RedisDict to a Python dictionary.
|
735
628
|
|
736
629
|
Returns:
|
737
630
|
Dict[str, Any]: A dictionary with the same key-value pairs as the RedisDict.
|
@@ -739,8 +632,7 @@ class RedisDict:
|
|
739
632
|
return dict(self.items())
|
740
633
|
|
741
634
|
def clear(self) -> None:
|
742
|
-
"""
|
743
|
-
Remove all key-value pairs from the RedisDict in one batch operation using pipelining.
|
635
|
+
"""Remove all key-value pairs from the RedisDict in one batch operation using pipelining.
|
744
636
|
|
745
637
|
This method mimics the behavior of the `clear` method from a standard Python dictionary.
|
746
638
|
Redis pipelining is employed to group multiple commands into a single request, minimizing
|
@@ -754,33 +646,33 @@ class RedisDict:
|
|
754
646
|
del self[key]
|
755
647
|
|
756
648
|
def pop(self, key: str, default: Union[Any, object] = SENTINEL) -> Any:
|
757
|
-
"""
|
649
|
+
"""Analogous to a dictionary's pop method.
|
650
|
+
|
758
651
|
Remove the value associated with the given key and return it, or return the default value
|
759
|
-
if the key is not found.
|
652
|
+
if the key is not found.
|
760
653
|
|
761
654
|
Args:
|
762
655
|
key (str): The key to remove the value.
|
763
656
|
default (Optional[Any], optional): The value to return if the key is not found.
|
764
657
|
|
765
658
|
Returns:
|
766
|
-
|
659
|
+
Any: The value associated with the key or the default value.
|
767
660
|
|
768
661
|
Raises:
|
769
662
|
KeyError: If the key is not found and no default value is provided.
|
770
663
|
"""
|
771
|
-
|
772
|
-
|
773
|
-
|
664
|
+
formatted_key = self._format_key(key)
|
665
|
+
value = self.get_redis.execute_command("GETDEL", formatted_key)
|
666
|
+
if value is None:
|
774
667
|
if default is not SENTINEL:
|
775
668
|
return default
|
776
|
-
raise
|
669
|
+
raise KeyError(formatted_key)
|
777
670
|
|
778
|
-
|
779
|
-
return value
|
671
|
+
return self._transform(value)
|
780
672
|
|
781
673
|
def popitem(self) -> Tuple[str, Any]:
|
782
|
-
"""
|
783
|
-
|
674
|
+
"""Remove and return a random (key, value) pair from the RedisDict as a tuple.
|
675
|
+
|
784
676
|
This method is analogous to the `popitem` method of a standard Python dictionary.
|
785
677
|
|
786
678
|
Returns:
|
@@ -799,7 +691,8 @@ class RedisDict:
|
|
799
691
|
continue
|
800
692
|
|
801
693
|
def setdefault(self, key: str, default_value: Optional[Any] = None) -> Any:
|
802
|
-
"""
|
694
|
+
"""Get value under key, and if not present set default value.
|
695
|
+
|
803
696
|
Return the value associated with the given key if it exists, otherwise set the value to the
|
804
697
|
default value and return it. Analogous to a dictionary's setdefault method.
|
805
698
|
|
@@ -810,15 +703,28 @@ class RedisDict:
|
|
810
703
|
Returns:
|
811
704
|
Any: The value associated with the key or the default value.
|
812
705
|
"""
|
813
|
-
|
814
|
-
|
815
|
-
|
706
|
+
formatted_key = self._format_key(key)
|
707
|
+
formatted_value = self._format_value(key, default_value)
|
708
|
+
|
709
|
+
# Setting {"get": True} enables parsing of the redis result as "GET", instead of "SET" command
|
710
|
+
options = {"get": True}
|
711
|
+
args = ["SET", formatted_key, formatted_value, "NX", "GET"]
|
712
|
+
if self.preserve_expiration:
|
713
|
+
args.append("KEEPTTL")
|
714
|
+
elif self.expire is not None:
|
715
|
+
expire_val = int(self.expire.total_seconds()) if isinstance(self.expire, timedelta) else self.expire
|
716
|
+
expire_str = str(1) if expire_val <= 1 else str(expire_val)
|
717
|
+
args.extend(["EX", expire_str])
|
718
|
+
|
719
|
+
result = self.get_redis.execute_command(*args, **options)
|
720
|
+
if result is None:
|
816
721
|
return default_value
|
817
|
-
|
722
|
+
|
723
|
+
return self._transform(result)
|
818
724
|
|
819
725
|
def copy(self) -> Dict[str, Any]:
|
820
|
-
"""
|
821
|
-
|
726
|
+
"""Create a shallow copy of the RedisDict and return it as a standard Python dictionary.
|
727
|
+
|
822
728
|
This method is analogous to the `copy` method of a standard Python dictionary
|
823
729
|
|
824
730
|
Returns:
|
@@ -841,7 +747,8 @@ class RedisDict:
|
|
841
747
|
self[key] = value
|
842
748
|
|
843
749
|
def fromkeys(self, iterable: List[str], value: Optional[Any] = None) -> 'RedisDict':
|
844
|
-
"""
|
750
|
+
"""Create a new RedisDict from an iterable of key-value pairs.
|
751
|
+
|
845
752
|
Create a new RedisDict with keys from the provided iterable and values set to the given value.
|
846
753
|
This method is analogous to the `fromkeys` method of a standard Python dictionary, populating
|
847
754
|
the RedisDict with the keys from the iterable and setting their corresponding values to the
|
@@ -861,10 +768,11 @@ class RedisDict:
|
|
861
768
|
return self
|
862
769
|
|
863
770
|
def __sizeof__(self) -> int:
|
864
|
-
"""
|
865
|
-
|
771
|
+
"""Return the approximate size of the RedisDict in memory, in bytes.
|
772
|
+
|
866
773
|
This method is analogous to the `__sizeof__` method of a standard Python dictionary, estimating
|
867
774
|
the memory consumption of the RedisDict based on the serialized in-memory representation.
|
775
|
+
Should be changed to redis view of the size.
|
868
776
|
|
869
777
|
Returns:
|
870
778
|
int: The approximate size of the RedisDict in memory, in bytes.
|
@@ -906,13 +814,12 @@ class RedisDict:
|
|
906
814
|
# compatibility with Python 3.9 typing
|
907
815
|
@contextmanager
|
908
816
|
def expire_at(self, sec_epoch: Union[int, timedelta]) -> Iterator[None]:
|
909
|
-
"""
|
910
|
-
Context manager to set the expiration time for keys in the RedisDict.
|
817
|
+
"""Context manager to set the expiration time for keys in the RedisDict.
|
911
818
|
|
912
819
|
Args:
|
913
820
|
sec_epoch (int, timedelta): The expiration duration is set using either an integer or a timedelta.
|
914
821
|
|
915
|
-
|
822
|
+
Yields:
|
916
823
|
ContextManager: A context manager during which the expiration time is the time set.
|
917
824
|
"""
|
918
825
|
self.expire, temp = sec_epoch, self.expire
|
@@ -924,7 +831,7 @@ class RedisDict:
|
|
924
831
|
"""
|
925
832
|
Context manager to create a Redis pipeline for batch operations.
|
926
833
|
|
927
|
-
|
834
|
+
Yields:
|
928
835
|
ContextManager: A context manager to create a Redis pipeline batching all operations within the context.
|
929
836
|
"""
|
930
837
|
top_level = False
|
@@ -1006,7 +913,8 @@ class RedisDict:
|
|
1006
913
|
return dict(self.redis.info())
|
1007
914
|
|
1008
915
|
def get_ttl(self, key: str) -> Optional[int]:
|
1009
|
-
"""
|
916
|
+
"""Get the Time To Live from Redis.
|
917
|
+
|
1010
918
|
Get the Time To Live (TTL) in seconds for a given key. If the key does not exist or does not have an
|
1011
919
|
associated `expire`, return None.
|
1012
920
|
|
redis_dict/py.typed
ADDED
File without changes
|
@@ -0,0 +1,273 @@
|
|
1
|
+
"""Type management module."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import base64
|
5
|
+
from collections import OrderedDict, defaultdict
|
6
|
+
from datetime import datetime, date, time, timedelta
|
7
|
+
|
8
|
+
from typing import Callable, Any, Dict, Tuple, Set
|
9
|
+
|
10
|
+
from uuid import UUID
|
11
|
+
from decimal import Decimal
|
12
|
+
|
13
|
+
|
14
|
+
SENTINEL = object()
|
15
|
+
|
16
|
+
EncodeFuncType = Callable[[Any], str]
|
17
|
+
DecodeFuncType = Callable[[str], Any]
|
18
|
+
EncodeType = Dict[str, EncodeFuncType]
|
19
|
+
DecodeType = Dict[str, DecodeFuncType]
|
20
|
+
|
21
|
+
|
22
|
+
def _create_default_encode(custom_encode_method: str) -> EncodeFuncType:
|
23
|
+
def default_encode(obj: Any) -> str:
|
24
|
+
return getattr(obj, custom_encode_method)() # type: ignore[no-any-return]
|
25
|
+
return default_encode
|
26
|
+
|
27
|
+
|
28
|
+
def _create_default_decode(cls: type, custom_decode_method: str) -> DecodeFuncType:
|
29
|
+
def default_decode(encoded_str: str) -> Any:
|
30
|
+
return getattr(cls, custom_decode_method)(encoded_str)
|
31
|
+
return default_decode
|
32
|
+
|
33
|
+
|
34
|
+
def _decode_tuple(val: str) -> Tuple[Any, ...]:
|
35
|
+
"""
|
36
|
+
Deserialize a JSON-formatted string to a tuple.
|
37
|
+
|
38
|
+
This function takes a JSON-formatted string, deserializes it to a list, and
|
39
|
+
then converts the list to a tuple.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
val (str): A JSON-formatted string representing a list.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
Tuple[Any, ...]: A tuple with the deserialized values from the input string.
|
46
|
+
"""
|
47
|
+
return tuple(json.loads(val))
|
48
|
+
|
49
|
+
|
50
|
+
def _encode_tuple(val: Tuple[Any, ...]) -> str:
|
51
|
+
"""
|
52
|
+
Serialize a tuple to a JSON-formatted string.
|
53
|
+
|
54
|
+
This function takes a tuple, converts it to a list, and then serializes
|
55
|
+
the list to a JSON-formatted string.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
val (Tuple[Any, ...]): A tuple with values to be serialized.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
str: A JSON-formatted string representing the input tuple.
|
62
|
+
"""
|
63
|
+
return json.dumps(list(val))
|
64
|
+
|
65
|
+
|
66
|
+
def _decode_set(val: str) -> Set[Any]:
|
67
|
+
"""
|
68
|
+
Deserialize a JSON-formatted string to a set.
|
69
|
+
|
70
|
+
This function takes a JSON-formatted string, deserializes it to a list, and
|
71
|
+
then converts the list to a set.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
val (str): A JSON-formatted string representing a list.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
set[Any]: A set with the deserialized values from the input string.
|
78
|
+
"""
|
79
|
+
return set(json.loads(val))
|
80
|
+
|
81
|
+
|
82
|
+
def _encode_set(val: Set[Any]) -> str:
|
83
|
+
"""
|
84
|
+
Serialize a set to a JSON-formatted string.
|
85
|
+
|
86
|
+
This function takes a set, converts it to a list, and then serializes the
|
87
|
+
list to a JSON-formatted string.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
val (set[Any]): A set with values to be serialized.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
str: A JSON-formatted string representing the input set.
|
94
|
+
"""
|
95
|
+
return json.dumps(list(val))
|
96
|
+
|
97
|
+
|
98
|
+
decoding_registry: DecodeType = {
|
99
|
+
type('').__name__: str,
|
100
|
+
type(1).__name__: int,
|
101
|
+
type(0.1).__name__: float,
|
102
|
+
type(True).__name__: lambda x: x == "True",
|
103
|
+
type(None).__name__: lambda x: None,
|
104
|
+
|
105
|
+
"list": json.loads,
|
106
|
+
"dict": json.loads,
|
107
|
+
"tuple": _decode_tuple,
|
108
|
+
type(set()).__name__: _decode_set,
|
109
|
+
|
110
|
+
datetime.__name__: datetime.fromisoformat,
|
111
|
+
date.__name__: date.fromisoformat,
|
112
|
+
time.__name__: time.fromisoformat,
|
113
|
+
timedelta.__name__: lambda x: timedelta(seconds=float(x)),
|
114
|
+
|
115
|
+
Decimal.__name__: Decimal,
|
116
|
+
complex.__name__: lambda x: complex(*map(float, x.split(','))),
|
117
|
+
bytes.__name__: base64.b64decode,
|
118
|
+
|
119
|
+
UUID.__name__: UUID,
|
120
|
+
OrderedDict.__name__: lambda x: OrderedDict(json.loads(x)),
|
121
|
+
defaultdict.__name__: lambda x: defaultdict(type(None), json.loads(x)),
|
122
|
+
frozenset.__name__: lambda x: frozenset(json.loads(x)),
|
123
|
+
}
|
124
|
+
|
125
|
+
|
126
|
+
encoding_registry: EncodeType = {
|
127
|
+
"list": json.dumps,
|
128
|
+
"dict": json.dumps,
|
129
|
+
"tuple": _encode_tuple,
|
130
|
+
type(set()).__name__: _encode_set,
|
131
|
+
|
132
|
+
datetime.__name__: datetime.isoformat,
|
133
|
+
date.__name__: date.isoformat,
|
134
|
+
time.__name__: time.isoformat,
|
135
|
+
timedelta.__name__: lambda x: str(x.total_seconds()),
|
136
|
+
|
137
|
+
complex.__name__: lambda x: f"{x.real},{x.imag}",
|
138
|
+
bytes.__name__: lambda x: base64.b64encode(x).decode('ascii'),
|
139
|
+
OrderedDict.__name__: lambda x: json.dumps(list(x.items())),
|
140
|
+
defaultdict.__name__: lambda x: json.dumps(dict(x)),
|
141
|
+
frozenset.__name__: lambda x: json.dumps(list(x)),
|
142
|
+
}
|
143
|
+
|
144
|
+
|
145
|
+
class RedisDictJSONEncoder(json.JSONEncoder):
|
146
|
+
"""Extends JSON encoding capabilities by reusing RedisDict type conversion.
|
147
|
+
|
148
|
+
Uses existing decoding_registry to know which types to handle specially and
|
149
|
+
encoding_registry (falls back to str) for converting to JSON-compatible formats.
|
150
|
+
|
151
|
+
Example:
|
152
|
+
The encoded format looks like::
|
153
|
+
|
154
|
+
{
|
155
|
+
"__type__": "TypeName",
|
156
|
+
"value": <encoded value>
|
157
|
+
}
|
158
|
+
|
159
|
+
Notes:
|
160
|
+
|
161
|
+
Uses decoding_registry (containing all supported types) to check if type
|
162
|
+
needs special handling. For encoding, defaults to str() if no encoder exists
|
163
|
+
in encoding_registry.
|
164
|
+
"""
|
165
|
+
def default(self, o: Any) -> Any:
|
166
|
+
"""Overwrite default from json encoder.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
o (Any): Object to be serialized.
|
170
|
+
|
171
|
+
Raises:
|
172
|
+
TypeError: If the object `o` cannot be serialized.
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Any: Serialized value.
|
176
|
+
"""
|
177
|
+
type_name = type(o).__name__
|
178
|
+
if type_name in decoding_registry:
|
179
|
+
return {
|
180
|
+
"__type__": type_name,
|
181
|
+
"value": encoding_registry.get(type_name, _default_encoder)(o)
|
182
|
+
}
|
183
|
+
try:
|
184
|
+
return json.JSONEncoder.default(self, o)
|
185
|
+
except TypeError as e:
|
186
|
+
raise TypeError(f"Object of type {type_name} is not JSON serializable") from e
|
187
|
+
|
188
|
+
|
189
|
+
class RedisDictJSONDecoder(json.JSONDecoder):
|
190
|
+
"""JSON decoder leveraging RedisDict existing type conversion system.
|
191
|
+
|
192
|
+
Works with RedisDictJSONEncoder to reconstruct Python objects from JSON using
|
193
|
+
RedisDict decoding_registry.
|
194
|
+
|
195
|
+
Still needs work but allows for more types than without.
|
196
|
+
"""
|
197
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
198
|
+
"""
|
199
|
+
Overwrite the __init__ method from JSON decoder.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
*args (Any): Positional arguments for initialization.
|
203
|
+
**kwargs (Any): Keyword arguments for initialization.
|
204
|
+
|
205
|
+
"""
|
206
|
+
def _object_hook(obj: Dict[Any, Any]) -> Any:
|
207
|
+
if "__type__" in obj and "value" in obj:
|
208
|
+
type_name = obj["__type__"]
|
209
|
+
if type_name in decoding_registry:
|
210
|
+
return decoding_registry[type_name](obj["value"])
|
211
|
+
return obj
|
212
|
+
|
213
|
+
super().__init__(object_hook=_object_hook, *args, **kwargs)
|
214
|
+
|
215
|
+
|
216
|
+
def encode_json(obj: Any) -> str:
|
217
|
+
"""
|
218
|
+
Encode a Python object to a JSON string using the existing encoding registry.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
obj (Any): The Python object to be encoded.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
str: The JSON-encoded string representation of the object.
|
225
|
+
"""
|
226
|
+
return json.dumps(obj, cls=RedisDictJSONEncoder)
|
227
|
+
|
228
|
+
|
229
|
+
def decode_json(s: str) -> Any:
|
230
|
+
"""
|
231
|
+
Decode a JSON string to a Python object using the existing decoding registry.
|
232
|
+
|
233
|
+
Args:
|
234
|
+
s (str): The JSON string to be decoded.
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
Any: The decoded Python object.
|
238
|
+
"""
|
239
|
+
return json.loads(s, cls=RedisDictJSONDecoder)
|
240
|
+
|
241
|
+
|
242
|
+
def _default_decoder(x: str) -> str:
|
243
|
+
"""
|
244
|
+
Pass-through decoder that returns the input string unchanged.
|
245
|
+
|
246
|
+
Args:
|
247
|
+
x (str): The input string.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
str: The same input string.
|
251
|
+
"""
|
252
|
+
return x
|
253
|
+
|
254
|
+
|
255
|
+
def _default_encoder(x: Any) -> str:
|
256
|
+
"""
|
257
|
+
Takes x and returns the result str of the object.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
x (Any): The input object
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
str: output of str of the object
|
264
|
+
"""
|
265
|
+
return str(x)
|
266
|
+
|
267
|
+
|
268
|
+
encoding_registry["dict"] = encode_json
|
269
|
+
decoding_registry["dict"] = decode_json
|
270
|
+
|
271
|
+
|
272
|
+
encoding_registry["list"] = encode_json
|
273
|
+
decoding_registry["list"] = decode_json
|
@@ -1,13 +1,14 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: redis-dict
|
3
|
-
Version:
|
3
|
+
Version: 3.0.0
|
4
4
|
Summary: Dictionary with Redis as storage backend
|
5
|
-
|
6
|
-
Author: Melvin Bijman
|
7
|
-
Author-email: bijman.m.m@gmail.com
|
5
|
+
Author-email: Melvin Bijman <bijman.m.m@gmail.com>
|
8
6
|
License: MIT
|
9
|
-
|
10
|
-
|
7
|
+
Project-URL: Homepage, https://github.com/Attumm/redisdict
|
8
|
+
Project-URL: Documentation, https://github.com/Attumm/redisdict#readme
|
9
|
+
Project-URL: Repository, https://github.com/Attumm/redisdict.git
|
10
|
+
Project-URL: Changelog, https://github.com/Attumm/redisdict/releases
|
11
|
+
Keywords: redis,python,dictionary,dict,key-value,database,caching,distributed-computing,dictionary-interface,large-datasets,scientific-computing,data-persistence,high-performance,scalable,pipelining,batching,big-data,data-types,distributed-algorithms,encryption,data-management
|
11
12
|
Classifier: Development Status :: 5 - Production/Stable
|
12
13
|
Classifier: Intended Audience :: Developers
|
13
14
|
Classifier: Intended Audience :: Information Technology
|
@@ -21,18 +22,51 @@ Classifier: Topic :: Software Development :: Object Brokering
|
|
21
22
|
Classifier: Topic :: Database :: Database Engines/Servers
|
22
23
|
Classifier: License :: OSI Approved :: MIT License
|
23
24
|
Classifier: Programming Language :: Python :: 3
|
24
|
-
Classifier: Programming Language :: Python :: 3.6
|
25
|
-
Classifier: Programming Language :: Python :: 3.7
|
26
25
|
Classifier: Programming Language :: Python :: 3.8
|
27
26
|
Classifier: Programming Language :: Python :: 3.9
|
28
27
|
Classifier: Programming Language :: Python :: 3.10
|
29
28
|
Classifier: Programming Language :: Python :: 3.11
|
30
29
|
Classifier: Programming Language :: Python :: 3.12
|
30
|
+
Classifier: Typing :: Typed
|
31
|
+
Requires-Python: >=3.8
|
31
32
|
Description-Content-Type: text/markdown
|
32
33
|
License-File: LICENSE
|
33
|
-
Requires-Dist: redis
|
34
|
+
Requires-Dist: redis>=4.0.0
|
35
|
+
Provides-Extra: dev
|
36
|
+
Requires-Dist: coverage==5.5; extra == "dev"
|
37
|
+
Requires-Dist: hypothesis==6.70.1; extra == "dev"
|
38
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
39
|
+
Requires-Dist: mypy-extensions>=1.0.0; extra == "dev"
|
40
|
+
Requires-Dist: types-pyOpenSSL>=24.0.0.0; extra == "dev"
|
41
|
+
Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
42
|
+
Requires-Dist: typing-extensions>=4.5.0; extra == "dev"
|
43
|
+
Requires-Dist: pylama>=8.4.1; extra == "dev"
|
44
|
+
Requires-Dist: pycodestyle==2.10.0; extra == "dev"
|
45
|
+
Requires-Dist: pydocstyle==6.3.0; extra == "dev"
|
46
|
+
Requires-Dist: pyflakes==3.0.1; extra == "dev"
|
47
|
+
Requires-Dist: pylint==3.2.7; extra == "dev"
|
48
|
+
Requires-Dist: mccabe==0.7.0; extra == "dev"
|
49
|
+
Requires-Dist: attrs==22.2.0; extra == "dev"
|
50
|
+
Requires-Dist: cffi==1.15.1; extra == "dev"
|
51
|
+
Requires-Dist: cryptography==43.0.1; extra == "dev"
|
52
|
+
Requires-Dist: exceptiongroup==1.1.1; extra == "dev"
|
53
|
+
Requires-Dist: future==0.18.3; extra == "dev"
|
54
|
+
Requires-Dist: pycparser==2.21; extra == "dev"
|
55
|
+
Requires-Dist: snowballstemmer==2.2.0; extra == "dev"
|
56
|
+
Requires-Dist: sortedcontainers==2.4.0; extra == "dev"
|
57
|
+
Requires-Dist: tomli==2.0.1; extra == "dev"
|
58
|
+
Requires-Dist: setuptools>=68.0.0; extra == "dev"
|
59
|
+
Requires-Dist: darglint; extra == "dev"
|
60
|
+
Requires-Dist: pydocstyle; extra == "dev"
|
61
|
+
Provides-Extra: docs
|
62
|
+
Requires-Dist: sphinx; extra == "docs"
|
63
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
64
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
|
65
|
+
Requires-Dist: tomli; extra == "docs"
|
66
|
+
Requires-Dist: myst-parser; extra == "docs"
|
34
67
|
|
35
68
|
# Redis-dict
|
69
|
+
[](https://pypi.org/project/redis-dict/)
|
36
70
|
[](https://github.com/Attumm/redis-dict/actions/workflows/ci.yml)
|
37
71
|
[](https://codecov.io/gh/Attumm/redis-dict)
|
38
72
|
[](https://pepy.tech/project/redis-dict)
|
@@ -86,7 +120,6 @@ In Redis our example looks like this.
|
|
86
120
|
|
87
121
|
### Namespaces
|
88
122
|
Acting as an identifier for your dictionary across different systems, RedisDict employs namespaces for organized data management. When a namespace isn't specified, "main" becomes the default. Thus allowing for data organization across systems and projects with the same redis instance.
|
89
|
-
|
90
123
|
This approach also minimizes the risk of key collisions between different applications, preventing hard-to-debug issues. By leveraging namespaces, RedisDict ensures a cleaner and more maintainable data management experience for developers working on multiple projects.
|
91
124
|
|
92
125
|
## Advanced Features
|
@@ -135,7 +168,6 @@ dic['gone'] = 'gone in 5 seconds'
|
|
135
168
|
Efficiently batch your requests using the Pipeline feature, which can be easily utilized with a context manager.
|
136
169
|
|
137
170
|
```python
|
138
|
-
from redis_dict import RedisDict
|
139
171
|
dic = RedisDict(namespace="example")
|
140
172
|
|
141
173
|
# one round trip to redis
|
@@ -263,14 +295,11 @@ This approach optimizes Redis database performance and efficiency by ensuring th
|
|
263
295
|
Following types are supported:
|
264
296
|
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
265
297
|
```python
|
266
|
-
from redis_dict import RedisDict
|
267
|
-
|
268
298
|
from uuid import UUID
|
269
299
|
from decimal import Decimal
|
270
300
|
from collections import OrderedDict, defaultdict
|
271
301
|
from datetime import datetime, date, time, timedelta
|
272
302
|
|
273
|
-
|
274
303
|
dic = RedisDict()
|
275
304
|
|
276
305
|
dic["string"] = "Hello World"
|
@@ -299,6 +328,32 @@ dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
|
299
328
|
dic["frozen"] = frozenset([1, 2, 3])
|
300
329
|
```
|
301
330
|
|
331
|
+
|
332
|
+
|
333
|
+
### Nested types
|
334
|
+
Nested Types
|
335
|
+
RedisDict supports nested structures with mixed types through JSON serialization. The feature works by utilizing JSON encoding and decoding under the hood. While this represents an upgrade in functionality, the feature is not fully implemented and should be used with caution. For optimal performance, using shallow dictionaries is recommended.
|
336
|
+
```python
|
337
|
+
from datetime import datetime, timedelta
|
338
|
+
|
339
|
+
dic["mixed"] = [1, "foobar", 3.14, [1, 2, 3], datetime.now()]
|
340
|
+
|
341
|
+
dic['dic'] = {"elapsed_time": timedelta(hours=60)}
|
342
|
+
```
|
343
|
+
|
344
|
+
### JSON Encoding - Decoding
|
345
|
+
The nested type support in RedisDict is implemented using custom JSON encoders and decoders. These JSON encoders and decoders are built on top of RedisDict's own encoding and decoding functionality, extending it for JSON compatibility. Since JSON serialization was a frequently requested feature, these enhanced encoders and decoders are available for use in other projects:
|
346
|
+
```python
|
347
|
+
import json
|
348
|
+
from datetime import datetime
|
349
|
+
from redis_dict import RedisDictJSONDecoder, RedisDictJSONEncoder
|
350
|
+
|
351
|
+
data = [1, "foobar", 3.14, [1, 2, 3], datetime.now()]
|
352
|
+
encoded = json.dumps(data, cls=RedisDictJSONEncoder)
|
353
|
+
result = json.loads(encoded, cls=RedisDictJSONDecoder)
|
354
|
+
```
|
355
|
+
|
356
|
+
|
302
357
|
### Extending RedisDict with Custom Types
|
303
358
|
|
304
359
|
RedisDict supports custom type serialization. Here's how to add a new type:
|
@@ -306,7 +361,6 @@ RedisDict supports custom type serialization. Here's how to add a new type:
|
|
306
361
|
|
307
362
|
```python
|
308
363
|
import json
|
309
|
-
from redis_dict import RedisDict
|
310
364
|
|
311
365
|
class Person:
|
312
366
|
def __init__(self, name, age):
|
@@ -335,23 +389,13 @@ assert result.name == person.name
|
|
335
389
|
assert result.age == person.age
|
336
390
|
```
|
337
391
|
|
338
|
-
|
339
|
-
>>> from datetime import datetime
|
340
|
-
>>> redis_dict.extends_type(datetime, datetime.isoformat, datetime.fromisoformat)
|
341
|
-
>>> redis_dict["now"] = datetime.now()
|
342
|
-
>>> redis_dict
|
343
|
-
{'now': datetime.datetime(2024, 10, 14, 18, 41, 53, 493775)}
|
344
|
-
>>> redis_dict["now"]
|
345
|
-
datetime.datetime(2024, 10, 14, 18, 41, 53, 493775)
|
346
|
-
```
|
347
|
-
|
348
|
-
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/extend_types_tests.py).
|
392
|
+
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/extend_types_tests.py).
|
349
393
|
### Redis Encryption
|
350
394
|
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
|
351
|
-
[Setup guide](https://github.com/Attumm/redis-dict/blob/main/encrypted_redis.MD)
|
395
|
+
[Setup guide](https://github.com/Attumm/redis-dict/blob/main/docs/tutorials/encrypted_redis.MD)
|
352
396
|
|
353
397
|
### Redis Storage Encryption
|
354
|
-
For storing encrypted data values, it's possible to use extended types. Take a look at this [encrypted test](https://github.com/Attumm/redis-dict/blob/main/encrypt_tests.py).
|
398
|
+
For storing encrypted data values, it's possible to use extended types. Take a look at this [encrypted test](https://github.com/Attumm/redis-dict/blob/main/tests/unit/encrypt_tests.py).
|
355
399
|
|
356
400
|
### Tests
|
357
401
|
The RedisDict library includes a comprehensive suite of tests that ensure its correctness and resilience. The test suite covers various data types, edge cases, and error handling scenarios. It also employs the Hypothesis library for property-based testing, which provides fuzz testing to evaluate the implementation
|
@@ -359,19 +403,16 @@ The RedisDict library includes a comprehensive suite of tests that ensure its co
|
|
359
403
|
### Redis config
|
360
404
|
To configure RedisDict using your Redis config.
|
361
405
|
|
362
|
-
Configure both the host and port.
|
406
|
+
Configure both the host and port. Or configuration with a setting dictionary.
|
363
407
|
```python
|
364
408
|
dic = RedisDict(host='127.0.0.1', port=6380)
|
365
|
-
```
|
366
409
|
|
367
|
-
Configuration with a dictionary.
|
368
|
-
```python
|
369
410
|
redis_config = {
|
370
411
|
'host': '127.0.0.1',
|
371
412
|
'port': 6380,
|
372
413
|
}
|
373
414
|
|
374
|
-
|
415
|
+
confid_dic = RedisDict(**redis_config)
|
375
416
|
```
|
376
417
|
|
377
418
|
## Installation
|
@@ -382,4 +423,3 @@ pip install redis-dict
|
|
382
423
|
### Note
|
383
424
|
* Please be aware that this project is currently being utilized by various organizations in their production environments. If you have any questions or concerns, feel free to raise issues
|
384
425
|
* This project only uses redis as dependency
|
385
|
-
|
@@ -0,0 +1,9 @@
|
|
1
|
+
redis_dict/__init__.py,sha256=fksonUr5DetzwFDEkT7lpmAaV3Jhmp2IQ12t62LwFb4,476
|
2
|
+
redis_dict/core.py,sha256=iLVTzpR4HmMPqcgZQWMgdAgwRepLEhTbdxP-tfA13ts,34698
|
3
|
+
redis_dict/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
redis_dict/type_management.py,sha256=U3aP_EtHByApRdHvpr-zSOjok6r9BVZ0g3YnpVCVt8Y,7690
|
5
|
+
redis_dict-3.0.0.dist-info/LICENSE,sha256=-QiLwYznh_vNUSz337k0faP9Jl0dgtCIHVZ39Uyl6cA,1070
|
6
|
+
redis_dict-3.0.0.dist-info/METADATA,sha256=8Zn6a75THLjxiCGfRdFuz675RwSsLrBf0JQE8fH-Kfo,16873
|
7
|
+
redis_dict-3.0.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
8
|
+
redis_dict-3.0.0.dist-info/top_level.txt,sha256=Wyp5Xvq_imoxvu-c-Le1rbTZ3pYM5BF440H9YAcgBZ8,11
|
9
|
+
redis_dict-3.0.0.dist-info/RECORD,,
|
@@ -1,6 +0,0 @@
|
|
1
|
-
redis_dict.py,sha256=LmP5C1lIf3KCG_3pfcnw3CZE5KuUUym2U_UqlbFoiVg,38214
|
2
|
-
redis_dict-2.7.0.dist-info/LICENSE,sha256=-QiLwYznh_vNUSz337k0faP9Jl0dgtCIHVZ39Uyl6cA,1070
|
3
|
-
redis_dict-2.7.0.dist-info/METADATA,sha256=8SDlje1sUiquUlKvk27LHRhLTsdwr9b5KTsKBa5XhGk,14300
|
4
|
-
redis_dict-2.7.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
5
|
-
redis_dict-2.7.0.dist-info/top_level.txt,sha256=Wyp5Xvq_imoxvu-c-Le1rbTZ3pYM5BF440H9YAcgBZ8,11
|
6
|
-
redis_dict-2.7.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|