redis-dict 3.1.2__tar.gz → 3.2.1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {redis_dict-3.1.2/src/redis_dict.egg-info → redis_dict-3.2.1}/PKG-INFO +61 -49
- {redis_dict-3.1.2 → redis_dict-3.2.1}/README.md +58 -46
- {redis_dict-3.1.2 → redis_dict-3.2.1}/pyproject.toml +2 -2
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict/__init__.py +2 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict/core.py +119 -56
- redis_dict-3.2.1/src/redis_dict/python_dict.py +331 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1/src/redis_dict.egg-info}/PKG-INFO +61 -49
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict.egg-info/SOURCES.txt +1 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict.egg-info/requires.txt +1 -1
- {redis_dict-3.1.2 → redis_dict-3.2.1}/LICENSE +0 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/setup.cfg +0 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict/py.typed +0 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict/type_management.py +0 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict.egg-info/dependency_links.txt +0 -0
- {redis_dict-3.1.2 → redis_dict-3.2.1}/src/redis_dict.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: redis-dict
|
3
|
-
Version: 3.1
|
3
|
+
Version: 3.2.1
|
4
4
|
Summary: Dictionary with Redis as storage backend
|
5
5
|
Author-email: Melvin Bijman <bijman.m.m@gmail.com>
|
6
6
|
License: MIT
|
@@ -42,7 +42,7 @@ Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
42
42
|
Requires-Dist: typing_extensions>=4.5.0; extra == "dev"
|
43
43
|
Requires-Dist: attrs==22.2.0; extra == "dev"
|
44
44
|
Requires-Dist: cffi==1.15.1; extra == "dev"
|
45
|
-
Requires-Dist: cryptography==
|
45
|
+
Requires-Dist: cryptography==44.0.1; extra == "dev"
|
46
46
|
Requires-Dist: exceptiongroup==1.1.1; extra == "dev"
|
47
47
|
Requires-Dist: future==0.18.3; extra == "dev"
|
48
48
|
Requires-Dist: pycparser==2.21; extra == "dev"
|
@@ -122,6 +122,48 @@ In Redis our example looks like this.
|
|
122
122
|
"str:hello world"
|
123
123
|
```
|
124
124
|
|
125
|
+
## Types
|
126
|
+
|
127
|
+
### standard types
|
128
|
+
RedisDict supports a range of Python data types, from basic types to nested structures.
|
129
|
+
Basic types are handled natively, while complex data types like lists and dictionaries, RedisDict uses JSON serialization, specifically avoiding [pickle](https://docs.python.org/3/library/pickle.html) due to its security vulnerabilities within distributed computing contexts.
|
130
|
+
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
131
|
+
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.
|
132
|
+
Following types are supported:
|
133
|
+
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
134
|
+
```python
|
135
|
+
from uuid import UUID
|
136
|
+
from decimal import Decimal
|
137
|
+
from collections import OrderedDict, defaultdict
|
138
|
+
from datetime import datetime, date, time, timedelta
|
139
|
+
|
140
|
+
dic = RedisDict()
|
141
|
+
|
142
|
+
dic["string"] = "Hello World"
|
143
|
+
dic["number"] = 42
|
144
|
+
dic["float"] = 3.14
|
145
|
+
dic["bool"] = True
|
146
|
+
dic["None"] = None
|
147
|
+
|
148
|
+
dic["list"] = [1, 2, 3]
|
149
|
+
dic["dict"] = {"a": 1, "b": 2}
|
150
|
+
dic["tuple"] = (1, 2, 3)
|
151
|
+
dic["set"] = {1, 2, 3}
|
152
|
+
|
153
|
+
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
154
|
+
dic["date"] = date(2024, 1, 1)
|
155
|
+
dic["time"] = time(12, 30, 45)
|
156
|
+
dic["delta"] = timedelta(days=1, hours=2)
|
157
|
+
|
158
|
+
dic["decimal"] = Decimal("3.14159")
|
159
|
+
dic["complex"] = complex(1, 2)
|
160
|
+
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
161
|
+
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
162
|
+
|
163
|
+
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
164
|
+
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
165
|
+
dic["frozen"] = frozenset([1, 2, 3])
|
166
|
+
```
|
125
167
|
|
126
168
|
### Namespaces
|
127
169
|
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.
|
@@ -290,51 +332,6 @@ print(dic["d"]) # Output: 4
|
|
290
332
|
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/unit/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/tests/load/load_test.py#L1).
|
291
333
|
The unit-tests can be as used as a starting point.
|
292
334
|
|
293
|
-
## Types
|
294
|
-
|
295
|
-
### standard types
|
296
|
-
RedisDict supports a range of Python data types, from basic types to nested structures.
|
297
|
-
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.
|
298
|
-
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
299
|
-
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.
|
300
|
-
Following types are supported:
|
301
|
-
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
302
|
-
```python
|
303
|
-
from uuid import UUID
|
304
|
-
from decimal import Decimal
|
305
|
-
from collections import OrderedDict, defaultdict
|
306
|
-
from datetime import datetime, date, time, timedelta
|
307
|
-
|
308
|
-
dic = RedisDict()
|
309
|
-
|
310
|
-
dic["string"] = "Hello World"
|
311
|
-
dic["number"] = 42
|
312
|
-
dic["float"] = 3.14
|
313
|
-
dic["bool"] = True
|
314
|
-
dic["None"] = None
|
315
|
-
|
316
|
-
dic["list"] = [1, 2, 3]
|
317
|
-
dic["dict"] = {"a": 1, "b": 2}
|
318
|
-
dic["tuple"] = (1, 2, 3)
|
319
|
-
dic["set"] = {1, 2, 3}
|
320
|
-
|
321
|
-
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
322
|
-
dic["date"] = date(2024, 1, 1)
|
323
|
-
dic["time"] = time(12, 30, 45)
|
324
|
-
dic["delta"] = timedelta(days=1, hours=2)
|
325
|
-
|
326
|
-
dic["decimal"] = Decimal("3.14159")
|
327
|
-
dic["complex"] = complex(1, 2)
|
328
|
-
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
329
|
-
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
330
|
-
|
331
|
-
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
332
|
-
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
333
|
-
dic["frozen"] = frozenset([1, 2, 3])
|
334
|
-
```
|
335
|
-
|
336
|
-
|
337
|
-
|
338
335
|
### Nested types
|
339
336
|
Nested Types
|
340
337
|
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.
|
@@ -394,6 +391,21 @@ assert result.name == person.name
|
|
394
391
|
assert result.age == person.age
|
395
392
|
```
|
396
393
|
|
394
|
+
### Insertion Order
|
395
|
+
For insertion order, use the PythonRedisDict. This class is focused on Python dictionary behavior one-to-one.
|
396
|
+
It will eventually become a drop-in replacement for dictionary. Currently, nested types and typed keys are not yet supported but will be added in the future.
|
397
|
+
|
398
|
+
```python
|
399
|
+
from redis_dict import PythonRedisDict
|
400
|
+
|
401
|
+
dic = PythonRedisDict()
|
402
|
+
dic["1"] = "one"
|
403
|
+
dic["2"] = "two"
|
404
|
+
dic["3"] = "three"
|
405
|
+
|
406
|
+
assert list(dic.keys()) == ["1", "2", "3"]
|
407
|
+
```
|
408
|
+
|
397
409
|
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/extend_types_tests.py).
|
398
410
|
### Redis Encryption
|
399
411
|
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
|
@@ -417,7 +429,7 @@ redis_config = {
|
|
417
429
|
'port': 6380,
|
418
430
|
}
|
419
431
|
|
420
|
-
|
432
|
+
config_dic = RedisDict(**redis_config)
|
421
433
|
```
|
422
434
|
|
423
435
|
## Installation
|
@@ -50,6 +50,48 @@ In Redis our example looks like this.
|
|
50
50
|
"str:hello world"
|
51
51
|
```
|
52
52
|
|
53
|
+
## Types
|
54
|
+
|
55
|
+
### standard types
|
56
|
+
RedisDict supports a range of Python data types, from basic types to nested structures.
|
57
|
+
Basic types are handled natively, while complex data types like lists and dictionaries, RedisDict uses JSON serialization, specifically avoiding [pickle](https://docs.python.org/3/library/pickle.html) due to its security vulnerabilities within distributed computing contexts.
|
58
|
+
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
59
|
+
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.
|
60
|
+
Following types are supported:
|
61
|
+
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
62
|
+
```python
|
63
|
+
from uuid import UUID
|
64
|
+
from decimal import Decimal
|
65
|
+
from collections import OrderedDict, defaultdict
|
66
|
+
from datetime import datetime, date, time, timedelta
|
67
|
+
|
68
|
+
dic = RedisDict()
|
69
|
+
|
70
|
+
dic["string"] = "Hello World"
|
71
|
+
dic["number"] = 42
|
72
|
+
dic["float"] = 3.14
|
73
|
+
dic["bool"] = True
|
74
|
+
dic["None"] = None
|
75
|
+
|
76
|
+
dic["list"] = [1, 2, 3]
|
77
|
+
dic["dict"] = {"a": 1, "b": 2}
|
78
|
+
dic["tuple"] = (1, 2, 3)
|
79
|
+
dic["set"] = {1, 2, 3}
|
80
|
+
|
81
|
+
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
82
|
+
dic["date"] = date(2024, 1, 1)
|
83
|
+
dic["time"] = time(12, 30, 45)
|
84
|
+
dic["delta"] = timedelta(days=1, hours=2)
|
85
|
+
|
86
|
+
dic["decimal"] = Decimal("3.14159")
|
87
|
+
dic["complex"] = complex(1, 2)
|
88
|
+
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
89
|
+
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
90
|
+
|
91
|
+
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
92
|
+
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
93
|
+
dic["frozen"] = frozenset([1, 2, 3])
|
94
|
+
```
|
53
95
|
|
54
96
|
### Namespaces
|
55
97
|
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.
|
@@ -218,51 +260,6 @@ print(dic["d"]) # Output: 4
|
|
218
260
|
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/unit/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/tests/load/load_test.py#L1).
|
219
261
|
The unit-tests can be as used as a starting point.
|
220
262
|
|
221
|
-
## Types
|
222
|
-
|
223
|
-
### standard types
|
224
|
-
RedisDict supports a range of Python data types, from basic types to nested structures.
|
225
|
-
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.
|
226
|
-
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
227
|
-
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.
|
228
|
-
Following types are supported:
|
229
|
-
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
230
|
-
```python
|
231
|
-
from uuid import UUID
|
232
|
-
from decimal import Decimal
|
233
|
-
from collections import OrderedDict, defaultdict
|
234
|
-
from datetime import datetime, date, time, timedelta
|
235
|
-
|
236
|
-
dic = RedisDict()
|
237
|
-
|
238
|
-
dic["string"] = "Hello World"
|
239
|
-
dic["number"] = 42
|
240
|
-
dic["float"] = 3.14
|
241
|
-
dic["bool"] = True
|
242
|
-
dic["None"] = None
|
243
|
-
|
244
|
-
dic["list"] = [1, 2, 3]
|
245
|
-
dic["dict"] = {"a": 1, "b": 2}
|
246
|
-
dic["tuple"] = (1, 2, 3)
|
247
|
-
dic["set"] = {1, 2, 3}
|
248
|
-
|
249
|
-
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
250
|
-
dic["date"] = date(2024, 1, 1)
|
251
|
-
dic["time"] = time(12, 30, 45)
|
252
|
-
dic["delta"] = timedelta(days=1, hours=2)
|
253
|
-
|
254
|
-
dic["decimal"] = Decimal("3.14159")
|
255
|
-
dic["complex"] = complex(1, 2)
|
256
|
-
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
257
|
-
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
258
|
-
|
259
|
-
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
260
|
-
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
261
|
-
dic["frozen"] = frozenset([1, 2, 3])
|
262
|
-
```
|
263
|
-
|
264
|
-
|
265
|
-
|
266
263
|
### Nested types
|
267
264
|
Nested Types
|
268
265
|
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.
|
@@ -322,6 +319,21 @@ assert result.name == person.name
|
|
322
319
|
assert result.age == person.age
|
323
320
|
```
|
324
321
|
|
322
|
+
### Insertion Order
|
323
|
+
For insertion order, use the PythonRedisDict. This class is focused on Python dictionary behavior one-to-one.
|
324
|
+
It will eventually become a drop-in replacement for dictionary. Currently, nested types and typed keys are not yet supported but will be added in the future.
|
325
|
+
|
326
|
+
```python
|
327
|
+
from redis_dict import PythonRedisDict
|
328
|
+
|
329
|
+
dic = PythonRedisDict()
|
330
|
+
dic["1"] = "one"
|
331
|
+
dic["2"] = "two"
|
332
|
+
dic["3"] = "three"
|
333
|
+
|
334
|
+
assert list(dic.keys()) == ["1", "2", "3"]
|
335
|
+
```
|
336
|
+
|
325
337
|
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/extend_types_tests.py).
|
326
338
|
### Redis Encryption
|
327
339
|
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
|
@@ -345,7 +357,7 @@ redis_config = {
|
|
345
357
|
'port': 6380,
|
346
358
|
}
|
347
359
|
|
348
|
-
|
360
|
+
config_dic = RedisDict(**redis_config)
|
349
361
|
```
|
350
362
|
|
351
363
|
## Installation
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "redis-dict"
|
7
|
-
version = "3.1
|
7
|
+
version = "3.2.1"
|
8
8
|
description = "Dictionary with Redis as storage backend"
|
9
9
|
authors = [
|
10
10
|
{name = "Melvin Bijman", email = "bijman.m.m@gmail.com"},
|
@@ -62,7 +62,7 @@ dev = [
|
|
62
62
|
|
63
63
|
"attrs==22.2.0",
|
64
64
|
"cffi==1.15.1",
|
65
|
-
"cryptography==
|
65
|
+
"cryptography==44.0.1",
|
66
66
|
"exceptiongroup==1.1.1",
|
67
67
|
"future==0.18.3",
|
68
68
|
"pycparser==2.21",
|
@@ -2,10 +2,12 @@
|
|
2
2
|
from importlib.metadata import version, PackageNotFoundError
|
3
3
|
|
4
4
|
from .core import RedisDict
|
5
|
+
from .python_dict import PythonRedisDict
|
5
6
|
from .type_management import decoding_registry, encoding_registry, RedisDictJSONEncoder, RedisDictJSONDecoder
|
6
7
|
|
7
8
|
__all__ = [
|
8
9
|
'RedisDict',
|
10
|
+
'PythonRedisDict',
|
9
11
|
'decoding_registry',
|
10
12
|
'encoding_registry',
|
11
13
|
'RedisDictJSONEncoder',
|
@@ -7,10 +7,10 @@ from collections.abc import Mapping
|
|
7
7
|
|
8
8
|
from redis import StrictRedis
|
9
9
|
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
10
|
+
from .type_management import SENTINEL, EncodeFuncType, DecodeFuncType, EncodeType, DecodeType
|
11
|
+
from .type_management import _create_default_encode, _create_default_decode, _default_decoder
|
12
|
+
from .type_management import encoding_registry as enc_reg
|
13
|
+
from .type_management import decoding_registry as dec_reg
|
14
14
|
|
15
15
|
|
16
16
|
# pylint: disable=R0902, R0904
|
@@ -40,12 +40,14 @@ class RedisDict:
|
|
40
40
|
encoding_registry: EncodeType = enc_reg
|
41
41
|
decoding_registry: DecodeType = dec_reg
|
42
42
|
|
43
|
+
# pylint: disable=R0913
|
43
44
|
def __init__(self,
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
namespace: str = 'main',
|
46
|
+
expire: Union[int, timedelta, None] = None,
|
47
|
+
preserve_expiration: Optional[bool] = False,
|
48
|
+
redis: "Optional[StrictRedis[Any]]" = None,
|
49
|
+
raise_key_error_delete: bool = False,
|
50
|
+
**redis_kwargs: Any) -> None: # noqa: D202:R0913 pydocstyle clashes with Sphinx
|
49
51
|
"""
|
50
52
|
Initialize a RedisDict instance.
|
51
53
|
|
@@ -56,12 +58,14 @@ class RedisDict:
|
|
56
58
|
expire (Union[int, timedelta, None], optional): Expiration time for keys.
|
57
59
|
preserve_expiration (Optional[bool], optional): Preserve expiration on key updates.
|
58
60
|
redis (Optional[StrictRedis[Any]], optional): A Redis connection instance.
|
61
|
+
raise_key_error_delete (bool): Enable strict Python dict behavior raise if key not found when deleting.
|
59
62
|
**redis_kwargs (Any): Additional kwargs for Redis connection if not provided.
|
60
63
|
"""
|
61
64
|
|
62
65
|
self.namespace: str = namespace
|
63
66
|
self.expire: Union[int, timedelta, None] = expire
|
64
67
|
self.preserve_expiration: Optional[bool] = preserve_expiration
|
68
|
+
self.raise_key_error_delete: bool = raise_key_error_delete
|
65
69
|
if redis:
|
66
70
|
redis.connection_pool.connection_kwargs["decode_responses"] = True
|
67
71
|
|
@@ -74,6 +78,8 @@ class RedisDict:
|
|
74
78
|
self._iter: Iterator[str] = iter([])
|
75
79
|
self._max_string_size: int = 500 * 1024 * 1024 # 500mb
|
76
80
|
self._temp_redis: Optional[StrictRedis[Any]] = None
|
81
|
+
self._insertion_order_key = f"redis-dict-insertion-order-{namespace}"
|
82
|
+
self._batch_size: int = 200
|
77
83
|
|
78
84
|
def _format_key(self, key: str) -> str:
|
79
85
|
"""
|
@@ -85,9 +91,21 @@ class RedisDict:
|
|
85
91
|
Returns:
|
86
92
|
str: The formatted key with the namespace prefix.
|
87
93
|
"""
|
88
|
-
return f'{self.namespace}:{
|
94
|
+
return f'{self.namespace}:{key}'
|
89
95
|
|
90
|
-
def
|
96
|
+
def _parse_key(self, key: str) -> str:
|
97
|
+
"""
|
98
|
+
Parse a formatted key with the namespace prefix and type.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
key (str): The key to be parsed to type.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
str: The parsed key
|
105
|
+
"""
|
106
|
+
return key[len(self.namespace) + 1:]
|
107
|
+
|
108
|
+
def _valid_input(self, value: Any) -> bool:
|
91
109
|
"""
|
92
110
|
Check if the input value is valid based on the specified value type.
|
93
111
|
|
@@ -96,36 +114,35 @@ class RedisDict:
|
|
96
114
|
length does not exceed the maximum allowed size (500 MB).
|
97
115
|
|
98
116
|
Args:
|
99
|
-
|
100
|
-
val_type (str): The type of the input value ("str", "int", "float", or "bool").
|
117
|
+
value (Any): The input value to be validated.
|
101
118
|
|
102
119
|
Returns:
|
103
120
|
bool: True if the input value is valid, False otherwise.
|
104
121
|
"""
|
105
|
-
|
106
|
-
|
122
|
+
store_type = type(value).__name__
|
123
|
+
if store_type == "str":
|
124
|
+
return len(value) < self._max_string_size
|
107
125
|
return True
|
108
126
|
|
109
|
-
def _format_value(self,
|
127
|
+
def _format_value(self, value: Any) -> str:
|
110
128
|
"""Format a valid value with the type and encoded representation of the value.
|
111
129
|
|
112
130
|
Args:
|
113
|
-
key (str): The key of the value to be formatted.
|
114
131
|
value (Any): The value to be encoded and formatted.
|
115
132
|
|
116
|
-
Raises:
|
117
|
-
ValueError: If the value or key fail validation.
|
118
|
-
|
119
133
|
Returns:
|
120
134
|
str: The formatted value with the type and encoded representation of the value.
|
121
135
|
"""
|
122
|
-
store_type
|
123
|
-
if not self._valid_input(value, store_type) or not self._valid_input(key, "str"):
|
124
|
-
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
136
|
+
store_type = type(value).__name__
|
125
137
|
encoded_value = self.encoding_registry.get(store_type, lambda x: x)(value) # type: ignore
|
126
|
-
|
127
138
|
return f'{store_type}:{encoded_value}'
|
128
139
|
|
140
|
+
def _store_set(self, formatted_key: str, formatted_value: str) -> None:
|
141
|
+
if self.preserve_expiration and self.get_redis.exists(formatted_key):
|
142
|
+
self.redis.set(formatted_key, formatted_value, keepttl=True)
|
143
|
+
else:
|
144
|
+
self.redis.set(formatted_key, formatted_value, ex=self.expire)
|
145
|
+
|
129
146
|
def _store(self, key: str, value: Any) -> None:
|
130
147
|
"""
|
131
148
|
Store a value in Redis with the given key.
|
@@ -134,6 +151,9 @@ class RedisDict:
|
|
134
151
|
key (str): The key to store the value.
|
135
152
|
value (Any): The value to be stored.
|
136
153
|
|
154
|
+
Raises:
|
155
|
+
ValueError: If the value or key fail validation.
|
156
|
+
|
137
157
|
Note: Validity checks could be refactored to allow for custom exceptions that inherit from ValueError,
|
138
158
|
providing detailed information about why a specific validation failed.
|
139
159
|
This would enable users to specify which validity checks should be executed, add custom validity functions,
|
@@ -142,12 +162,13 @@ class RedisDict:
|
|
142
162
|
Allowing for simple dict set operation, but only cache data that makes sense.
|
143
163
|
|
144
164
|
"""
|
165
|
+
if not self._valid_input(value) or not self._valid_input(key):
|
166
|
+
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
167
|
+
|
145
168
|
formatted_key = self._format_key(key)
|
146
|
-
formatted_value = self._format_value(
|
147
|
-
|
148
|
-
|
149
|
-
else:
|
150
|
-
self.redis.set(formatted_key, formatted_value, ex=self.expire)
|
169
|
+
formatted_value = self._format_value(value)
|
170
|
+
|
171
|
+
self._store_set(formatted_key, formatted_value)
|
151
172
|
|
152
173
|
def _load(self, key: str) -> Tuple[bool, Any]:
|
153
174
|
"""
|
@@ -213,7 +234,7 @@ class RedisDict:
|
|
213
234
|
decode: Optional[DecodeFuncType] = None,
|
214
235
|
encoding_method_name: Optional[str] = None,
|
215
236
|
decoding_method_name: Optional[str] = None,
|
216
|
-
) -> None:
|
237
|
+
) -> None: # noqa: D202 pydocstyle clashes with Sphinx
|
217
238
|
"""
|
218
239
|
Extend RedisDict to support a custom type in the encode/decode mapping.
|
219
240
|
|
@@ -349,10 +370,25 @@ class RedisDict:
|
|
349
370
|
"""
|
350
371
|
Delete the value associated with the given key, analogous to a dictionary.
|
351
372
|
|
373
|
+
For distributed systems, we intentionally don't raise KeyError when the key doesn't exist.
|
374
|
+
This ensures identical code running across different systems won't randomly fail
|
375
|
+
when another system already achieved the deletion goal (key not existing).
|
376
|
+
|
377
|
+
Warning:
|
378
|
+
Setting dict_compliant=True will raise KeyError when key doesn't exist.
|
379
|
+
This is not recommended for distributed systems as it can cause KeyErrors
|
380
|
+
that are hard to debug when multiple systems interact with the same keys.
|
381
|
+
|
352
382
|
Args:
|
353
|
-
key (str): The key to delete
|
383
|
+
key (str): The key to delete
|
384
|
+
|
385
|
+
Raises:
|
386
|
+
KeyError: Only if dict_compliant=True and key doesn't exist
|
354
387
|
"""
|
355
|
-
self.
|
388
|
+
formatted_key = self._format_key(key)
|
389
|
+
result = self.redis.delete(formatted_key)
|
390
|
+
if self.raise_key_error_delete and not result:
|
391
|
+
raise KeyError(key)
|
356
392
|
|
357
393
|
def __contains__(self, key: str) -> bool:
|
358
394
|
"""
|
@@ -373,7 +409,7 @@ class RedisDict:
|
|
373
409
|
Returns:
|
374
410
|
int: The number of items in the RedisDict.
|
375
411
|
"""
|
376
|
-
return
|
412
|
+
return sum(1 for _ in self._scan_keys(full_scan=True))
|
377
413
|
|
378
414
|
def __iter__(self) -> Iterator[str]:
|
379
415
|
"""
|
@@ -470,12 +506,12 @@ class RedisDict:
|
|
470
506
|
return self
|
471
507
|
|
472
508
|
@classmethod
|
473
|
-
def __class_getitem__(cls: Type['RedisDict'],
|
509
|
+
def __class_getitem__(cls: Type['RedisDict'], _key: Any) -> Type['RedisDict']:
|
474
510
|
"""
|
475
511
|
Enable type hinting support like RedisDict[str, Any].
|
476
512
|
|
477
513
|
Args:
|
478
|
-
|
514
|
+
_key (Any): The type parameter(s) used in the type hint.
|
479
515
|
|
480
516
|
Returns:
|
481
517
|
Type[RedisDict]: The class itself, enabling type hint usage.
|
@@ -537,18 +573,19 @@ class RedisDict:
|
|
537
573
|
"""
|
538
574
|
return f'{self.namespace}:{search_term}*'
|
539
575
|
|
540
|
-
def _scan_keys(self, search_term: str = '') -> Iterator[str]:
|
541
|
-
"""
|
542
|
-
Scan for Redis keys matching the given search term.
|
576
|
+
def _scan_keys(self, search_term: str = '', full_scan: bool = False) -> Iterator[str]:
|
577
|
+
"""Scan for Redis keys matching the given search term.
|
543
578
|
|
544
579
|
Args:
|
545
580
|
search_term (str): A search term to filter keys. Defaults to ''.
|
581
|
+
full_scan (bool): During full scan uses batches of self._batch_size by default 200
|
546
582
|
|
547
583
|
Returns:
|
548
584
|
Iterator[str]: An iterator of matching Redis keys.
|
549
585
|
"""
|
550
586
|
search_query = self._create_iter_query(search_term)
|
551
|
-
|
587
|
+
count = None if full_scan else self._batch_size
|
588
|
+
return self.get_redis.scan_iter(match=search_query, count=count)
|
552
589
|
|
553
590
|
def get(self, key: str, default: Optional[Any] = None) -> Any:
|
554
591
|
"""Return the value for the given key if it exists, otherwise return the default value.
|
@@ -636,12 +673,24 @@ class RedisDict:
|
|
636
673
|
Redis pipelining is employed to group multiple commands into a single request, minimizing
|
637
674
|
network round-trip time, latency, and I/O load, thereby enhancing the overall performance.
|
638
675
|
|
639
|
-
It is important to highlight that the clear method can be safely executed within
|
640
|
-
the context of an initiated pipeline operation.
|
641
676
|
"""
|
642
677
|
with self.pipeline():
|
643
|
-
for key in self:
|
644
|
-
|
678
|
+
for key in self._scan_keys(full_scan=True):
|
679
|
+
self.redis.delete(key)
|
680
|
+
|
681
|
+
def _pop(self, formatted_key: str) -> Any:
|
682
|
+
"""
|
683
|
+
Remove the value associated with the given key and return it.
|
684
|
+
|
685
|
+
Or return the default value if the key is not found.
|
686
|
+
|
687
|
+
Args:
|
688
|
+
formatted_key (str): The formatted key to remove the value.
|
689
|
+
|
690
|
+
Returns:
|
691
|
+
Any: The value associated with the key or the default value.
|
692
|
+
"""
|
693
|
+
return self.get_redis.execute_command("GETDEL", formatted_key)
|
645
694
|
|
646
695
|
def pop(self, key: str, default: Union[Any, object] = SENTINEL) -> Any:
|
647
696
|
"""Analogous to a dictionary's pop method.
|
@@ -660,12 +709,11 @@ class RedisDict:
|
|
660
709
|
KeyError: If the key is not found and no default value is provided.
|
661
710
|
"""
|
662
711
|
formatted_key = self._format_key(key)
|
663
|
-
value = self.
|
712
|
+
value = self._pop(formatted_key)
|
664
713
|
if value is None:
|
665
714
|
if default is not SENTINEL:
|
666
715
|
return default
|
667
716
|
raise KeyError(formatted_key)
|
668
|
-
|
669
717
|
return self._transform(value)
|
670
718
|
|
671
719
|
def popitem(self) -> Tuple[str, Any]:
|
@@ -673,6 +721,8 @@ class RedisDict:
|
|
673
721
|
|
674
722
|
This method is analogous to the `popitem` method of a standard Python dictionary.
|
675
723
|
|
724
|
+
if dict_compliant set true stays true to In Python 3.7+, removes the last inserted item (LIFO order)
|
725
|
+
|
676
726
|
Returns:
|
677
727
|
tuple: A tuple containing a randomly chosen (key, value) pair.
|
678
728
|
|
@@ -688,22 +738,16 @@ class RedisDict:
|
|
688
738
|
except KeyError:
|
689
739
|
continue
|
690
740
|
|
691
|
-
def
|
692
|
-
"""
|
693
|
-
|
694
|
-
Return the value associated with the given key if it exists, otherwise set the value to the
|
695
|
-
default value and return it. Analogous to a dictionary's setdefault method.
|
741
|
+
def _create_set_get_command(self, formatted_key: str, formatted_value: str) -> Tuple[List[str], Dict[str, bool]]:
|
742
|
+
"""Create SET command arguments and options for Redis. For setdefault operation.
|
696
743
|
|
697
744
|
Args:
|
698
|
-
|
699
|
-
|
745
|
+
formatted_key (str): The formatted Redis key.
|
746
|
+
formatted_value (str): The formatted value to be set.
|
700
747
|
|
701
748
|
Returns:
|
702
|
-
|
749
|
+
Tuple[List[str], Dict[str, bool]]: A tuple containing the command arguments and options.
|
703
750
|
"""
|
704
|
-
formatted_key = self._format_key(key)
|
705
|
-
formatted_value = self._format_value(key, default_value)
|
706
|
-
|
707
751
|
# Setting {"get": True} enables parsing of the redis result as "GET", instead of "SET" command
|
708
752
|
options = {"get": True}
|
709
753
|
args = ["SET", formatted_key, formatted_value, "NX", "GET"]
|
@@ -713,8 +757,27 @@ class RedisDict:
|
|
713
757
|
expire_val = int(self.expire.total_seconds()) if isinstance(self.expire, timedelta) else self.expire
|
714
758
|
expire_str = str(1) if expire_val <= 1 else str(expire_val)
|
715
759
|
args.extend(["EX", expire_str])
|
760
|
+
return args, options
|
716
761
|
|
762
|
+
def setdefault(self, key: str, default_value: Optional[Any] = None) -> Any:
|
763
|
+
"""Get value under key, and if not present set default value.
|
764
|
+
|
765
|
+
Return the value associated with the given key if it exists, otherwise set the value to the
|
766
|
+
default value and return it. Analogous to a dictionary's setdefault method.
|
767
|
+
|
768
|
+
Args:
|
769
|
+
key (str): The key to retrieve the value.
|
770
|
+
default_value (Optional[Any], optional): The value to set if the key is not found.
|
771
|
+
|
772
|
+
Returns:
|
773
|
+
Any: The value associated with the key or the default value.
|
774
|
+
"""
|
775
|
+
formatted_key = self._format_key(key)
|
776
|
+
formatted_value = self._format_value(default_value)
|
777
|
+
|
778
|
+
args, options = self._create_set_get_command(formatted_key, formatted_value)
|
717
779
|
result = self.get_redis.execute_command(*args, **options)
|
780
|
+
|
718
781
|
if result is None:
|
719
782
|
return default_value
|
720
783
|
|
@@ -0,0 +1,331 @@
|
|
1
|
+
"""Python Redis Dict module."""
|
2
|
+
from typing import Any, Iterator, Tuple, Union, Optional, List, Dict
|
3
|
+
|
4
|
+
import time
|
5
|
+
from datetime import timedelta
|
6
|
+
|
7
|
+
from redis import StrictRedis
|
8
|
+
|
9
|
+
from .core import RedisDict
|
10
|
+
|
11
|
+
|
12
|
+
class PythonRedisDict(RedisDict):
|
13
|
+
"""Python dictionary with Redis as backend.
|
14
|
+
|
15
|
+
With support for advanced features, such as custom data types, pipelining, and key expiration.
|
16
|
+
|
17
|
+
This class focuses on having one-to-on behavior of a dictionary while using Redis as storage layer, allowing
|
18
|
+
for efficient storage and retrieval of key-value pairs. It supports various data types, including
|
19
|
+
strings, integers, floats, lists, dictionaries, tuples, sets, and user-defined types. The class
|
20
|
+
leverages the power of Redis pipelining to minimize network round-trip time, latency, and I/O load,
|
21
|
+
thereby optimizing performance for batch operations. Additionally, it allows for the management of
|
22
|
+
key expiration through the use of context managers.
|
23
|
+
|
24
|
+
The RedisDict class is designed to be analogous to a standard Python dictionary while providing
|
25
|
+
enhanced functionality, such as support for a wider range of data types and efficient batch operations.
|
26
|
+
It aims to offer a seamless and familiar interface for developers familiar with Python dictionaries,
|
27
|
+
enabling a smooth transition to a Redis-backed data store.
|
28
|
+
|
29
|
+
Extendable Types: You can extend RedisDict by adding or overriding encoding and decoding functions.
|
30
|
+
This functionality enables various use cases, such as managing encrypted data in Redis,
|
31
|
+
To implement this, simply create and register your custom encoding and decoding functions.
|
32
|
+
By delegating serialization to redis-dict, reduce complexity and have simple code in the codebase.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self,
|
36
|
+
namespace: str = 'main',
|
37
|
+
expire: Union[int, timedelta, None] = None,
|
38
|
+
preserve_expiration: Optional[bool] = False,
|
39
|
+
redis: "Optional[StrictRedis[Any]]" = None,
|
40
|
+
**redis_kwargs: Any) -> None: # noqa: D202 pydocstyle clashes with Sphinx
|
41
|
+
"""
|
42
|
+
Initialize a RedisDict instance.
|
43
|
+
|
44
|
+
Init the RedisDict instance.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
namespace (str): A prefix for keys stored in Redis.
|
48
|
+
expire (Union[int, timedelta, None], optional): Expiration time for keys.
|
49
|
+
preserve_expiration (Optional[bool], optional): Preserve expiration on key updates.
|
50
|
+
redis (Optional[StrictRedis[Any]], optional): A Redis connection instance.
|
51
|
+
**redis_kwargs (Any): Additional kwargs for Redis connection if not provided.
|
52
|
+
"""
|
53
|
+
super().__init__(
|
54
|
+
namespace=namespace,
|
55
|
+
expire=expire,
|
56
|
+
preserve_expiration=preserve_expiration,
|
57
|
+
redis=redis,
|
58
|
+
raise_key_error_delete=True,
|
59
|
+
**redis_kwargs
|
60
|
+
)
|
61
|
+
self._insertion_order_key = f"redis-dict-insertion-order-{namespace}"
|
62
|
+
|
63
|
+
def __delitem__(self, key: str) -> None:
|
64
|
+
"""
|
65
|
+
Delete the value associated with the given key, analogous to a dictionary.
|
66
|
+
|
67
|
+
For distributed systems, we intentionally don't raise KeyError when the key doesn't exist.
|
68
|
+
This ensures identical code running across different systems won't randomly fail
|
69
|
+
when another system already achieved the deletion goal (key not existing).
|
70
|
+
|
71
|
+
Warning:
|
72
|
+
Setting dict_compliant=True will raise KeyError when key doesn't exist.
|
73
|
+
This is not recommended for distributed systems as it can cause KeyErrors
|
74
|
+
that are hard to debug when multiple systems interact with the same keys.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
key (str): The key to delete
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
KeyError: Only if dict_compliant=True and key doesn't exist
|
81
|
+
"""
|
82
|
+
formatted_key = self._format_key(key)
|
83
|
+
|
84
|
+
result = self.redis.delete(formatted_key)
|
85
|
+
self._insertion_order_delete(formatted_key)
|
86
|
+
if not result:
|
87
|
+
raise KeyError(key)
|
88
|
+
|
89
|
+
def _store(self, key: str, value: Any) -> None:
|
90
|
+
"""
|
91
|
+
Store a value in Redis with the given key.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
key (str): The key to store the value.
|
95
|
+
value (Any): The value to be stored.
|
96
|
+
|
97
|
+
Raises:
|
98
|
+
ValueError: If the value or key fail validation.
|
99
|
+
|
100
|
+
Note: Validity checks could be refactored to allow for custom exceptions that inherit from ValueError,
|
101
|
+
providing detailed information about why a specific validation failed.
|
102
|
+
This would enable users to specify which validity checks should be executed, add custom validity functions,
|
103
|
+
and choose whether to fail on validation errors, or drop the data and only issue a warning and continue.
|
104
|
+
Example use case is caching, to cache data only when it's between min and max sizes.
|
105
|
+
Allowing for simple dict set operation, but only cache data that makes sense.
|
106
|
+
|
107
|
+
"""
|
108
|
+
if not self._valid_input(value) or not self._valid_input(key):
|
109
|
+
raise ValueError("Invalid input value or key size exceeded the maximum limit.")
|
110
|
+
|
111
|
+
formatted_key = self._format_key(key)
|
112
|
+
formatted_value = self._format_value(value)
|
113
|
+
|
114
|
+
with self.pipeline():
|
115
|
+
self._insertion_order_add(formatted_key)
|
116
|
+
self._store_set(formatted_key, formatted_value)
|
117
|
+
|
118
|
+
def setdefault(self, key: str, default_value: Optional[Any] = None) -> Any:
|
119
|
+
"""Get value under key, and if not present set default value.
|
120
|
+
|
121
|
+
Return the value associated with the given key if it exists, otherwise set the value to the
|
122
|
+
default value and return it. Analogous to a dictionary's setdefault method.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
key (str): The key to retrieve the value.
|
126
|
+
default_value (Optional[Any], optional): The value to set if the key is not found.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Any: The value associated with the key or the default value.
|
130
|
+
"""
|
131
|
+
formatted_key = self._format_key(key)
|
132
|
+
formatted_value = self._format_value(default_value)
|
133
|
+
|
134
|
+
# Todo bind both commands
|
135
|
+
args, options = self._create_set_get_command(formatted_key, formatted_value)
|
136
|
+
result = self.get_redis.execute_command(*args, **options)
|
137
|
+
self._insertion_order_add(formatted_key)
|
138
|
+
|
139
|
+
if result is None:
|
140
|
+
return default_value
|
141
|
+
|
142
|
+
return self._transform(result)
|
143
|
+
|
144
|
+
def __len__(self) -> int:
|
145
|
+
"""
|
146
|
+
Get the number of items in the RedisDict, analogous to a dictionary.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
int: The number of items in the RedisDict.
|
150
|
+
"""
|
151
|
+
return self._insertion_order_len()
|
152
|
+
|
153
|
+
def _scan_keys(self, search_term: str = '', full_scan: bool = False) -> Iterator[str]:
|
154
|
+
return self._insertion_order_iter()
|
155
|
+
|
156
|
+
def clear(self) -> None:
|
157
|
+
"""Remove all key-value pairs from the RedisDict in one batch operation using pipelining.
|
158
|
+
|
159
|
+
This method mimics the behavior of the `clear` method from a standard Python dictionary.
|
160
|
+
Redis pipelining is employed to group multiple commands into a single request, minimizing
|
161
|
+
network round-trip time, latency, and I/O load, thereby enhancing the overall performance.
|
162
|
+
|
163
|
+
"""
|
164
|
+
with self.pipeline():
|
165
|
+
self._insertion_order_clear()
|
166
|
+
for key in self._scan_keys(full_scan=True):
|
167
|
+
self.redis.delete(key)
|
168
|
+
|
169
|
+
def popitem(self) -> Tuple[str, Any]:
|
170
|
+
"""Remove and return a random (key, value) pair from the RedisDict as a tuple.
|
171
|
+
|
172
|
+
This method is analogous to the `popitem` method of a standard Python dictionary.
|
173
|
+
|
174
|
+
if dict_compliant set true stays true to In Python 3.7+, removes the last inserted item (LIFO order)
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
tuple: A tuple containing a randomly chosen (key, value) pair.
|
178
|
+
|
179
|
+
Raises:
|
180
|
+
KeyError: If RedisDict is empty.
|
181
|
+
"""
|
182
|
+
key = self._insertion_order_latest()
|
183
|
+
if key is None:
|
184
|
+
raise KeyError("popitem(): dictionary is empty")
|
185
|
+
return self._parse_key(key), self._transform(self._pop(key))
|
186
|
+
|
187
|
+
def _pop(self, formatted_key: str) -> Any:
|
188
|
+
"""
|
189
|
+
Remove the value associated with the given key and return it.
|
190
|
+
|
191
|
+
Or return the default value if the key is not found.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
formatted_key (str): The formatted key to remove the value.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
Any: The value associated with the key or the default value.
|
198
|
+
"""
|
199
|
+
# TODO bind both commands
|
200
|
+
self._insertion_order_delete(formatted_key)
|
201
|
+
return self.get_redis.execute_command("GETDEL", formatted_key)
|
202
|
+
|
203
|
+
def multi_get(self, _key: str) -> List[Any]:
|
204
|
+
"""
|
205
|
+
Not part of Python Redis Dict.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
_key (str): Not used.
|
209
|
+
|
210
|
+
Raises:
|
211
|
+
NotImplementedError: Not part of Python Redis Dict.
|
212
|
+
"""
|
213
|
+
raise NotImplementedError("Not part of PythonRedisDict")
|
214
|
+
|
215
|
+
def multi_chain_get(self, _keys: List[str]) -> List[Any]:
|
216
|
+
"""
|
217
|
+
Not part of Python Redis Dict.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
_keys (List[str]): Not used.
|
221
|
+
|
222
|
+
Raises:
|
223
|
+
NotImplementedError: Not part of Python Redis Dict.
|
224
|
+
"""
|
225
|
+
raise NotImplementedError("Not part of PythonRedisDict")
|
226
|
+
|
227
|
+
def multi_dict(self, _key: str) -> Dict[str, Any]:
|
228
|
+
"""
|
229
|
+
Not part of Python Redis Dict.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
_key (str): Not used.
|
233
|
+
|
234
|
+
Raises:
|
235
|
+
NotImplementedError: Not part of Python Redis Dict.
|
236
|
+
"""
|
237
|
+
raise NotImplementedError("Not part of PythonRedisDict")
|
238
|
+
|
239
|
+
def multi_del(self, _key: str) -> int:
|
240
|
+
"""
|
241
|
+
Not part of Python Redis Dict.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
_key (str): Not used.
|
245
|
+
|
246
|
+
Raises:
|
247
|
+
NotImplementedError: Not part of Python Redis Dict.
|
248
|
+
"""
|
249
|
+
raise NotImplementedError("Not part of PythonRedisDict")
|
250
|
+
|
251
|
+
def _insertion_order_add(self, formatted_key: str) -> bool:
|
252
|
+
"""Record a key's insertion into the dictionary.
|
253
|
+
|
254
|
+
This private method updates the insertion order tracking when a new key is added
|
255
|
+
to the dictionary.
|
256
|
+
|
257
|
+
Args:
|
258
|
+
formatted_key (str): The key being added to the dictionary.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
bool: True if the insertion order was updated, False otherwise.
|
262
|
+
"""
|
263
|
+
return bool(self.redis.zadd(self._insertion_order_key, {formatted_key: time.time()}))
|
264
|
+
|
265
|
+
def _insertion_order_delete(self, formatted_key: str) -> bool:
|
266
|
+
"""Remove a key from the insertion order tracking.
|
267
|
+
|
268
|
+
This private method updates the insertion order tracking when a key is removed
|
269
|
+
from the dictionary.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
formatted_key (str): The key being removed from the dictionary.
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
bool: True if the insertion order was updated, False otherwise.
|
276
|
+
"""
|
277
|
+
return bool(self.redis.zrem(self._insertion_order_key, formatted_key))
|
278
|
+
|
279
|
+
def _insertion_order_iter(self) -> Iterator[str]:
|
280
|
+
"""Create an iterator for dictionary keys in their insertion order.
|
281
|
+
|
282
|
+
This private method allows for iterating over the dictionary's keys in the order
|
283
|
+
they were inserted.
|
284
|
+
|
285
|
+
Yields:
|
286
|
+
str: Keys in their insertion order.
|
287
|
+
"""
|
288
|
+
# TODO add full_scan boolean and search terms.
|
289
|
+
first = True
|
290
|
+
cursor = -1
|
291
|
+
while cursor != 0:
|
292
|
+
if first:
|
293
|
+
cursor = 0
|
294
|
+
first = False
|
295
|
+
cursor, data = self.get_redis.zscan(
|
296
|
+
name=self._insertion_order_key,
|
297
|
+
cursor=cursor,
|
298
|
+
count=1
|
299
|
+
)
|
300
|
+
yield from (item[0] for item in data)
|
301
|
+
|
302
|
+
def _insertion_order_clear(self) -> bool:
|
303
|
+
"""Clear all insertion order information.
|
304
|
+
|
305
|
+
This private method resets the insertion order tracking for the dictionary.
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
bool: True if the insertion order was successfully cleared, False otherwise.
|
309
|
+
"""
|
310
|
+
return bool(self.redis.delete(self._insertion_order_key))
|
311
|
+
|
312
|
+
def _insertion_order_len(self) -> int:
|
313
|
+
"""Get the number of keys in the insertion order tracking.
|
314
|
+
|
315
|
+
This private method returns the count of keys being tracked for insertion order.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
int: The number of keys in the insertion order tracking.
|
319
|
+
"""
|
320
|
+
return self.get_redis.zcard(self._insertion_order_key)
|
321
|
+
|
322
|
+
def _insertion_order_latest(self) -> Union[str, None]:
|
323
|
+
"""Get the most recently inserted key in the dictionary.
|
324
|
+
|
325
|
+
This private method retrieves the key that was most recently added to the dictionary.
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
Union[str, None]: The most recently inserted key, or None if the dictionary is empty.
|
329
|
+
"""
|
330
|
+
result = self.redis.zrange(self._insertion_order_key, -1, -1)
|
331
|
+
return result[0] if result else None
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: redis-dict
|
3
|
-
Version: 3.1
|
3
|
+
Version: 3.2.1
|
4
4
|
Summary: Dictionary with Redis as storage backend
|
5
5
|
Author-email: Melvin Bijman <bijman.m.m@gmail.com>
|
6
6
|
License: MIT
|
@@ -42,7 +42,7 @@ Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
42
42
|
Requires-Dist: typing_extensions>=4.5.0; extra == "dev"
|
43
43
|
Requires-Dist: attrs==22.2.0; extra == "dev"
|
44
44
|
Requires-Dist: cffi==1.15.1; extra == "dev"
|
45
|
-
Requires-Dist: cryptography==
|
45
|
+
Requires-Dist: cryptography==44.0.1; extra == "dev"
|
46
46
|
Requires-Dist: exceptiongroup==1.1.1; extra == "dev"
|
47
47
|
Requires-Dist: future==0.18.3; extra == "dev"
|
48
48
|
Requires-Dist: pycparser==2.21; extra == "dev"
|
@@ -122,6 +122,48 @@ In Redis our example looks like this.
|
|
122
122
|
"str:hello world"
|
123
123
|
```
|
124
124
|
|
125
|
+
## Types
|
126
|
+
|
127
|
+
### standard types
|
128
|
+
RedisDict supports a range of Python data types, from basic types to nested structures.
|
129
|
+
Basic types are handled natively, while complex data types like lists and dictionaries, RedisDict uses JSON serialization, specifically avoiding [pickle](https://docs.python.org/3/library/pickle.html) due to its security vulnerabilities within distributed computing contexts.
|
130
|
+
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
131
|
+
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.
|
132
|
+
Following types are supported:
|
133
|
+
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
134
|
+
```python
|
135
|
+
from uuid import UUID
|
136
|
+
from decimal import Decimal
|
137
|
+
from collections import OrderedDict, defaultdict
|
138
|
+
from datetime import datetime, date, time, timedelta
|
139
|
+
|
140
|
+
dic = RedisDict()
|
141
|
+
|
142
|
+
dic["string"] = "Hello World"
|
143
|
+
dic["number"] = 42
|
144
|
+
dic["float"] = 3.14
|
145
|
+
dic["bool"] = True
|
146
|
+
dic["None"] = None
|
147
|
+
|
148
|
+
dic["list"] = [1, 2, 3]
|
149
|
+
dic["dict"] = {"a": 1, "b": 2}
|
150
|
+
dic["tuple"] = (1, 2, 3)
|
151
|
+
dic["set"] = {1, 2, 3}
|
152
|
+
|
153
|
+
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
154
|
+
dic["date"] = date(2024, 1, 1)
|
155
|
+
dic["time"] = time(12, 30, 45)
|
156
|
+
dic["delta"] = timedelta(days=1, hours=2)
|
157
|
+
|
158
|
+
dic["decimal"] = Decimal("3.14159")
|
159
|
+
dic["complex"] = complex(1, 2)
|
160
|
+
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
161
|
+
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
162
|
+
|
163
|
+
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
164
|
+
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
165
|
+
dic["frozen"] = frozenset([1, 2, 3])
|
166
|
+
```
|
125
167
|
|
126
168
|
### Namespaces
|
127
169
|
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.
|
@@ -290,51 +332,6 @@ print(dic["d"]) # Output: 4
|
|
290
332
|
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/unit/tests.py#L1) Or take a look at load test for batching [load test](https://github.com/Attumm/redis-dict/blob/main/tests/load/load_test.py#L1).
|
291
333
|
The unit-tests can be as used as a starting point.
|
292
334
|
|
293
|
-
## Types
|
294
|
-
|
295
|
-
### standard types
|
296
|
-
RedisDict supports a range of Python data types, from basic types to nested structures.
|
297
|
-
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.
|
298
|
-
Although the library supports nested structures, the recommended best practice is to use RedisDict as a shallow dictionary.
|
299
|
-
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.
|
300
|
-
Following types are supported:
|
301
|
-
`str, int, float, bool, NoneType, list, dict, tuple, set, datetime, date, time, timedelta, Decimal, complex, bytes, UUID, OrderedDict, defaultdict, frozenset`
|
302
|
-
```python
|
303
|
-
from uuid import UUID
|
304
|
-
from decimal import Decimal
|
305
|
-
from collections import OrderedDict, defaultdict
|
306
|
-
from datetime import datetime, date, time, timedelta
|
307
|
-
|
308
|
-
dic = RedisDict()
|
309
|
-
|
310
|
-
dic["string"] = "Hello World"
|
311
|
-
dic["number"] = 42
|
312
|
-
dic["float"] = 3.14
|
313
|
-
dic["bool"] = True
|
314
|
-
dic["None"] = None
|
315
|
-
|
316
|
-
dic["list"] = [1, 2, 3]
|
317
|
-
dic["dict"] = {"a": 1, "b": 2}
|
318
|
-
dic["tuple"] = (1, 2, 3)
|
319
|
-
dic["set"] = {1, 2, 3}
|
320
|
-
|
321
|
-
dic["datetime"] = datetime.date(2024, 1, 1, 12, 30, 45)
|
322
|
-
dic["date"] = date(2024, 1, 1)
|
323
|
-
dic["time"] = time(12, 30, 45)
|
324
|
-
dic["delta"] = timedelta(days=1, hours=2)
|
325
|
-
|
326
|
-
dic["decimal"] = Decimal("3.14159")
|
327
|
-
dic["complex"] = complex(1, 2)
|
328
|
-
dic["bytes"] = bytes([72, 101, 108, 108, 111])
|
329
|
-
dic["uuid"] = UUID('12345678-1234-5678-1234-567812345678')
|
330
|
-
|
331
|
-
dic["ordered"] = OrderedDict([('a', 1), ('b', 2)])
|
332
|
-
dic["default"] = defaultdict(int, {'a': 1, 'b': 2})
|
333
|
-
dic["frozen"] = frozenset([1, 2, 3])
|
334
|
-
```
|
335
|
-
|
336
|
-
|
337
|
-
|
338
335
|
### Nested types
|
339
336
|
Nested Types
|
340
337
|
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.
|
@@ -394,6 +391,21 @@ assert result.name == person.name
|
|
394
391
|
assert result.age == person.age
|
395
392
|
```
|
396
393
|
|
394
|
+
### Insertion Order
|
395
|
+
For insertion order, use the PythonRedisDict. This class is focused on Python dictionary behavior one-to-one.
|
396
|
+
It will eventually become a drop-in replacement for dictionary. Currently, nested types and typed keys are not yet supported but will be added in the future.
|
397
|
+
|
398
|
+
```python
|
399
|
+
from redis_dict import PythonRedisDict
|
400
|
+
|
401
|
+
dic = PythonRedisDict()
|
402
|
+
dic["1"] = "one"
|
403
|
+
dic["2"] = "two"
|
404
|
+
dic["3"] = "three"
|
405
|
+
|
406
|
+
assert list(dic.keys()) == ["1", "2", "3"]
|
407
|
+
```
|
408
|
+
|
397
409
|
For more information on [extending types](https://github.com/Attumm/redis-dict/blob/main/tests/unit/extend_types_tests.py).
|
398
410
|
### Redis Encryption
|
399
411
|
Setup guide for configuring and utilizing encrypted Redis TLS for redis-dict.
|
@@ -417,7 +429,7 @@ redis_config = {
|
|
417
429
|
'port': 6380,
|
418
430
|
}
|
419
431
|
|
420
|
-
|
432
|
+
config_dic = RedisDict(**redis_config)
|
421
433
|
```
|
422
434
|
|
423
435
|
## Installation
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|