coredis 4.24.0__py3-none-any.whl → 5.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of coredis might be problematic. Click here for more details.
- coredis/__init__.py +1 -3
- coredis/_packer.py +10 -10
- coredis/_protocols.py +23 -32
- coredis/_py_311_typing.py +20 -0
- coredis/_py_312_typing.py +17 -0
- coredis/_utils.py +49 -51
- coredis/_version.py +3 -3
- coredis/cache.py +57 -82
- coredis/client/__init__.py +1 -2
- coredis/client/basic.py +129 -56
- coredis/client/cluster.py +147 -70
- coredis/commands/__init__.py +27 -7
- coredis/commands/_key_spec.py +11 -10
- coredis/commands/_utils.py +1 -1
- coredis/commands/_validators.py +30 -20
- coredis/commands/_wrappers.py +19 -99
- coredis/commands/bitfield.py +10 -2
- coredis/commands/constants.py +20 -3
- coredis/commands/core.py +1627 -1246
- coredis/commands/function.py +21 -19
- coredis/commands/monitor.py +0 -71
- coredis/commands/pubsub.py +7 -142
- coredis/commands/request.py +108 -0
- coredis/commands/script.py +9 -9
- coredis/commands/sentinel.py +60 -49
- coredis/connection.py +14 -15
- coredis/exceptions.py +2 -2
- coredis/experimental/__init__.py +0 -4
- coredis/globals.py +3 -0
- coredis/modules/autocomplete.py +28 -30
- coredis/modules/base.py +15 -31
- coredis/modules/filters.py +269 -245
- coredis/modules/graph.py +61 -62
- coredis/modules/json.py +172 -140
- coredis/modules/response/_callbacks/autocomplete.py +5 -4
- coredis/modules/response/_callbacks/graph.py +34 -29
- coredis/modules/response/_callbacks/json.py +5 -3
- coredis/modules/response/_callbacks/search.py +49 -53
- coredis/modules/response/_callbacks/timeseries.py +18 -30
- coredis/modules/response/types.py +1 -5
- coredis/modules/search.py +186 -169
- coredis/modules/timeseries.py +184 -164
- coredis/parser.py +6 -19
- coredis/pipeline.py +391 -422
- coredis/pool/basic.py +7 -7
- coredis/pool/cluster.py +3 -3
- coredis/pool/nodemanager.py +10 -3
- coredis/response/_callbacks/__init__.py +76 -57
- coredis/response/_callbacks/acl.py +0 -3
- coredis/response/_callbacks/cluster.py +25 -16
- coredis/response/_callbacks/command.py +8 -6
- coredis/response/_callbacks/connection.py +4 -3
- coredis/response/_callbacks/geo.py +17 -13
- coredis/response/_callbacks/hash.py +13 -11
- coredis/response/_callbacks/keys.py +9 -5
- coredis/response/_callbacks/module.py +2 -3
- coredis/response/_callbacks/script.py +6 -8
- coredis/response/_callbacks/sentinel.py +21 -17
- coredis/response/_callbacks/server.py +36 -14
- coredis/response/_callbacks/sets.py +3 -4
- coredis/response/_callbacks/sorted_set.py +27 -24
- coredis/response/_callbacks/streams.py +22 -13
- coredis/response/_callbacks/strings.py +7 -6
- coredis/response/_callbacks/vector_sets.py +126 -0
- coredis/response/types.py +13 -4
- coredis/sentinel.py +1 -1
- coredis/stream.py +4 -3
- coredis/tokens.py +343 -16
- coredis/typing.py +432 -79
- {coredis-4.24.0.dist-info → coredis-5.0.0rc1.dist-info}/METADATA +4 -5
- coredis-5.0.0rc1.dist-info/RECORD +95 -0
- coredis/client/keydb.py +0 -336
- coredis/pipeline.pyi +0 -2103
- coredis-4.24.0.dist-info/RECORD +0 -93
- {coredis-4.24.0.dist-info → coredis-5.0.0rc1.dist-info}/WHEEL +0 -0
- {coredis-4.24.0.dist-info → coredis-5.0.0rc1.dist-info}/licenses/LICENSE +0 -0
- {coredis-4.24.0.dist-info → coredis-5.0.0rc1.dist-info}/top_level.txt +0 -0
coredis/typing.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import dataclasses
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
4
6
|
from collections import OrderedDict
|
|
5
7
|
from collections.abc import (
|
|
6
8
|
AsyncGenerator,
|
|
@@ -20,9 +22,10 @@ from collections.abc import (
|
|
|
20
22
|
Set,
|
|
21
23
|
ValuesView,
|
|
22
24
|
)
|
|
23
|
-
from types import ModuleType
|
|
25
|
+
from types import GenericAlias, ModuleType, UnionType
|
|
24
26
|
from typing import (
|
|
25
27
|
TYPE_CHECKING,
|
|
28
|
+
Any,
|
|
26
29
|
AnyStr,
|
|
27
30
|
ClassVar,
|
|
28
31
|
Final,
|
|
@@ -34,45 +37,25 @@ from typing import (
|
|
|
34
37
|
TypedDict,
|
|
35
38
|
TypeGuard,
|
|
36
39
|
TypeVar,
|
|
40
|
+
cast,
|
|
41
|
+
get_origin,
|
|
42
|
+
get_type_hints,
|
|
37
43
|
runtime_checkable,
|
|
38
44
|
)
|
|
39
45
|
|
|
40
|
-
from
|
|
46
|
+
from beartype import beartype
|
|
47
|
+
from beartype.door import infer_hint, is_bearable, is_subhint
|
|
48
|
+
from typing_extensions import (
|
|
49
|
+
NotRequired,
|
|
50
|
+
Self,
|
|
51
|
+
Unpack,
|
|
52
|
+
)
|
|
41
53
|
|
|
42
54
|
from coredis.config import Config
|
|
43
55
|
|
|
44
56
|
_runtime_checks = False
|
|
45
|
-
_beartype_found = False
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
import beartype
|
|
49
|
-
|
|
50
|
-
if not TYPE_CHECKING:
|
|
51
|
-
from beartype.typing import ( # noqa: F811
|
|
52
|
-
Iterable,
|
|
53
|
-
Iterator,
|
|
54
|
-
Mapping,
|
|
55
|
-
MutableMapping,
|
|
56
|
-
MutableSequence,
|
|
57
|
-
MutableSet,
|
|
58
|
-
OrderedDict,
|
|
59
|
-
Sequence,
|
|
60
|
-
ValuesView,
|
|
61
|
-
)
|
|
62
|
-
_beartype_found = True
|
|
63
|
-
except ImportError: # pragma: no cover
|
|
64
|
-
pass
|
|
65
|
-
|
|
66
|
-
if Config.runtime_checks and not TYPE_CHECKING: # pragma: no cover
|
|
67
|
-
if _beartype_found:
|
|
68
|
-
_runtime_checks = True
|
|
69
|
-
else:
|
|
70
|
-
warnings.warn(
|
|
71
|
-
"Runtime checks were enabled via environment variable COREDIS_RUNTIME_CHECKS"
|
|
72
|
-
" but could not import beartype"
|
|
73
|
-
)
|
|
74
57
|
|
|
75
|
-
RUNTIME_TYPECHECKS =
|
|
58
|
+
RUNTIME_TYPECHECKS = Config.runtime_checks and not TYPE_CHECKING
|
|
76
59
|
|
|
77
60
|
P = ParamSpec("P")
|
|
78
61
|
T_co = TypeVar("T_co", covariant=True)
|
|
@@ -80,15 +63,12 @@ R = TypeVar("R")
|
|
|
80
63
|
|
|
81
64
|
|
|
82
65
|
def safe_beartype(func: Callable[P, R]) -> Callable[P, R]:
|
|
83
|
-
if
|
|
84
|
-
return func
|
|
85
|
-
|
|
86
|
-
return beartype.beartype(func) if _beartype_found else func
|
|
66
|
+
return beartype(func) if RUNTIME_TYPECHECKS else func
|
|
87
67
|
|
|
88
68
|
|
|
89
69
|
def add_runtime_checks(func: Callable[P, R]) -> Callable[P, R]:
|
|
90
70
|
if RUNTIME_TYPECHECKS and not TYPE_CHECKING:
|
|
91
|
-
return
|
|
71
|
+
return beartype(func)
|
|
92
72
|
|
|
93
73
|
return func
|
|
94
74
|
|
|
@@ -100,9 +80,6 @@ class RedisError(Exception):
|
|
|
100
80
|
"""
|
|
101
81
|
|
|
102
82
|
|
|
103
|
-
CommandArgList = list[str | bytes | int | float]
|
|
104
|
-
|
|
105
|
-
|
|
106
83
|
class Node(TypedDict):
|
|
107
84
|
"""
|
|
108
85
|
Definition of a cluster node
|
|
@@ -112,19 +89,414 @@ class Node(TypedDict):
|
|
|
112
89
|
port: int
|
|
113
90
|
|
|
114
91
|
|
|
92
|
+
class RedisCommandP(Protocol):
|
|
93
|
+
"""
|
|
94
|
+
Protocol of a redis command with all associated arguments
|
|
95
|
+
converted into the shape expected by the redis server.
|
|
96
|
+
Used by :meth:`~coredis.Redis.execute_command`
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
#: The name of the redis command
|
|
100
|
+
name: bytes
|
|
101
|
+
#: All arguments to be passed to the command
|
|
102
|
+
arguments: tuple[RedisValueT, ...]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclasses.dataclass
|
|
106
|
+
class RedisCommand:
|
|
107
|
+
"""
|
|
108
|
+
Convenience data class that conforms to :class:`~coredis.typing.RedisCommandP`
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
#: The name of the redis command
|
|
112
|
+
name: bytes
|
|
113
|
+
#: All arguments to be passed to the command
|
|
114
|
+
arguments: tuple[RedisValueT, ...]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ExecutionParameters(TypedDict):
|
|
118
|
+
"""
|
|
119
|
+
Extra parameters that can be passed to :meth:`~coredis.Redis.execute_command`
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
#: Whether to decode the response
|
|
123
|
+
#: (ignoring the value of :paramref:`~coredis.Redis.decode_responses`)
|
|
124
|
+
decode: NotRequired[bool]
|
|
125
|
+
slot_arguments_range: NotRequired[tuple[int, int]]
|
|
126
|
+
|
|
127
|
+
|
|
115
128
|
#: Represents the acceptable types of a redis key
|
|
116
129
|
KeyT = str | bytes
|
|
117
130
|
|
|
131
|
+
|
|
132
|
+
class Serializable(Generic[R]):
|
|
133
|
+
"""
|
|
134
|
+
Wrapper to be used to pass arbitrary types to redis commands
|
|
135
|
+
to be eventually serialized by :class:`coredis.typing.TypeAdapter.serialize`
|
|
136
|
+
|
|
137
|
+
Wrapping a value in :class:`Serializable` will pass type checking
|
|
138
|
+
wherever a method expects a :class:`coredis.typing.ValueT` - however
|
|
139
|
+
it will still fail if there is no serializer registered through the instance
|
|
140
|
+
of :class:`coredis.typing.TypeAdapter` that is associated with the client.
|
|
141
|
+
|
|
142
|
+
For example::
|
|
143
|
+
|
|
144
|
+
class MyThing:
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
client = coredis.Redis()
|
|
148
|
+
|
|
149
|
+
# This will pass type checking but will fail with an :exc:`LookupError`
|
|
150
|
+
# at runtime
|
|
151
|
+
await client.set("fubar", coredis.typing.Serializable(MyThing()))
|
|
152
|
+
|
|
153
|
+
# however, if a serializer is registered, the above would succeed
|
|
154
|
+
@client.type_adapter.serializer
|
|
155
|
+
def _(value: MyThing) -> str:
|
|
156
|
+
... # some way to convert it to a string
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, value: R) -> None:
|
|
160
|
+
self.value = value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
AdaptableType = type | UnionType | GenericAlias
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TypeAdapter:
|
|
167
|
+
"""
|
|
168
|
+
Used by the coredis clients :class:`~coredis.Redis` and :class:`~coredis.RedisCluster`
|
|
169
|
+
through :paramref:`~coredis.Redis.type_adapter` for adapting complex types that require
|
|
170
|
+
custom serialization/deserialization with redis commands.
|
|
171
|
+
|
|
172
|
+
For example to use Decimal types with some common redis operations::
|
|
173
|
+
|
|
174
|
+
from decimal import Decimal
|
|
175
|
+
from typing import Any, Mapping, Iterable
|
|
176
|
+
from coredis import Redis
|
|
177
|
+
from coredis.typing import TypeAdapter, Serializable
|
|
178
|
+
|
|
179
|
+
adapter = TypeAdapter()
|
|
180
|
+
|
|
181
|
+
@adapter.serializer
|
|
182
|
+
def decimal_to_str(value: Decimal) -> str:
|
|
183
|
+
return str(value)
|
|
184
|
+
|
|
185
|
+
@adapter.deserializer
|
|
186
|
+
def value_to_decimal(value: str|bytes) -> Decimal:
|
|
187
|
+
return Decimal(value.decode("utf-8") if isinstance(value, bytes) else value)
|
|
188
|
+
|
|
189
|
+
@adapter.deserializer
|
|
190
|
+
def list_to_decimal_list(items: Iterable[str|bytes]) -> list[Decimal]:
|
|
191
|
+
return [value_to_decimal(value) for value in items]
|
|
192
|
+
|
|
193
|
+
@adapter.deserializer
|
|
194
|
+
def mapping_to_decimal_mapping(mapping: Mapping[str|bytes, str|bytes]) -> dict[str|bytes, Decimal]:
|
|
195
|
+
return {key: value_to_decimal(value) for key, value in mapping.items()}
|
|
196
|
+
|
|
197
|
+
client = coredis.Redis(type_adapter=adapter, decode_responses=True)
|
|
198
|
+
await client.set("key", Serializable(Decimal(1.5)))
|
|
199
|
+
await client.lpush("list", [Serializable(Decimal(1.5))])
|
|
200
|
+
await client.hset("dict", {"first": Serializable(Decimal(1.5))})
|
|
201
|
+
assert Decimal(1.5) == await client.get("key").transform(Decimal)
|
|
202
|
+
assert [Decimal(1.5)] == await client.lrange("list", 0, 0).transform(list[Decimal])
|
|
203
|
+
assert {"first": Decimal(1.5)} == await client.hgetall("dict").transform(dict[str, Decimal])
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
) -> None:
|
|
209
|
+
self.__serializers: dict[
|
|
210
|
+
AdaptableType,
|
|
211
|
+
tuple[Callable[[Any], RedisValueT], int],
|
|
212
|
+
] = {}
|
|
213
|
+
self.__deserializers: dict[
|
|
214
|
+
AdaptableType,
|
|
215
|
+
dict[AdaptableType, tuple[Callable[..., Any], int]],
|
|
216
|
+
] = {}
|
|
217
|
+
self.__deserializer_cache: dict[
|
|
218
|
+
tuple[AdaptableType, AdaptableType | GenericAlias],
|
|
219
|
+
Callable[..., Any],
|
|
220
|
+
] = {}
|
|
221
|
+
self.__serializer_cache: dict[AdaptableType, Callable[[Any], RedisValueT]] = {}
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def format_type(cls, type_like: AdaptableType) -> str:
|
|
225
|
+
if get_origin(type_like):
|
|
226
|
+
return str(type_like)
|
|
227
|
+
else:
|
|
228
|
+
return getattr(type_like, "__name__", str(type_like))
|
|
229
|
+
|
|
230
|
+
def register(
|
|
231
|
+
self,
|
|
232
|
+
type: type[R] | UnionType,
|
|
233
|
+
serializer: Callable[[R], RedisValueT],
|
|
234
|
+
deserializer: Callable[[Any], R],
|
|
235
|
+
deserializable_type: type = object,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Register both a serializer and a deserializer for :paramref:`type`
|
|
239
|
+
|
|
240
|
+
:param type: The type that should be serialized/deserialized
|
|
241
|
+
:param serializer: a function that receives an instance of :paramref:`type`
|
|
242
|
+
and returns a value of type :data:`coredis.typing.RedisValueT`
|
|
243
|
+
:param deserializer: a function that accepts the return types from
|
|
244
|
+
the redis commands that are expected to be used when deserializing
|
|
245
|
+
to :paramref:`type`.
|
|
246
|
+
:param deserializable_type: the types of values :paramref:`deserializer` should
|
|
247
|
+
be considered for
|
|
248
|
+
"""
|
|
249
|
+
self.register_serializer(type, serializer)
|
|
250
|
+
self.register_deserializer(type, deserializer, deserializable_type)
|
|
251
|
+
|
|
252
|
+
def register_serializer(
|
|
253
|
+
self,
|
|
254
|
+
serializable_type: type[R] | UnionType,
|
|
255
|
+
serializer: Callable[[R], RedisValueT],
|
|
256
|
+
) -> None:
|
|
257
|
+
"""
|
|
258
|
+
Register a serializer for :paramref:`type`
|
|
259
|
+
|
|
260
|
+
:param type: The type that will be serialized
|
|
261
|
+
:param serializer: a function that receives an instance of :paramref:`type`
|
|
262
|
+
and returns a value of type :data:`coredis.typing.RedisValueT`
|
|
263
|
+
"""
|
|
264
|
+
self.__serializers.setdefault(serializable_type, (serializer, 0))
|
|
265
|
+
self.__serializer_cache.clear()
|
|
266
|
+
|
|
267
|
+
def register_deserializer(
|
|
268
|
+
self,
|
|
269
|
+
deserialized_type: type[R] | UnionType,
|
|
270
|
+
deserializer: Callable[[Any], R],
|
|
271
|
+
deserializable_type: AdaptableType = object,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Register a deserializer for :paramref:`type` and automatically register
|
|
275
|
+
deserializers for common collection types that use this type.
|
|
276
|
+
|
|
277
|
+
:param type: The type that should be deserialized
|
|
278
|
+
:param deserializer: a function that accepts the return types from
|
|
279
|
+
the redis commands that are expected to be used when deserializing
|
|
280
|
+
to :paramref:`type`.
|
|
281
|
+
:param deserializable_type: the types of values :paramref:`deserializer` should
|
|
282
|
+
be considered for
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def register_collection_deserializer(
|
|
286
|
+
collection_type: AdaptableType,
|
|
287
|
+
deserializable_type: AdaptableType,
|
|
288
|
+
deserializer: Callable[[Any], Any],
|
|
289
|
+
) -> None:
|
|
290
|
+
self.__deserializers.setdefault(collection_type, {}).setdefault(
|
|
291
|
+
deserializable_type,
|
|
292
|
+
(deserializer, -1),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Register the base deserializer
|
|
296
|
+
self.__deserializers.setdefault(deserialized_type, {})[deserializable_type or object] = (
|
|
297
|
+
deserializer,
|
|
298
|
+
0,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Register collection deserializers
|
|
302
|
+
register_collection_deserializer(
|
|
303
|
+
GenericAlias(list, (deserialized_type,)),
|
|
304
|
+
GenericAlias(Iterable, deserializable_type),
|
|
305
|
+
lambda v: [deserializer(item) for item in v],
|
|
306
|
+
)
|
|
307
|
+
register_collection_deserializer(
|
|
308
|
+
GenericAlias(set, (deserialized_type,)),
|
|
309
|
+
GenericAlias(Iterable, deserializable_type),
|
|
310
|
+
lambda v: {deserializer(item) for item in v},
|
|
311
|
+
)
|
|
312
|
+
register_collection_deserializer(
|
|
313
|
+
GenericAlias(tuple, (deserialized_type, ...)),
|
|
314
|
+
GenericAlias(Iterable, deserializable_type),
|
|
315
|
+
lambda v: tuple([deserializer(item) for item in v]),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Register dictionary deserializers for existing types
|
|
319
|
+
for t in list(self.__deserializers):
|
|
320
|
+
if t != deserialized_type:
|
|
321
|
+
for rt in list(self.__deserializers[t]):
|
|
322
|
+
_deserializer, priority = self.__deserializers[t][rt]
|
|
323
|
+
if priority >= 0:
|
|
324
|
+
register_collection_deserializer(
|
|
325
|
+
GenericAlias(dict, (t, deserialized_type)),
|
|
326
|
+
GenericAlias(Mapping, (rt, deserializable_type)),
|
|
327
|
+
lambda m, key_deserializer=_deserializer: { # type: ignore
|
|
328
|
+
key_deserializer(k): deserializer(v) for k, v in m.items()
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
register_collection_deserializer(
|
|
332
|
+
GenericAlias(dict, (deserialized_type, t)),
|
|
333
|
+
GenericAlias(Mapping, (deserializable_type, rt)),
|
|
334
|
+
lambda m, value_deserializer=_deserializer: { # type: ignore
|
|
335
|
+
deserializer(k): value_deserializer(v) for k, v in m.items()
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Register dictionary deserializers for primitive types
|
|
340
|
+
for t in {bytes, str}:
|
|
341
|
+
register_collection_deserializer(
|
|
342
|
+
GenericAlias(dict, (t, deserialized_type)),
|
|
343
|
+
GenericAlias(Mapping, (t, deserializable_type)),
|
|
344
|
+
lambda v: {k: deserializer(v) for k, v in v.items()},
|
|
345
|
+
)
|
|
346
|
+
register_collection_deserializer(
|
|
347
|
+
GenericAlias(dict, (deserialized_type, t)),
|
|
348
|
+
GenericAlias(Mapping, (deserializable_type, t)),
|
|
349
|
+
lambda v: {deserializer(k): v for k, v in v.items()},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
self.__deserializer_cache.clear()
|
|
353
|
+
|
|
354
|
+
def serializer(self, func: Callable[[R], RedisValueT]) -> Callable[[R], RedisValueT]:
|
|
355
|
+
"""
|
|
356
|
+
Decorator for registering a serializer
|
|
357
|
+
|
|
358
|
+
:param func: A serialization function that accepts an instance of
|
|
359
|
+
type `R` and returns one of the types defined by :data:`coredis.typing.RedisValueT`
|
|
360
|
+
The acceptable serializable types are inferred
|
|
361
|
+
from the annotations in the function signature.
|
|
362
|
+
|
|
363
|
+
:raises ValueError: when the appropriate serializable type cannot be
|
|
364
|
+
inferred.
|
|
365
|
+
"""
|
|
366
|
+
if (parameters := list(inspect.signature(func).parameters.keys())) and (
|
|
367
|
+
input_hint := get_type_hints(func).get(parameters[0])
|
|
368
|
+
):
|
|
369
|
+
self.register_serializer(input_hint, func)
|
|
370
|
+
return func
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
"Unable to infer custom input type from decorated function. Check type annotations."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def deserializer(self, func: Callable[[Any], R]) -> Callable[[Any], R]:
|
|
377
|
+
"""
|
|
378
|
+
Decorator for registering a deserializer
|
|
379
|
+
|
|
380
|
+
:param func: A deserialization function that returns an instance of
|
|
381
|
+
type `R` that can be used with :meth:`deserialize`. The acceptable
|
|
382
|
+
deserializable types and the expected deserialized type are inferred
|
|
383
|
+
from the annotations in the function signature.
|
|
384
|
+
|
|
385
|
+
:raises ValueError: when the appropriate input/output types cannot be
|
|
386
|
+
inferred.
|
|
387
|
+
"""
|
|
388
|
+
if (
|
|
389
|
+
(parameters := list(inspect.signature(func).parameters.keys()))
|
|
390
|
+
and (input_hint := get_type_hints(func).get(parameters[0]))
|
|
391
|
+
) and (response_type := get_type_hints(func).get("return")):
|
|
392
|
+
self.register_deserializer(response_type, func, input_hint)
|
|
393
|
+
return func
|
|
394
|
+
else:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
"Unable to infer response type from decorated function. Check annotations."
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def serialize(self, value: Serializable[R]) -> RedisValueT:
|
|
400
|
+
"""
|
|
401
|
+
Serializes :paramref:`value` into one of the types represented by
|
|
402
|
+
:data:`~coredis.typing.RedisValueT` using a serializer registered
|
|
403
|
+
via :meth:`register_serializer` or decorated by :meth:`serializer`.
|
|
404
|
+
|
|
405
|
+
:param: a value wrapped in :class:`coredis.typing.Serializable`
|
|
406
|
+
"""
|
|
407
|
+
value_type = cast(AdaptableType, infer_hint(value.value))
|
|
408
|
+
if not (transform_function := self.__serializer_cache.get(value_type, None)):
|
|
409
|
+
candidate: tuple[AdaptableType, Callable[[R], RedisValueT] | None] = (object, None)
|
|
410
|
+
|
|
411
|
+
for t in self.__serializers:
|
|
412
|
+
if is_bearable(value.value, t):
|
|
413
|
+
if not candidate[1] or is_subhint(t, candidate[0]):
|
|
414
|
+
candidate = (t, self.__serializers[t][0])
|
|
415
|
+
if candidate[1]:
|
|
416
|
+
transform_function = candidate[1]
|
|
417
|
+
self.__serializer_cache[value_type] = transform_function
|
|
418
|
+
if not transform_function:
|
|
419
|
+
raise LookupError(
|
|
420
|
+
f"No registered serializer to serialize {self.format_type(value_type)}"
|
|
421
|
+
)
|
|
422
|
+
return transform_function(value.value)
|
|
423
|
+
|
|
424
|
+
def deserialize(self, value: Any, return_type: type[R]) -> R:
|
|
425
|
+
"""
|
|
426
|
+
Deserializes :paramref:`value` into an instance of :paramref:`return_type`
|
|
427
|
+
using a deserializer registered via :meth:`register_deserializer` or decorated
|
|
428
|
+
by :meth:`deserializer`.
|
|
429
|
+
|
|
430
|
+
:param value: the value to be deserialized (typically something returned by one of
|
|
431
|
+
the redis commands)
|
|
432
|
+
:param return_type: The type to deserialize to
|
|
433
|
+
"""
|
|
434
|
+
value_type = cast(AdaptableType, infer_hint(value))
|
|
435
|
+
if not (deserializer := self.__deserializer_cache.get((value_type, return_type), None)):
|
|
436
|
+
if exact_match := self.__deserializers.get(return_type, {}).get(value_type, None):
|
|
437
|
+
deserializer = exact_match[0]
|
|
438
|
+
else:
|
|
439
|
+
candidate: tuple[AdaptableType, AdaptableType, Callable[[Any], R] | None, int] = (
|
|
440
|
+
object,
|
|
441
|
+
object,
|
|
442
|
+
None,
|
|
443
|
+
-100,
|
|
444
|
+
)
|
|
445
|
+
for registered_type, transforms in self.__deserializers.items():
|
|
446
|
+
if is_subhint(return_type, registered_type):
|
|
447
|
+
for expected_value_type in transforms:
|
|
448
|
+
if (
|
|
449
|
+
is_bearable(value, expected_value_type)
|
|
450
|
+
and is_subhint(registered_type, candidate[0])
|
|
451
|
+
and is_subhint(expected_value_type, candidate[1])
|
|
452
|
+
and transforms[expected_value_type][1] >= candidate[3]
|
|
453
|
+
):
|
|
454
|
+
candidate = (
|
|
455
|
+
registered_type,
|
|
456
|
+
expected_value_type,
|
|
457
|
+
transforms[expected_value_type][0],
|
|
458
|
+
transforms[expected_value_type][1],
|
|
459
|
+
)
|
|
460
|
+
deserializer = candidate[2]
|
|
461
|
+
if deserializer:
|
|
462
|
+
deserialized = deserializer(value)
|
|
463
|
+
if RUNTIME_TYPECHECKS and not is_subhint(
|
|
464
|
+
transformed_type := cast(type, infer_hint(deserialized)), return_type
|
|
465
|
+
):
|
|
466
|
+
raise TypeError(
|
|
467
|
+
f"Invalid deserializer. Requested {self.format_type(return_type)} but deserializer returned {self.format_type(transformed_type)}"
|
|
468
|
+
)
|
|
469
|
+
self.__deserializer_cache[(value_type, return_type)] = deserializer
|
|
470
|
+
return deserialized
|
|
471
|
+
elif is_subhint(value_type, return_type):
|
|
472
|
+
return cast(R, value)
|
|
473
|
+
else:
|
|
474
|
+
raise LookupError(
|
|
475
|
+
f"No registered deserializer to convert {self.format_type(value_type)} to {self.format_type(return_type)}"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
118
479
|
#: Represents the different python primitives that are accepted
|
|
119
480
|
#: as input parameters for commands that can be used with loosely
|
|
120
|
-
#: defined types. These
|
|
121
|
-
#:
|
|
122
|
-
|
|
481
|
+
#: defined types. These will eventually be serialized before being
|
|
482
|
+
#: sent to redis.
|
|
483
|
+
#:
|
|
484
|
+
#: Additionally any object wrapped in a :class:`Serializable` will be
|
|
485
|
+
#: accepted and will be serialized using an appropriate type adapter
|
|
486
|
+
#: registered with the client. See :ref:`api/typing:custom types` for more details.
|
|
487
|
+
ValueT = str | bytes | int | float | Serializable[Any]
|
|
123
488
|
|
|
124
489
|
#: The canonical type used for input parameters that represent "strings"
|
|
125
490
|
#: that are transmitted to redis.
|
|
126
491
|
StringT = str | bytes
|
|
127
492
|
|
|
493
|
+
CommandArgList = list[ValueT]
|
|
494
|
+
|
|
495
|
+
#: Primitive types that we can expect to be sent to redis with
|
|
496
|
+
#: simple serialization. The internals of coredis
|
|
497
|
+
#: pass around arguments to redis commands as this type.
|
|
498
|
+
RedisValueT = str | bytes | int | float
|
|
499
|
+
|
|
128
500
|
#: Restricted union of container types accepted as arguments to apis
|
|
129
501
|
#: that accept a variable number values for an argument (such as keys, values).
|
|
130
502
|
#: This is used instead of :class:`typing.Iterable` as the latter allows
|
|
@@ -147,45 +519,17 @@ StringT = str | bytes
|
|
|
147
519
|
#: length(b"123") # invalid
|
|
148
520
|
Parameters = list[T_co] | Set[T_co] | tuple[T_co, ...] | ValuesView[T_co] | Iterator[T_co]
|
|
149
521
|
|
|
150
|
-
#:
|
|
522
|
+
#: Primitives returned by redis
|
|
151
523
|
ResponsePrimitive = StringT | int | float | bool | None
|
|
152
524
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#:
|
|
156
|
-
#: This should preferably be represented by a recursive definition to allow for
|
|
157
|
-
#: Limitations in runtime type checkers (beartype) requires conditionally loosening
|
|
158
|
-
#: the definition with the use of :class:`typing.Any` for now.
|
|
159
|
-
|
|
160
|
-
if TYPE_CHECKING:
|
|
161
|
-
ResponseType = (
|
|
162
|
-
ResponsePrimitive
|
|
163
|
-
| list["ResponseType"]
|
|
164
|
-
| MutableSet[
|
|
165
|
-
ResponsePrimitive | tuple[ResponsePrimitive, ...] | frozenset[ResponsePrimitive]
|
|
166
|
-
]
|
|
167
|
-
| dict[
|
|
168
|
-
ResponsePrimitive | tuple[ResponsePrimitive, ...] | frozenset[ResponsePrimitive],
|
|
169
|
-
"ResponseType",
|
|
170
|
-
]
|
|
171
|
-
| RedisError # response errors get mapped to exceptions.
|
|
172
|
-
)
|
|
525
|
+
if sys.version_info >= (3, 12):
|
|
526
|
+
from ._py_312_typing import JsonType, ResponseType
|
|
173
527
|
else:
|
|
174
|
-
from
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
ResponsePrimitive
|
|
178
|
-
| list[Any]
|
|
179
|
-
| MutableSet[
|
|
180
|
-
ResponsePrimitive | tuple[ResponsePrimitive, ...] | frozenset[ResponsePrimitive]
|
|
181
|
-
]
|
|
182
|
-
| dict[
|
|
183
|
-
ResponsePrimitive | tuple[ResponsePrimitive, ...] | frozenset[ResponsePrimitive],
|
|
184
|
-
Any,
|
|
185
|
-
]
|
|
186
|
-
| RedisError # response errors get mapped to exceptions.
|
|
187
|
-
)
|
|
528
|
+
from ._py_311_typing import JsonType, ResponseType
|
|
529
|
+
|
|
530
|
+
|
|
188
531
|
__all__ = [
|
|
532
|
+
"Serializable",
|
|
189
533
|
"AnyStr",
|
|
190
534
|
"AsyncIterator",
|
|
191
535
|
"AsyncGenerator",
|
|
@@ -200,6 +544,7 @@ __all__ = [
|
|
|
200
544
|
"Hashable",
|
|
201
545
|
"Iterable",
|
|
202
546
|
"Iterator",
|
|
547
|
+
"JsonType",
|
|
203
548
|
"KeyT",
|
|
204
549
|
"Literal",
|
|
205
550
|
"Mapping",
|
|
@@ -209,10 +554,14 @@ __all__ = [
|
|
|
209
554
|
"MutableSequence",
|
|
210
555
|
"NamedTuple",
|
|
211
556
|
"Node",
|
|
557
|
+
"NotRequired",
|
|
212
558
|
"OrderedDict",
|
|
213
559
|
"Parameters",
|
|
214
560
|
"ParamSpec",
|
|
215
561
|
"Protocol",
|
|
562
|
+
"RedisCommand",
|
|
563
|
+
"RedisCommandP",
|
|
564
|
+
"ExecutionParameters",
|
|
216
565
|
"ResponsePrimitive",
|
|
217
566
|
"ResponseType",
|
|
218
567
|
"runtime_checkable",
|
|
@@ -222,8 +571,12 @@ __all__ = [
|
|
|
222
571
|
"TypeGuard",
|
|
223
572
|
"TypedDict",
|
|
224
573
|
"TypeVar",
|
|
574
|
+
"Unpack",
|
|
225
575
|
"ValueT",
|
|
576
|
+
"RedisValueT",
|
|
226
577
|
"ValuesView",
|
|
227
578
|
"TYPE_CHECKING",
|
|
579
|
+
"TypeAdapter",
|
|
580
|
+
"ValueT",
|
|
228
581
|
"RUNTIME_TYPECHECKS",
|
|
229
582
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coredis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0rc1
|
|
4
4
|
Summary: Python async client for Redis key-value store
|
|
5
5
|
Home-page: https://github.com/alisaifee/coredis
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -26,11 +26,11 @@ Requires-Python: >=3.10
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: async_timeout<6,>4
|
|
29
|
+
Requires-Dist: beartype>=0.20
|
|
29
30
|
Requires-Dist: deprecated>=1.2
|
|
30
|
-
Requires-Dist: typing_extensions>=4.
|
|
31
|
+
Requires-Dist: typing_extensions>=4.13
|
|
31
32
|
Requires-Dist: packaging<26,>=21
|
|
32
33
|
Requires-Dist: pympler<2,>1
|
|
33
|
-
Requires-Dist: wrapt<2,>=1.1.0
|
|
34
34
|
Provides-Extra: recipes
|
|
35
35
|
Requires-Dist: aiobotocore>=2.15.2; extra == "recipes"
|
|
36
36
|
Requires-Dist: asyncache>=0.3.1; extra == "recipes"
|
|
@@ -201,7 +201,7 @@ Details about supported Redis modules and their commands can be found
|
|
|
201
201
|
|
|
202
202
|
## Compatibility
|
|
203
203
|
|
|
204
|
-
coredis is tested against redis versions >= `
|
|
204
|
+
coredis is tested against redis versions >= `7.0`
|
|
205
205
|
The test matrix status can be reviewed
|
|
206
206
|
[here](https://github.com/alisaifee/coredis/actions/workflows/main.yml)
|
|
207
207
|
|
|
@@ -221,7 +221,6 @@ coredis is additionally tested against:
|
|
|
221
221
|
|
|
222
222
|
**coredis** is known to work with the following databases that have redis protocol compatibility:
|
|
223
223
|
|
|
224
|
-
- [KeyDB](https://docs.keydb.dev/)
|
|
225
224
|
- [Dragonfly](https://dragonflydb.io/)
|
|
226
225
|
- [Redict](https://redict.io/)
|
|
227
226
|
- [Valkey](https://github.com/valkey-io/valkey)
|