redis-dict 2.5.1__py3-none-any.whl → 2.7.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {redis_dict-2.5.1.dist-info → redis_dict-2.7.0.dist-info}/METADATA +135 -21
- redis_dict-2.7.0.dist-info/RECORD +6 -0
- {redis_dict-2.5.1.dist-info → redis_dict-2.7.0.dist-info}/WHEEL +1 -1
- redis_dict.py +317 -55
- redis_dict-2.5.1.dist-info/RECORD +0 -6
- {redis_dict-2.5.1.dist-info → redis_dict-2.7.0.dist-info}/LICENSE +0 -0
- {redis_dict-2.5.1.dist-info → redis_dict-2.7.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,24 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: redis-dict
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.0
|
4
4
|
Summary: Dictionary with Redis as storage backend
|
5
5
|
Home-page: https://github.com/Attumm/redisdict
|
6
6
|
Author: Melvin Bijman
|
7
7
|
Author-email: bijman.m.m@gmail.com
|
8
8
|
License: MIT
|
9
|
+
Keywords: redis python dictionary dict key-value 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
|
10
|
+
Platform: any
|
9
11
|
Classifier: Development Status :: 5 - Production/Stable
|
10
12
|
Classifier: Intended Audience :: Developers
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
15
|
+
Classifier: Topic :: Internet
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
11
17
|
Classifier: Topic :: Database
|
12
18
|
Classifier: Topic :: System :: Distributed Computing
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
|
+
Classifier: Topic :: Software Development :: Object Brokering
|
21
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
13
22
|
Classifier: License :: OSI Approved :: MIT License
|
14
23
|
Classifier: Programming Language :: Python :: 3
|
15
24
|
Classifier: Programming Language :: Python :: 3.6
|
@@ -28,34 +37,40 @@ Requires-Dist: redis
|
|
28
37
|
[](https://codecov.io/gh/Attumm/redis-dict)
|
29
38
|
[](https://pepy.tech/project/redis-dict)
|
30
39
|
|
31
|
-
RedisDict is a Python library that
|
32
|
-
|
33
|
-
By leveraging Redis for efficient key-value storage, RedisDict allows for high-performance data management and is particularly useful for handling large datasets that may exceed local memory capacity.
|
40
|
+
RedisDict is a Python library that offers a convenient and familiar interface for interacting with Redis, treating it as if it were a Python dictionary. Its goal is to help developers write clean, Pythonic code while using Redis as a storage solution for seamless distributed computing. This simple yet powerful library utilizes Redis as a key-value store and supports various data types, including strings, integers, floats, booleans, lists, and dictionaries. Additionally, developers can extend RedisDict to work with custom objects.
|
34
41
|
|
42
|
+
The library includes utility functions for more complex use cases such as caching, batching, and more. By leveraging Redis for efficient key-value storage, RedisDict enables high-performance data management, maintaining efficiency even with large datasets and Redis instances.
|
35
43
|
|
36
44
|
## Features
|
37
45
|
|
38
46
|
* Dictionary-like interface: Use familiar Python dictionary syntax to interact with Redis.
|
39
|
-
* Data Type Support: Comprehensive support for various data types
|
47
|
+
* Data Type Support: Comprehensive support for various data types.
|
40
48
|
* Pipelining support: Use pipelines for batch operations to improve performance.
|
41
49
|
* Expiration Support: Enables the setting of expiration times either globally or individually per key, through the use of context managers.
|
42
50
|
* Efficiency and Scalability: RedisDict is designed for use with large datasets and is optimized for efficiency. It retrieves only the data needed for a particular operation, ensuring efficient memory usage and fast performance.
|
43
51
|
* Namespace Management: Provides simple and efficient namespace handling to help organize and manage data in Redis, streamlining data access and manipulation.
|
44
52
|
* Distributed Computing: With its ability to seamlessly connect to other instances or servers with access to the same Redis instance, RedisDict enables easy distributed computing.
|
45
|
-
* Custom data types: Add custom types
|
53
|
+
* Custom data: types: Add custom types encoding/decoding to store your data types.
|
54
|
+
* Encryption: allows for storing data encrypted, while retaining the simple dictionary interface.
|
46
55
|
|
47
56
|
## Example
|
48
57
|
Redis is an exceptionally fast database when used appropriately. RedisDict leverages Redis for efficient key-value storage, enabling high-performance data management.
|
49
58
|
|
50
|
-
```
|
51
|
-
|
59
|
+
```bash
|
60
|
+
pip install redis-dict
|
61
|
+
```
|
52
62
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
dic[
|
58
|
-
|
63
|
+
```python
|
64
|
+
>>> from redis_dict import RedisDict
|
65
|
+
>>> dic = RedisDict()
|
66
|
+
>>> dic['foo'] = 42
|
67
|
+
>>> dic['foo']
|
68
|
+
42
|
69
|
+
>>> 'foo' in dic
|
70
|
+
True
|
71
|
+
>>> dic["baz"] = "hello world"
|
72
|
+
>>> dic
|
73
|
+
{'foo': 42, 'baz': 'hello world'}
|
59
74
|
```
|
60
75
|
In Redis our example looks like this.
|
61
76
|
```
|
@@ -68,12 +83,12 @@ In Redis our example looks like this.
|
|
68
83
|
"str:hello world"
|
69
84
|
```
|
70
85
|
|
86
|
+
|
71
87
|
### Namespaces
|
72
|
-
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
|
88
|
+
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.
|
73
89
|
|
74
90
|
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.
|
75
91
|
|
76
|
-
|
77
92
|
## Advanced Features
|
78
93
|
|
79
94
|
### Expiration
|
@@ -106,6 +121,8 @@ with dic.expire_at(seconds):
|
|
106
121
|
3. Updating keys while preserving the initial timeout In certain situations, there is a need to update the value while keeping the expiration intact. This is achievable by setting the 'preserve_expiration' to true.
|
107
122
|
|
108
123
|
```python
|
124
|
+
import time
|
125
|
+
|
109
126
|
dic = RedisDict(expire=10, preserve_expiration=True)
|
110
127
|
dic['gone'] = 'in ten seconds'
|
111
128
|
|
@@ -118,6 +135,7 @@ dic['gone'] = 'gone in 5 seconds'
|
|
118
135
|
Efficiently batch your requests using the Pipeline feature, which can be easily utilized with a context manager.
|
119
136
|
|
120
137
|
```python
|
138
|
+
from redis_dict import RedisDict
|
121
139
|
dic = RedisDict(namespace="example")
|
122
140
|
|
123
141
|
# one round trip to redis
|
@@ -147,13 +165,14 @@ print(dic["foo"]) # outputs "bar"
|
|
147
165
|
### Caching made simple
|
148
166
|
```python
|
149
167
|
import time
|
168
|
+
from datetime import timedelta
|
150
169
|
from redis_dict import RedisDict
|
151
170
|
|
152
171
|
def expensive_function(x):
|
153
|
-
time.sleep(
|
172
|
+
time.sleep(x)
|
154
173
|
return x * 2
|
155
174
|
|
156
|
-
cache = RedisDict(namespace="cache", expire=
|
175
|
+
cache = RedisDict(namespace="cache", expire=timedelta(minutes=60))
|
157
176
|
|
158
177
|
def cached_expensive_function(x):
|
159
178
|
if x not in cache:
|
@@ -161,7 +180,7 @@ def cached_expensive_function(x):
|
|
161
180
|
return cache[x]
|
162
181
|
|
163
182
|
start_time = time.time()
|
164
|
-
print(cached_expensive_function(5)) # Takes around
|
183
|
+
print(cached_expensive_function(5)) # Takes around 5 seconds to compute and caches the result.
|
165
184
|
print(f"Time taken: {time.time() - start_time:.2f} seconds")
|
166
185
|
|
167
186
|
start_time = time.time()
|
@@ -181,7 +200,7 @@ dic["name"] = "John Doe"
|
|
181
200
|
dic["age"] = 32
|
182
201
|
dic["city"] = "Amsterdam"
|
183
202
|
|
184
|
-
# Get value by key
|
203
|
+
# Get value by key, from any instance connected to the same redis/namespace
|
185
204
|
print(dic["name"]) # Output: John Doe
|
186
205
|
|
187
206
|
# Update value by key, got a year older
|
@@ -234,10 +253,105 @@ print(dic["d"]) # Output: 4
|
|
234
253
|
For more advanced examples of RedisDict, please refer to the unit-test files in the repository. All features and functionalities are thoroughly tested in [unit tests (here)](https://github.com/Attumm/redis-dict/blob/main/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/load_test.py#L1).
|
235
254
|
The unit-tests can be as used as a starting point.
|
236
255
|
|
256
|
+
## Types
|
257
|
+
|
258
|
+
### standard types
|
259
|
+
RedisDict supports a range of Python data types, from basic types to nested structures.
|
260
|
+
Basic types are handled natively, while complex data types like lists and dictionaries, RedisDict uses JSON serialization, specifically avoiding `pickle` due to its [security vulnerabilities](https://docs.python.org/3/library/pickle.html) in distributed computing contexts.
|
261
|
+
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
262
|
+
This approach optimizes Redis database performance and efficiency by ensuring that each set and get operation efficiently maps to Redis's key-value storage capabilities, while still preserving the library's Pythonic interface.
|
263
|
+
Following types are supported:
|
264
|
+
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
265
|
+
```python
|
266
|
+
from redis_dict import RedisDict
|
267
|
+
|
268
|
+
from uuid import UUID
|
269
|
+
from decimal import Decimal
|
270
|
+
from collections import OrderedDict, defaultdict
|
271
|
+
from datetime import datetime, date, time, timedelta
|
272
|
+
|
273
|
+
|
274
|
+
dic = RedisDict()
|
275
|
+
|
276
|
+
dic["string"] = "Hello World"
|
277
|
+
dic["number"] = 42
|
278
|
+
dic["float"] = 3.14
|
279
|
+
dic["bool"] = True
|
280
|
+
dic["None"] = None
|
281
|
+
|
282
|
+
dic["list"] = [1, 2, 3]
|
283
|
+
dic["dict"] = {"a": 1, "b": 2}
|
284
|
+
dic["tuple"] = (1, 2, 3)
|
285
|
+
dic["set"] = {1, 2, 3}
|
286
|
+
|
287
|
+
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
288
|
+
dic["date"] = date(2024, 1, 1)
|
289
|
+
dic["time"] = time(12, 30, 45)
|
290
|
+
dic["delta"] = timedelta(days=1, hours=2)
|
291
|
+
|
292
|
+
dic["decimal"] = Decimal("3.14159")
|
293
|
+
dic["complex"] = complex(1, 2)
|
294
|
+
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
295
|
+
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
296
|
+
|
297
|
+
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
298
|
+
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
299
|
+
dic["frozen"] = frozenset([1, 2, 3])
|
300
|
+
```
|
301
|
+
|
302
|
+
### Extending RedisDict with Custom Types
|
303
|
+
|
304
|
+
RedisDict supports custom type serialization. Here's how to add a new type:
|
305
|
+
|
306
|
+
|
307
|
+
```python
|
308
|
+
import json
|
309
|
+
from redis_dict import RedisDict
|
310
|
+
|
311
|
+
class Person:
|
312
|
+
def __init__(self, name, age):
|
313
|
+
self.name = name
|
314
|
+
self.age = age
|
315
|
+
|
316
|
+
def encode(self) -> str:
|
317
|
+
return json.dumps(self.__dict__)
|
318
|
+
|
319
|
+
@classmethod
|
320
|
+
def decode(cls, encoded_str: str) -> 'Person':
|
321
|
+
return cls(**json.loads(encoded_str))
|
322
|
+
|
323
|
+
redis_dict = RedisDict()
|
324
|
+
|
325
|
+
# Extend redis dict with the new type
|
326
|
+
redis_dict.extends_type(Person)
|
327
|
+
|
328
|
+
# RedisDict can now seamlessly handle Person instances.
|
329
|
+
person = Person(name="John", age=32)
|
330
|
+
redis_dict["person1"] = person
|
331
|
+
|
332
|
+
result = redis_dict["person1"]
|
333
|
+
|
334
|
+
assert result.name == person.name
|
335
|
+
assert result.age == person.age
|
336
|
+
```
|
337
|
+
|
338
|
+
```python
|
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).
|
237
349
|
### Redis Encryption
|
238
|
-
Setup guide for configuring and utilizing encrypted Redis for redis-dict.
|
350
|
+
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
|
239
351
|
[Setup guide](https://github.com/Attumm/redis-dict/blob/main/encrypted_redis.MD)
|
240
352
|
|
353
|
+
### 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).
|
241
355
|
|
242
356
|
### Tests
|
243
357
|
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
|
@@ -0,0 +1,6 @@
|
|
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,,
|
redis_dict.py
CHANGED
@@ -1,17 +1,106 @@
|
|
1
|
+
"""
|
2
|
+
redis_dict.py
|
3
|
+
|
4
|
+
RedisDict is a Python library that provides a convenient and familiar interface for
|
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
|
1
70
|
import json
|
2
|
-
from datetime import timedelta
|
3
|
-
from
|
4
|
-
from
|
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
|
5
76
|
|
77
|
+
from typing import Any, Callable, Dict, Iterator, Set, List, Tuple, Union, Optional
|
6
78
|
from contextlib import contextmanager
|
7
79
|
|
80
|
+
from redis import StrictRedis
|
81
|
+
|
8
82
|
SENTINEL = object()
|
9
83
|
|
10
|
-
|
11
|
-
|
84
|
+
EncodeFuncType = Callable[[Any], str]
|
85
|
+
DecodeFuncType = Callable[[str], Any]
|
12
86
|
|
87
|
+
EncodeType = Dict[str, EncodeFuncType]
|
88
|
+
DecodeType = Dict[str, DecodeFuncType]
|
13
89
|
|
14
|
-
|
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, ...]:
|
15
104
|
"""
|
16
105
|
Deserialize a JSON-formatted string to a tuple.
|
17
106
|
|
@@ -27,7 +116,7 @@ def _transform_tuple(val: str) -> Tuple[Any, ...]:
|
|
27
116
|
return tuple(json.loads(val))
|
28
117
|
|
29
118
|
|
30
|
-
def
|
119
|
+
def _encode_tuple(val: Tuple[Any, ...]) -> str:
|
31
120
|
"""
|
32
121
|
Serialize a tuple to a JSON-formatted string.
|
33
122
|
|
@@ -43,7 +132,7 @@ def _pre_transform_tuple(val: Tuple[Any, ...]) -> str:
|
|
43
132
|
return json.dumps(list(val))
|
44
133
|
|
45
134
|
|
46
|
-
def
|
135
|
+
def _decode_set(val: str) -> Set[Any]:
|
47
136
|
"""
|
48
137
|
Deserialize a JSON-formatted string to a set.
|
49
138
|
|
@@ -59,7 +148,7 @@ def _transform_set(val: str) -> Set[Any]:
|
|
59
148
|
return set(json.loads(val))
|
60
149
|
|
61
150
|
|
62
|
-
def
|
151
|
+
def _encode_set(val: Set[Any]) -> str:
|
63
152
|
"""
|
64
153
|
Serialize a set to a JSON-formatted string.
|
65
154
|
|
@@ -75,6 +164,7 @@ def _pre_transform_set(val: Set[Any]) -> str:
|
|
75
164
|
return json.dumps(list(val))
|
76
165
|
|
77
166
|
|
167
|
+
# pylint: disable=R0902, R0904
|
78
168
|
class RedisDict:
|
79
169
|
"""
|
80
170
|
A Redis-backed dictionary-like data structure with support for advanced features, such as
|
@@ -92,15 +182,19 @@ class RedisDict:
|
|
92
182
|
It aims to offer a seamless and familiar interface for developers familiar with Python dictionaries,
|
93
183
|
enabling a smooth transition to a Redis-backed data store.
|
94
184
|
|
185
|
+
Extendable Types: You can extend RedisDict by adding or overriding encoding and decoding functions.
|
186
|
+
This functionality enables various use cases, such as managing encrypted data in Redis,
|
187
|
+
To implement this, simply create and register your custom encoding and decoding functions.
|
188
|
+
By delegating serialization to redis-dict, reduce complexity and have simple code in the codebase.
|
189
|
+
|
95
190
|
Attributes:
|
96
|
-
|
97
|
-
|
191
|
+
decoding_registry (Dict[str, DecodeFuncType]): Mapping of decoding transformation functions based on type
|
192
|
+
encoding_registry (Dict[str, EncodeFuncType]): Mapping of encoding transformation functions based on type
|
98
193
|
namespace (str): A string used as a prefix for Redis keys to separate data in different namespaces.
|
99
194
|
expire (Union[int, None]): An optional expiration time for keys, in seconds.
|
100
195
|
|
101
196
|
"""
|
102
|
-
|
103
|
-
transform: transform_type = {
|
197
|
+
decoding_registry: DecodeType = {
|
104
198
|
type('').__name__: str,
|
105
199
|
type(1).__name__: int,
|
106
200
|
type(0.1).__name__: float,
|
@@ -109,15 +203,40 @@ class RedisDict:
|
|
109
203
|
|
110
204
|
"list": json.loads,
|
111
205
|
"dict": json.loads,
|
112
|
-
"tuple":
|
113
|
-
type(set()).__name__:
|
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)),
|
114
222
|
}
|
115
223
|
|
116
|
-
|
224
|
+
encoding_registry: EncodeType = {
|
117
225
|
"list": json.dumps,
|
118
226
|
"dict": json.dumps,
|
119
|
-
"tuple":
|
120
|
-
type(set()).__name__:
|
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)),
|
121
240
|
}
|
122
241
|
|
123
242
|
def __init__(self,
|
@@ -134,13 +253,20 @@ class RedisDict:
|
|
134
253
|
preserve_expiration (bool, optional): Preserve the expiration count when the key is updated.
|
135
254
|
**redis_kwargs: Additional keyword arguments passed to StrictRedis.
|
136
255
|
"""
|
137
|
-
|
256
|
+
|
138
257
|
self.namespace: str = namespace
|
139
258
|
self.expire: Union[int, timedelta, None] = expire
|
140
259
|
self.preserve_expiration: Optional[bool] = preserve_expiration
|
141
260
|
self.redis: StrictRedis[Any] = StrictRedis(decode_responses=True, **redis_kwargs)
|
142
261
|
self.get_redis: StrictRedis[Any] = self.redis
|
143
262
|
|
263
|
+
self.custom_encode_method = "encode"
|
264
|
+
self.custom_decode_method = "decode"
|
265
|
+
|
266
|
+
self._iter: Iterator[str] = iter([])
|
267
|
+
self._max_string_size: int = 500 * 1024 * 1024 # 500mb
|
268
|
+
self._temp_redis: Optional[StrictRedis[Any]] = None
|
269
|
+
|
144
270
|
def _format_key(self, key: str) -> str:
|
145
271
|
"""
|
146
272
|
Format a key with the namespace prefix.
|
@@ -151,7 +277,7 @@ class RedisDict:
|
|
151
277
|
Returns:
|
152
278
|
str: The formatted key with the namespace prefix.
|
153
279
|
"""
|
154
|
-
return '{}:{
|
280
|
+
return f'{self.namespace}:{str(key)}'
|
155
281
|
|
156
282
|
def _valid_input(self, val: Any, val_type: str) -> bool:
|
157
283
|
"""
|
@@ -169,9 +295,17 @@ class RedisDict:
|
|
169
295
|
bool: True if the input value is valid, False otherwise.
|
170
296
|
"""
|
171
297
|
if val_type == "str":
|
172
|
-
return len(val) <
|
298
|
+
return len(val) < self._max_string_size
|
173
299
|
return True
|
174
300
|
|
301
|
+
def _format_value(self, key: str, value: Any) -> str:
|
302
|
+
store_type, key = type(value).__name__, str(key)
|
303
|
+
if not self._valid_input(value, store_type) or not self._valid_input(key, "str"):
|
304
|
+
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
305
|
+
encoded_value = self.encoding_registry.get(store_type, lambda x: x)(value) # type: ignore
|
306
|
+
|
307
|
+
return f'{store_type}:{encoded_value}'
|
308
|
+
|
175
309
|
def _store(self, key: str, value: Any) -> None:
|
176
310
|
"""
|
177
311
|
Store a value in Redis with the given key.
|
@@ -179,20 +313,21 @@ class RedisDict:
|
|
179
313
|
Args:
|
180
314
|
key (str): The key to store the value.
|
181
315
|
value (Any): The value to be stored.
|
182
|
-
"""
|
183
|
-
store_type, key = type(value).__name__, str(key)
|
184
|
-
if not self._valid_input(value, store_type) or not self._valid_input(key, "str"):
|
185
|
-
# TODO When needed, make valid_input, pass the reason, or throw a exception.
|
186
|
-
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
187
|
-
value = self.pre_transform.get(store_type, lambda x: x)(value) # type: ignore
|
188
316
|
|
189
|
-
|
190
|
-
|
317
|
+
Note: Validity checks could be refactored to allow for custom exceptions that inherit from ValueError,
|
318
|
+
providing detailed information about why a specific validation failed.
|
319
|
+
This would enable users to specify which validity checks should be executed, add custom validity functions,
|
320
|
+
and choose whether to fail on validation errors, or drop the data and only issue a warning and continue.
|
321
|
+
Example use case is caching, to cache data only when it's between min and max sizes.
|
322
|
+
Allowing for simple dict set operation, but only cache data that makes sense.
|
191
323
|
|
324
|
+
"""
|
325
|
+
formatted_key = self._format_key(key)
|
326
|
+
formatted_value = self._format_value(key, value)
|
192
327
|
if self.preserve_expiration and self.redis.exists(formatted_key):
|
193
|
-
self.redis.set(formatted_key,
|
328
|
+
self.redis.set(formatted_key, formatted_value, keepttl=True)
|
194
329
|
else:
|
195
|
-
self.redis.set(formatted_key,
|
330
|
+
self.redis.set(formatted_key, formatted_value, ex=self.expire)
|
196
331
|
|
197
332
|
def _load(self, key: str) -> Tuple[bool, Any]:
|
198
333
|
"""
|
@@ -207,8 +342,7 @@ class RedisDict:
|
|
207
342
|
result = self.get_redis.get(self._format_key(key))
|
208
343
|
if result is None:
|
209
344
|
return False, None
|
210
|
-
|
211
|
-
return True, self.transform.get(t, lambda x: x)(value)
|
345
|
+
return True, self._transform(result)
|
212
346
|
|
213
347
|
def _transform(self, result: str) -> Any:
|
214
348
|
"""
|
@@ -220,18 +354,118 @@ class RedisDict:
|
|
220
354
|
Returns:
|
221
355
|
Any: The transformed Python object.
|
222
356
|
"""
|
223
|
-
|
224
|
-
return self.
|
357
|
+
type_, value = result.split(':', 1)
|
358
|
+
return self.decoding_registry.get(type_, lambda x: x)(value)
|
225
359
|
|
226
|
-
def
|
360
|
+
def new_type_compliance(
|
361
|
+
self,
|
362
|
+
class_type: type,
|
363
|
+
encode_method_name: Optional[str] = None,
|
364
|
+
decode_method_name: Optional[str] = None,
|
365
|
+
) -> None:
|
227
366
|
"""
|
228
|
-
|
367
|
+
Checks if a class complies with the required encoding and decoding methods.
|
229
368
|
|
230
369
|
Args:
|
231
|
-
|
232
|
-
|
370
|
+
class_type (type): The class to check for compliance.
|
371
|
+
encode_method_name (str, optional): Name of encoding method of the class for redis-dict custom types.
|
372
|
+
decode_method_name (str, optional): Name of decoding method of the class for redis-dict custom types.
|
373
|
+
|
374
|
+
Raises:
|
375
|
+
NotImplementedError: If the class does not implement the required methods when the respective check is True.
|
376
|
+
"""
|
377
|
+
if encode_method_name is not None:
|
378
|
+
if not (hasattr(class_type, encode_method_name) and callable(
|
379
|
+
getattr(class_type, encode_method_name))):
|
380
|
+
raise NotImplementedError(
|
381
|
+
f"Class {class_type.__name__} does not implement the required {encode_method_name} method.")
|
382
|
+
|
383
|
+
if decode_method_name is not None:
|
384
|
+
if not (hasattr(class_type, decode_method_name) and callable(
|
385
|
+
getattr(class_type, decode_method_name))):
|
386
|
+
raise NotImplementedError(
|
387
|
+
f"Class {class_type.__name__} does not implement the required {decode_method_name} class method.")
|
388
|
+
|
389
|
+
def extends_type(
|
390
|
+
self,
|
391
|
+
class_type: type,
|
392
|
+
encode: Optional[EncodeFuncType] = None,
|
393
|
+
decode: Optional[DecodeFuncType] = None,
|
394
|
+
encoding_method_name: Optional[str] = None,
|
395
|
+
decoding_method_name: Optional[str] = None,
|
396
|
+
) -> None:
|
397
|
+
"""
|
398
|
+
Extends RedisDict to support a custom type in the encode/decode mapping.
|
399
|
+
|
400
|
+
This method enables serialization of instances based on their type,
|
401
|
+
allowing for custom types, specialized storage formats, and more.
|
402
|
+
There are three ways to add custom types:
|
403
|
+
1. Have a class with an `encode` instance method and a `decode` class method.
|
404
|
+
2. Have a class and pass encoding and decoding functions, where
|
405
|
+
`encode` converts the class instance to a string, and
|
406
|
+
`decode` takes the string and recreates the class instance.
|
407
|
+
3. Have a class that already has serialization methods, that satisfies the:
|
408
|
+
EncodeFuncType = Callable[[Any], str]
|
409
|
+
DecodeFuncType = Callable[[str], Any]
|
410
|
+
|
411
|
+
`custom_encode_method`
|
412
|
+
`custom_decode_method` attributes.
|
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.
|
422
|
+
|
423
|
+
If no encoding or decoding function is provided, default to use the `encode` and `decode` methods of the class.
|
424
|
+
|
425
|
+
The `encode` method should be an instance method that converts the object to a string.
|
426
|
+
The `decode` method should be a class method that takes a string and returns an instance of the class.
|
427
|
+
|
428
|
+
The method names for encoding and decoding can be changed by modifying the
|
429
|
+
- `custom_encode_method`
|
430
|
+
- `custom_decode_method`
|
431
|
+
attributes of the RedisDict instance
|
432
|
+
|
433
|
+
Example:
|
434
|
+
class Person:
|
435
|
+
def __init__(self, name, age):
|
436
|
+
self.name = name
|
437
|
+
self.age = age
|
438
|
+
|
439
|
+
def encode(self) -> str:
|
440
|
+
return json.dumps(self.__dict__)
|
441
|
+
|
442
|
+
@classmethod
|
443
|
+
def decode(cls, encoded_str: str) -> 'Person':
|
444
|
+
return cls(**json.loads(encoded_str))
|
445
|
+
|
446
|
+
redis_dict.extends_type(Person)
|
447
|
+
|
448
|
+
Note:
|
449
|
+
You can check for compliance of a class separately using the `new_type_compliance` method:
|
450
|
+
|
451
|
+
This method raises a NotImplementedError if either `encode` or `decode` is `None`
|
452
|
+
and the class does not implement the corresponding method.
|
233
453
|
"""
|
234
|
-
|
454
|
+
|
455
|
+
if encode is None or decode is None:
|
456
|
+
encode_method_name = encoding_method_name or self.custom_encode_method
|
457
|
+
if encode is None:
|
458
|
+
self.new_type_compliance(class_type, encode_method_name=encode_method_name)
|
459
|
+
encode = _create_default_encode(encode_method_name)
|
460
|
+
|
461
|
+
if decode is None:
|
462
|
+
decode_method_name = decoding_method_name or self.custom_decode_method
|
463
|
+
self.new_type_compliance(class_type, decode_method_name=decode_method_name)
|
464
|
+
decode = _create_default_decode(class_type, decode_method_name)
|
465
|
+
|
466
|
+
type_name = class_type.__name__
|
467
|
+
self.decoding_registry[type_name] = decode
|
468
|
+
self.encoding_registry[type_name] = encode
|
235
469
|
|
236
470
|
def __eq__(self, other: Any) -> bool:
|
237
471
|
"""
|
@@ -327,7 +561,7 @@ class RedisDict:
|
|
327
561
|
Returns:
|
328
562
|
Iterator[str]: An iterator over the keys of the RedisDict.
|
329
563
|
"""
|
330
|
-
self.
|
564
|
+
self._iter = self.iterkeys()
|
331
565
|
return self
|
332
566
|
|
333
567
|
def __repr__(self) -> str:
|
@@ -358,7 +592,7 @@ class RedisDict:
|
|
358
592
|
Raises:
|
359
593
|
StopIteration: If there are no more items.
|
360
594
|
"""
|
361
|
-
return next(self.
|
595
|
+
return next(self._iter)
|
362
596
|
|
363
597
|
def next(self) -> str:
|
364
598
|
"""
|
@@ -370,7 +604,29 @@ class RedisDict:
|
|
370
604
|
Raises:
|
371
605
|
StopIteration: If there are no more items.
|
372
606
|
"""
|
373
|
-
return self
|
607
|
+
return next(self)
|
608
|
+
|
609
|
+
def _create_iter_query(self, search_term: str) -> str:
|
610
|
+
"""
|
611
|
+
Create a Redis query string for iterating over keys based on the given search term.
|
612
|
+
|
613
|
+
This method constructs a query by prefixing the search term with the namespace
|
614
|
+
followed by a wildcard to facilitate scanning for keys that start with the
|
615
|
+
provided search term.
|
616
|
+
|
617
|
+
Args:
|
618
|
+
search_term (str): The term to search for in Redis keys.
|
619
|
+
|
620
|
+
Returns:
|
621
|
+
str: A formatted query string that can be used to find keys in Redis.
|
622
|
+
|
623
|
+
Example:
|
624
|
+
>>> d = RedisDict(namespace='foo')
|
625
|
+
>>> query = self._create_iter_query('bar')
|
626
|
+
>>> print(query)
|
627
|
+
'foo:bar*'
|
628
|
+
"""
|
629
|
+
return f'{self.namespace}:{search_term}*'
|
374
630
|
|
375
631
|
def _scan_keys(self, search_term: str = '') -> Iterator[str]:
|
376
632
|
"""
|
@@ -382,7 +638,8 @@ class RedisDict:
|
|
382
638
|
Returns:
|
383
639
|
Iterator[str]: An iterator of matching Redis keys.
|
384
640
|
"""
|
385
|
-
|
641
|
+
search_query = self._create_iter_query(search_term)
|
642
|
+
return self.get_redis.scan_iter(match=search_query)
|
386
643
|
|
387
644
|
def get(self, key: str, default: Optional[Any] = None) -> Any:
|
388
645
|
"""
|
@@ -413,7 +670,8 @@ class RedisDict:
|
|
413
670
|
Note: for python2 str is needed
|
414
671
|
"""
|
415
672
|
to_rm = len(self.namespace) + 1
|
416
|
-
|
673
|
+
search_query = self._create_iter_query(search_term)
|
674
|
+
_, data = self.get_redis.scan(match=search_query, count=1)
|
417
675
|
for item in data:
|
418
676
|
return str(item[to_rm:])
|
419
677
|
|
@@ -435,7 +693,7 @@ class RedisDict:
|
|
435
693
|
to_rm = len(self.namespace) + 1
|
436
694
|
for item in self._scan_keys():
|
437
695
|
try:
|
438
|
-
yield
|
696
|
+
yield str(item[to_rm:]), self[item[to_rm:]]
|
439
697
|
except KeyError:
|
440
698
|
pass
|
441
699
|
|
@@ -488,11 +746,12 @@ class RedisDict:
|
|
488
746
|
Redis pipelining is employed to group multiple commands into a single request, minimizing
|
489
747
|
network round-trip time, latency, and I/O load, thereby enhancing the overall performance.
|
490
748
|
|
491
|
-
It is important to highlight that the clear method can be safely executed within
|
749
|
+
It is important to highlight that the clear method can be safely executed within
|
750
|
+
the context of an initiated pipeline operation.
|
492
751
|
"""
|
493
752
|
with self.pipeline():
|
494
753
|
for key in self:
|
495
|
-
del
|
754
|
+
del self[key]
|
496
755
|
|
497
756
|
def pop(self, key: str, default: Union[Any, object] = SENTINEL) -> Any:
|
498
757
|
"""
|
@@ -516,7 +775,7 @@ class RedisDict:
|
|
516
775
|
return default
|
517
776
|
raise
|
518
777
|
|
519
|
-
del
|
778
|
+
del self[key]
|
520
779
|
return value
|
521
780
|
|
522
781
|
def popitem(self) -> Tuple[str, Any]:
|
@@ -594,7 +853,8 @@ class RedisDict:
|
|
594
853
|
value (Optional[Any], optional): The value to be assigned to each key in the RedisDict. Defaults to None.
|
595
854
|
|
596
855
|
Returns:
|
597
|
-
RedisDict: The current RedisDict instance,
|
856
|
+
RedisDict: The current RedisDict instance,populated with the keys from the iterable and their
|
857
|
+
corresponding values.
|
598
858
|
"""
|
599
859
|
for key in iterable:
|
600
860
|
self[key] = value
|
@@ -640,7 +900,7 @@ class RedisDict:
|
|
640
900
|
Args:
|
641
901
|
iterable (List[str]): A list of keys representing the chain.
|
642
902
|
"""
|
643
|
-
|
903
|
+
del self[':'.join(iterable)]
|
644
904
|
|
645
905
|
# def expire_at(self, sec_epoch: int | timedelta) -> Iterator[None]:
|
646
906
|
# compatibility with Python 3.9 typing
|
@@ -668,13 +928,13 @@ class RedisDict:
|
|
668
928
|
ContextManager: A context manager to create a Redis pipeline batching all operations within the context.
|
669
929
|
"""
|
670
930
|
top_level = False
|
671
|
-
if self.
|
672
|
-
self.redis, self.
|
931
|
+
if self._temp_redis is None:
|
932
|
+
self.redis, self._temp_redis, top_level = self.redis.pipeline(), self.redis, True
|
673
933
|
try:
|
674
934
|
yield
|
675
935
|
finally:
|
676
936
|
if top_level:
|
677
|
-
_, self.
|
937
|
+
_, self._temp_redis, self.redis = self.redis.execute(), None, self._temp_redis # type: ignore
|
678
938
|
|
679
939
|
def multi_get(self, key: str) -> List[Any]:
|
680
940
|
"""
|
@@ -717,7 +977,9 @@ class RedisDict:
|
|
717
977
|
if len(keys) == 0:
|
718
978
|
return {}
|
719
979
|
to_rm = keys[0].rfind(':') + 1
|
720
|
-
return dict(
|
980
|
+
return dict(
|
981
|
+
zip([i[to_rm:] for i in keys], (self._transform(i) for i in self.redis.mget(keys) if i is not None))
|
982
|
+
)
|
721
983
|
|
722
984
|
def multi_del(self, key: str) -> int:
|
723
985
|
"""
|
@@ -1,6 +0,0 @@
|
|
1
|
-
redis_dict.py,sha256=ZQSg6bCaV_Ho18x7CtjYgD0kakFeskn3TfJzR3e0I28,25677
|
2
|
-
redis_dict-2.5.1.dist-info/LICENSE,sha256=-QiLwYznh_vNUSz337k0faP9Jl0dgtCIHVZ39Uyl6cA,1070
|
3
|
-
redis_dict-2.5.1.dist-info/METADATA,sha256=dKyus34pm_rQ-icVu7L53N_QzMSaxbLWhKuVSTh7RfI,10000
|
4
|
-
redis_dict-2.5.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
5
|
-
redis_dict-2.5.1.dist-info/top_level.txt,sha256=Wyp5Xvq_imoxvu-c-Le1rbTZ3pYM5BF440H9YAcgBZ8,11
|
6
|
-
redis_dict-2.5.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|