coredis 5.5.0__cp313-cp313-macosx_11_0_arm64.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.
- 22fe76227e35f92ab5c3__mypyc.cpython-313-darwin.so +0 -0
- coredis/__init__.py +42 -0
- coredis/_enum.py +42 -0
- coredis/_json.py +11 -0
- coredis/_packer.cpython-313-darwin.so +0 -0
- coredis/_packer.py +71 -0
- coredis/_protocols.py +50 -0
- coredis/_py_311_typing.py +20 -0
- coredis/_py_312_typing.py +17 -0
- coredis/_sidecar.py +114 -0
- coredis/_utils.cpython-313-darwin.so +0 -0
- coredis/_utils.py +440 -0
- coredis/_version.py +34 -0
- coredis/_version.pyi +1 -0
- coredis/cache.py +801 -0
- coredis/client/__init__.py +6 -0
- coredis/client/basic.py +1240 -0
- coredis/client/cluster.py +1265 -0
- coredis/commands/__init__.py +64 -0
- coredis/commands/_key_spec.py +517 -0
- coredis/commands/_utils.py +108 -0
- coredis/commands/_validators.py +159 -0
- coredis/commands/_wrappers.py +175 -0
- coredis/commands/bitfield.py +110 -0
- coredis/commands/constants.py +662 -0
- coredis/commands/core.py +8484 -0
- coredis/commands/function.py +408 -0
- coredis/commands/monitor.py +168 -0
- coredis/commands/pubsub.py +905 -0
- coredis/commands/request.py +108 -0
- coredis/commands/script.py +296 -0
- coredis/commands/sentinel.py +246 -0
- coredis/config.py +50 -0
- coredis/connection.py +906 -0
- coredis/constants.cpython-313-darwin.so +0 -0
- coredis/constants.py +37 -0
- coredis/credentials.py +45 -0
- coredis/exceptions.py +360 -0
- coredis/experimental/__init__.py +1 -0
- coredis/globals.py +23 -0
- coredis/modules/__init__.py +121 -0
- coredis/modules/autocomplete.py +138 -0
- coredis/modules/base.py +262 -0
- coredis/modules/filters.py +1319 -0
- coredis/modules/graph.py +362 -0
- coredis/modules/json.py +691 -0
- coredis/modules/response/__init__.py +0 -0
- coredis/modules/response/_callbacks/__init__.py +0 -0
- coredis/modules/response/_callbacks/autocomplete.py +42 -0
- coredis/modules/response/_callbacks/graph.py +237 -0
- coredis/modules/response/_callbacks/json.py +21 -0
- coredis/modules/response/_callbacks/search.py +221 -0
- coredis/modules/response/_callbacks/timeseries.py +158 -0
- coredis/modules/response/types.py +179 -0
- coredis/modules/search.py +1089 -0
- coredis/modules/timeseries.py +1139 -0
- coredis/parser.cpython-313-darwin.so +0 -0
- coredis/parser.py +344 -0
- coredis/pipeline.py +1225 -0
- coredis/pool/__init__.py +11 -0
- coredis/pool/basic.py +453 -0
- coredis/pool/cluster.py +517 -0
- coredis/pool/nodemanager.py +340 -0
- coredis/py.typed +0 -0
- coredis/recipes/__init__.py +0 -0
- coredis/recipes/credentials/__init__.py +5 -0
- coredis/recipes/credentials/iam_provider.py +63 -0
- coredis/recipes/locks/__init__.py +5 -0
- coredis/recipes/locks/extend.lua +17 -0
- coredis/recipes/locks/lua_lock.py +281 -0
- coredis/recipes/locks/release.lua +10 -0
- coredis/response/__init__.py +5 -0
- coredis/response/_callbacks/__init__.py +538 -0
- coredis/response/_callbacks/acl.py +32 -0
- coredis/response/_callbacks/cluster.py +183 -0
- coredis/response/_callbacks/command.py +86 -0
- coredis/response/_callbacks/connection.py +31 -0
- coredis/response/_callbacks/geo.py +58 -0
- coredis/response/_callbacks/hash.py +85 -0
- coredis/response/_callbacks/keys.py +59 -0
- coredis/response/_callbacks/module.py +33 -0
- coredis/response/_callbacks/script.py +85 -0
- coredis/response/_callbacks/sentinel.py +179 -0
- coredis/response/_callbacks/server.py +241 -0
- coredis/response/_callbacks/sets.py +44 -0
- coredis/response/_callbacks/sorted_set.py +204 -0
- coredis/response/_callbacks/streams.py +185 -0
- coredis/response/_callbacks/strings.py +70 -0
- coredis/response/_callbacks/vector_sets.py +159 -0
- coredis/response/_utils.py +33 -0
- coredis/response/types.py +416 -0
- coredis/retry.py +233 -0
- coredis/sentinel.py +477 -0
- coredis/stream.py +369 -0
- coredis/tokens.py +2286 -0
- coredis/typing.py +593 -0
- coredis-5.5.0.dist-info/METADATA +211 -0
- coredis-5.5.0.dist-info/RECORD +100 -0
- coredis-5.5.0.dist-info/WHEEL +6 -0
- coredis-5.5.0.dist-info/licenses/LICENSE +23 -0
|
Binary file
|
coredis/parser.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Hashable
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from coredis._protocols import ConnectionP
|
|
9
|
+
from coredis._utils import b
|
|
10
|
+
from coredis.constants import SYM_CRLF, RESPDataType
|
|
11
|
+
from coredis.exceptions import (
|
|
12
|
+
AskError,
|
|
13
|
+
AuthenticationFailureError,
|
|
14
|
+
AuthenticationRequiredError,
|
|
15
|
+
AuthorizationError,
|
|
16
|
+
BusyLoadingError,
|
|
17
|
+
ClusterCrossSlotError,
|
|
18
|
+
ClusterDownError,
|
|
19
|
+
ConnectionError,
|
|
20
|
+
ExecAbortError,
|
|
21
|
+
InvalidResponse,
|
|
22
|
+
MovedError,
|
|
23
|
+
NoKeyError,
|
|
24
|
+
NoScriptError,
|
|
25
|
+
NotBusyError,
|
|
26
|
+
ProtocolError,
|
|
27
|
+
ReadOnlyError,
|
|
28
|
+
RedisError,
|
|
29
|
+
ResponseError,
|
|
30
|
+
StreamConsumerGroupError,
|
|
31
|
+
StreamDuplicateConsumerGroupError,
|
|
32
|
+
TryAgainError,
|
|
33
|
+
UnblockedError,
|
|
34
|
+
UnknownCommandError,
|
|
35
|
+
WrongTypeError,
|
|
36
|
+
)
|
|
37
|
+
from coredis.typing import (
|
|
38
|
+
Final,
|
|
39
|
+
MutableSet,
|
|
40
|
+
NamedTuple,
|
|
41
|
+
ResponsePrimitive,
|
|
42
|
+
ResponseType,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NotEnoughData:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
NOT_ENOUGH_DATA: Final[NotEnoughData] = NotEnoughData()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RESPNode:
|
|
54
|
+
__slots__ = ("depth", "key", "node_type")
|
|
55
|
+
depth: int
|
|
56
|
+
key: Hashable
|
|
57
|
+
node_type: int
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
depth: int,
|
|
62
|
+
node_type: int,
|
|
63
|
+
key: (ResponsePrimitive | tuple[ResponsePrimitive, ...] | frozenset[ResponsePrimitive]),
|
|
64
|
+
):
|
|
65
|
+
self.depth = depth
|
|
66
|
+
self.node_type = node_type
|
|
67
|
+
self.key = key
|
|
68
|
+
|
|
69
|
+
def append(self, item: ResponseType) -> None:
|
|
70
|
+
raise NotImplementedError()
|
|
71
|
+
|
|
72
|
+
def ensure_hashable(self, item: ResponseType) -> Hashable:
|
|
73
|
+
if isinstance(item, (int, float, bool, str, bytes)):
|
|
74
|
+
return item
|
|
75
|
+
elif isinstance(item, set):
|
|
76
|
+
return frozenset(self.ensure_hashable(cast(ResponseType, i)) for i in item)
|
|
77
|
+
elif isinstance(item, list):
|
|
78
|
+
return tuple(self.ensure_hashable(i) for i in item)
|
|
79
|
+
elif isinstance(item, dict):
|
|
80
|
+
return tuple(
|
|
81
|
+
(cast(ResponsePrimitive, k), self.ensure_hashable(v)) for k, v in item.items()
|
|
82
|
+
)
|
|
83
|
+
return item # noqa
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ListNode(RESPNode):
|
|
87
|
+
__slots__ = ("container",)
|
|
88
|
+
|
|
89
|
+
def __init__(self, depth: int, node_type: int) -> None:
|
|
90
|
+
self.container: list[ResponseType] = []
|
|
91
|
+
super().__init__(depth, node_type, None)
|
|
92
|
+
|
|
93
|
+
def append(self, item: ResponseType) -> None:
|
|
94
|
+
self.depth -= 1
|
|
95
|
+
self.container.append(item)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DictNode(RESPNode):
|
|
99
|
+
__slots__ = ("container", "key")
|
|
100
|
+
|
|
101
|
+
def __init__(self, depth: int) -> None:
|
|
102
|
+
self.container: dict[Hashable, ResponseType] = {}
|
|
103
|
+
super().__init__(depth * 2, RESPDataType.MAP, None)
|
|
104
|
+
|
|
105
|
+
def append(self, item: ResponseType) -> None:
|
|
106
|
+
self.depth -= 1
|
|
107
|
+
if not self.key:
|
|
108
|
+
self.key = self.ensure_hashable(item)
|
|
109
|
+
else:
|
|
110
|
+
self.container[self.key] = item
|
|
111
|
+
self.key = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SetNode(RESPNode):
|
|
115
|
+
__slots__ = ("container",)
|
|
116
|
+
|
|
117
|
+
def __init__(self, depth: int) -> None:
|
|
118
|
+
self.container: MutableSet[Hashable] = set()
|
|
119
|
+
super().__init__(depth, RESPDataType.SET, None)
|
|
120
|
+
|
|
121
|
+
def append(
|
|
122
|
+
self,
|
|
123
|
+
item: ResponseType,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.depth -= 1
|
|
126
|
+
self.container.add(self.ensure_hashable(item))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class UnpackedResponse(NamedTuple):
|
|
130
|
+
response_type: int
|
|
131
|
+
response: ResponseType
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Parser:
|
|
135
|
+
"""
|
|
136
|
+
Interface between a connection and Unpacker
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
EXCEPTION_CLASSES: dict[str, type[RedisError] | dict[str, type[RedisError]]] = {
|
|
140
|
+
"ASK": AskError,
|
|
141
|
+
"BUSYGROUP": StreamDuplicateConsumerGroupError,
|
|
142
|
+
"CLUSTERDOWN": ClusterDownError,
|
|
143
|
+
"CROSSSLOT": ClusterCrossSlotError,
|
|
144
|
+
"ERR": {
|
|
145
|
+
"max number of clients reached": ConnectionError,
|
|
146
|
+
"unknown command": UnknownCommandError,
|
|
147
|
+
"unknown subcommand": UnknownCommandError,
|
|
148
|
+
"sub command not supported": UnknownCommandError,
|
|
149
|
+
},
|
|
150
|
+
"EXECABORT": ExecAbortError,
|
|
151
|
+
"LOADING": BusyLoadingError,
|
|
152
|
+
"NOSCRIPT": NoScriptError,
|
|
153
|
+
"MOVED": MovedError,
|
|
154
|
+
"NOAUTH": AuthenticationRequiredError,
|
|
155
|
+
"NOGROUP": StreamConsumerGroupError,
|
|
156
|
+
"NOKEY": NoKeyError,
|
|
157
|
+
"NOPERM": AuthorizationError,
|
|
158
|
+
"NOPROTO": ProtocolError,
|
|
159
|
+
"NOTBUSY": NotBusyError,
|
|
160
|
+
"READONLY": ReadOnlyError,
|
|
161
|
+
"TRYAGAIN": TryAgainError,
|
|
162
|
+
"UNBLOCKED": UnblockedError,
|
|
163
|
+
"WRONGPASS": AuthenticationFailureError,
|
|
164
|
+
"WRONGTYPE": WrongTypeError,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def __init__(self) -> None:
|
|
168
|
+
self.push_messages: asyncio.Queue[ResponseType] | None = None
|
|
169
|
+
self.localbuffer: BytesIO = BytesIO(b"")
|
|
170
|
+
self.bytes_read: int = 0
|
|
171
|
+
self.bytes_written: int = 0
|
|
172
|
+
self.nodes: list[ListNode | SetNode | DictNode] = []
|
|
173
|
+
|
|
174
|
+
def feed(self, data: bytes) -> None:
|
|
175
|
+
self.localbuffer.seek(self.bytes_written)
|
|
176
|
+
self.bytes_written += self.localbuffer.write(data)
|
|
177
|
+
self.localbuffer.seek(self.bytes_read)
|
|
178
|
+
|
|
179
|
+
def on_connect(self, connection: ConnectionP) -> None:
|
|
180
|
+
"""Called when the stream connects"""
|
|
181
|
+
self.push_messages = connection.push_messages
|
|
182
|
+
|
|
183
|
+
def on_disconnect(self) -> None:
|
|
184
|
+
"""Called when the stream disconnects"""
|
|
185
|
+
if not self.localbuffer.closed:
|
|
186
|
+
self.localbuffer.seek(0)
|
|
187
|
+
self.localbuffer.truncate()
|
|
188
|
+
self.bytes_read = self.bytes_written = 0
|
|
189
|
+
self.nodes.clear()
|
|
190
|
+
|
|
191
|
+
def can_read(self) -> bool:
|
|
192
|
+
return (self.bytes_written - self.bytes_read) > 0
|
|
193
|
+
|
|
194
|
+
def try_decode(self, data: bytes, encoding: str) -> bytes | str:
|
|
195
|
+
try:
|
|
196
|
+
return data.decode(encoding)
|
|
197
|
+
except ValueError:
|
|
198
|
+
return data
|
|
199
|
+
|
|
200
|
+
def get_response(
|
|
201
|
+
self,
|
|
202
|
+
decode: bool,
|
|
203
|
+
encoding: str | None = None,
|
|
204
|
+
push_message_types: set[bytes] | None = None,
|
|
205
|
+
) -> NotEnoughData | ResponseType:
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
:param decode: Whether to decode simple or bulk strings
|
|
209
|
+
:param push_message_types: the push message types to return if they
|
|
210
|
+
arrive. If a message arrives that does not match the filter, it will
|
|
211
|
+
be put on the :data:`~coredis.connection.BaseConnection.push_messages`
|
|
212
|
+
queue
|
|
213
|
+
:return: The next available parsed response read from the connection.
|
|
214
|
+
If there is not enough data on the wire a ``NotEnoughData`` instance
|
|
215
|
+
will be returned.
|
|
216
|
+
"""
|
|
217
|
+
while True:
|
|
218
|
+
response = self.parse(decode, encoding)
|
|
219
|
+
if isinstance(response, NotEnoughData):
|
|
220
|
+
return response
|
|
221
|
+
else:
|
|
222
|
+
if response and response.response_type == RESPDataType.PUSH:
|
|
223
|
+
assert isinstance(response.response, list)
|
|
224
|
+
assert self.push_messages
|
|
225
|
+
if not push_message_types or b(response.response[0]) not in push_message_types:
|
|
226
|
+
self.push_messages.put_nowait(response.response)
|
|
227
|
+
continue
|
|
228
|
+
else:
|
|
229
|
+
break
|
|
230
|
+
else:
|
|
231
|
+
break
|
|
232
|
+
return response.response if response else None
|
|
233
|
+
|
|
234
|
+
def parse(
|
|
235
|
+
self,
|
|
236
|
+
decode_bytes: bool,
|
|
237
|
+
encoding: str | None,
|
|
238
|
+
) -> UnpackedResponse | None | NotEnoughData:
|
|
239
|
+
parsed: UnpackedResponse | None = None
|
|
240
|
+
|
|
241
|
+
while True:
|
|
242
|
+
data = self.localbuffer.readline()
|
|
243
|
+
if not data[-2::] == SYM_CRLF:
|
|
244
|
+
return NOT_ENOUGH_DATA
|
|
245
|
+
data_len = len(data)
|
|
246
|
+
self.bytes_read += data_len
|
|
247
|
+
marker, chunk = data[0], data[1:-2]
|
|
248
|
+
response: ResponseType = None
|
|
249
|
+
if marker == RESPDataType.SIMPLE_STRING:
|
|
250
|
+
response = chunk
|
|
251
|
+
if decode_bytes and encoding:
|
|
252
|
+
response = self.try_decode(response, encoding)
|
|
253
|
+
elif marker == RESPDataType.BULK_STRING or marker == RESPDataType.VERBATIM:
|
|
254
|
+
length = int(chunk)
|
|
255
|
+
if length == -1:
|
|
256
|
+
response = None
|
|
257
|
+
else:
|
|
258
|
+
if (self.bytes_written - self.bytes_read) < length + 2:
|
|
259
|
+
self.bytes_read -= data_len
|
|
260
|
+
return NOT_ENOUGH_DATA
|
|
261
|
+
data = self.localbuffer.read(length + 2)
|
|
262
|
+
self.bytes_read += length + 2
|
|
263
|
+
response = data[:-2]
|
|
264
|
+
if marker == RESPDataType.VERBATIM:
|
|
265
|
+
if response[:3] != b"txt":
|
|
266
|
+
raise InvalidResponse(
|
|
267
|
+
f"Unexpected verbatim string of type {response[:3]!r}"
|
|
268
|
+
)
|
|
269
|
+
response = response[4:]
|
|
270
|
+
if decode_bytes and encoding:
|
|
271
|
+
response = self.try_decode(response, encoding)
|
|
272
|
+
elif marker in [RESPDataType.INT, RESPDataType.BIGNUMBER]:
|
|
273
|
+
response = int(chunk)
|
|
274
|
+
elif marker == RESPDataType.DOUBLE:
|
|
275
|
+
response = float(chunk)
|
|
276
|
+
elif marker == RESPDataType.NONE:
|
|
277
|
+
response = None
|
|
278
|
+
elif marker == RESPDataType.BOOLEAN:
|
|
279
|
+
response = chunk[0] == ord(b"t")
|
|
280
|
+
elif (
|
|
281
|
+
marker == RESPDataType.ARRAY
|
|
282
|
+
or marker == RESPDataType.PUSH
|
|
283
|
+
or marker == RESPDataType.MAP
|
|
284
|
+
or marker == RESPDataType.SET
|
|
285
|
+
):
|
|
286
|
+
length = int(chunk)
|
|
287
|
+
if length >= 0:
|
|
288
|
+
if marker in {RESPDataType.ARRAY, RESPDataType.PUSH}:
|
|
289
|
+
self.nodes.append(ListNode(length, marker))
|
|
290
|
+
elif marker == RESPDataType.MAP:
|
|
291
|
+
self.nodes.append(DictNode(length))
|
|
292
|
+
else:
|
|
293
|
+
self.nodes.append(SetNode(length))
|
|
294
|
+
if length > 0:
|
|
295
|
+
continue
|
|
296
|
+
elif marker == RESPDataType.ERROR:
|
|
297
|
+
response = cast(ResponseType, self.parse_error(bytes(chunk).decode()))
|
|
298
|
+
else:
|
|
299
|
+
raise InvalidResponse(f"Protocol Error: {chr(marker)}, {bytes(chunk)!r}")
|
|
300
|
+
|
|
301
|
+
if self.nodes:
|
|
302
|
+
if self.nodes[-1].depth > 0:
|
|
303
|
+
self.nodes[-1].append(response)
|
|
304
|
+
if self.nodes[-1].depth > 1:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
while len(self.nodes) > 1 and self.nodes[-1].depth == 0:
|
|
308
|
+
self.nodes[-2].append(self.nodes[-1].container)
|
|
309
|
+
self.nodes.pop()
|
|
310
|
+
|
|
311
|
+
if len(self.nodes) == 1 and self.nodes[-1].depth == 0:
|
|
312
|
+
node = self.nodes.pop()
|
|
313
|
+
parsed = UnpackedResponse(node.node_type, node.container)
|
|
314
|
+
break
|
|
315
|
+
if not self.nodes:
|
|
316
|
+
parsed = UnpackedResponse(marker, response)
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
if self.bytes_read == self.bytes_written:
|
|
320
|
+
self.localbuffer.seek(0)
|
|
321
|
+
self.localbuffer.truncate()
|
|
322
|
+
self.bytes_read = self.bytes_written = 0
|
|
323
|
+
return parsed
|
|
324
|
+
|
|
325
|
+
def parse_error(self, response: str) -> RedisError:
|
|
326
|
+
"""
|
|
327
|
+
Parse an error response
|
|
328
|
+
|
|
329
|
+
:meta private:
|
|
330
|
+
"""
|
|
331
|
+
error_code = response.split(" ")[0]
|
|
332
|
+
if error_code in self.EXCEPTION_CLASSES:
|
|
333
|
+
response = response[len(error_code) + 1 :]
|
|
334
|
+
exception_class = self.EXCEPTION_CLASSES[error_code]
|
|
335
|
+
|
|
336
|
+
if isinstance(exception_class, dict):
|
|
337
|
+
options = exception_class.items()
|
|
338
|
+
exception_class = ResponseError
|
|
339
|
+
for err, exc in options:
|
|
340
|
+
if response.lower().startswith(err):
|
|
341
|
+
exception_class = exc
|
|
342
|
+
break
|
|
343
|
+
return exception_class(response)
|
|
344
|
+
return ResponseError(response)
|