Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/_parsers/resp3.py: 12%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from logging import getLogger
2from typing import Any, Union
4from ..exceptions import ConnectionError, InvalidResponse, ResponseError
5from ..typing import EncodableT
6from ..utils import SENTINEL
7from .base import (
8 AsyncPushNotificationsParser,
9 PushNotificationsParser,
10 _AsyncRESPBase,
11 _RESPBase,
12)
13from .socket import SERVER_CLOSED_CONNECTION_ERROR
16class _RESP3Parser(_RESPBase, PushNotificationsParser):
17 """RESP3 protocol implementation"""
19 def __init__(self, socket_read_size):
20 super().__init__(socket_read_size)
21 self.pubsub_push_handler_func = self.handle_pubsub_push_response
22 self.node_moving_push_handler_func = None
23 self.maintenance_push_handler_func = None
24 self.oss_cluster_maint_push_handler_func = None
25 self.invalidation_push_handler_func = None
27 def handle_pubsub_push_response(self, response):
28 logger = getLogger("push_response")
29 logger.debug("Push response: %s", response)
30 return response
32 def read_response(
33 self,
34 disable_decoding=False,
35 push_request=False,
36 timeout: Union[float, object] = SENTINEL,
37 ):
38 pos = self._buffer.get_pos() if self._buffer is not None else None
39 try:
40 result = self._read_response(
41 disable_decoding=disable_decoding,
42 push_request=push_request,
43 timeout=timeout,
44 )
45 except BaseException:
46 if self._buffer is not None:
47 self._buffer.rewind(pos)
48 raise
49 else:
50 if self._buffer is not None:
51 try:
52 self._buffer.purge()
53 except AttributeError:
54 # Buffer may have been set to None by another thread after
55 # the check above; result is still valid so we don't raise
56 pass
57 return result
59 def _read_response(
60 self,
61 disable_decoding=False,
62 push_request=False,
63 timeout: Union[float, object] = SENTINEL,
64 ):
65 raw = self._buffer.readline(timeout=timeout)
66 if not raw:
67 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
69 byte, response = raw[:1], raw[1:]
71 # server returned an error
72 if byte in (b"-", b"!"):
73 if byte == b"!":
74 response = self._buffer.read(int(response), timeout=timeout)
75 response = response.decode("utf-8", errors="replace")
76 error = self.parse_error(response)
77 # if the error is a ConnectionError, raise immediately so the user
78 # is notified
79 if isinstance(error, ConnectionError):
80 raise error
81 # otherwise, we're dealing with a ResponseError that might belong
82 # inside a pipeline response. the connection's read_response()
83 # and/or the pipeline's execute() will raise this error if
84 # necessary, so just return the exception instance here.
85 return error
86 # single value
87 elif byte == b"+":
88 pass
89 # null value
90 elif byte == b"_":
91 return None
92 # int and big int values
93 elif byte in (b":", b"("):
94 return int(response)
95 # double value
96 elif byte == b",":
97 return float(response)
98 # bool value
99 elif byte == b"#":
100 return response == b"t"
101 # bulk response
102 elif byte == b"$":
103 response = self._buffer.read(int(response), timeout=timeout)
104 # verbatim string response
105 elif byte == b"=":
106 response = self._buffer.read(int(response), timeout=timeout)[4:]
107 # array response
108 elif byte == b"*":
109 response = [
110 self._read_response(disable_decoding=disable_decoding, timeout=timeout)
111 for _ in range(int(response))
112 ]
113 # set response
114 elif byte == b"~":
115 # redis can return unhashable types (like dict) in a set,
116 # so we return sets as list, all the time, for predictability
117 response = [
118 self._read_response(disable_decoding=disable_decoding, timeout=timeout)
119 for _ in range(int(response))
120 ]
121 # map response
122 elif byte == b"%":
123 # We cannot use a dict-comprehension to parse stream.
124 # Evaluation order of key:val expression in dict comprehension only
125 # became defined to be left-right in version 3.8
126 resp_dict = {}
127 for _ in range(int(response)):
128 key = self._read_response(
129 disable_decoding=disable_decoding, timeout=timeout
130 )
131 resp_dict[key] = self._read_response(
132 disable_decoding=disable_decoding,
133 push_request=push_request,
134 timeout=timeout,
135 )
136 response = resp_dict
137 # push response
138 elif byte == b">":
139 response = [
140 self._read_response(
141 disable_decoding=disable_decoding,
142 push_request=push_request,
143 timeout=timeout,
144 )
145 for _ in range(int(response))
146 ]
147 response = self.handle_push_response(response)
149 # if this is a push request return the push response
150 if push_request:
151 return response
153 # `timeout` has to be forwarded here too: it is the caller's bound
154 # for this read, and dropping it would silently fall back to the
155 # connection's socket_timeout for the rest of the response just
156 # because a push message happened to interleave. The hiredis parser
157 # already forwards it on the same continuation.
158 return self._read_response(
159 disable_decoding=disable_decoding,
160 push_request=push_request,
161 timeout=timeout,
162 )
163 else:
164 raise InvalidResponse(f"Protocol Error: {raw!r}")
166 if isinstance(response, bytes) and disable_decoding is False:
167 response = self.encoder.decode(response)
169 return response
172class _AsyncRESP3Parser(_AsyncRESPBase, AsyncPushNotificationsParser):
173 def __init__(self, socket_read_size):
174 super().__init__(socket_read_size)
175 self.pubsub_push_handler_func = self.handle_pubsub_push_response
176 self.invalidation_push_handler_func = None
178 async def handle_pubsub_push_response(self, response):
179 logger = getLogger("push_response")
180 logger.debug("Push response: %s", response)
181 return response
183 async def read_response(
184 self, disable_decoding: bool = False, push_request: bool = False
185 ):
186 if not self._connected:
187 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
188 if self._chunks:
189 # augment parsing buffer with previously read data
190 self._buffer += b"".join(self._chunks)
191 self._chunks.clear()
192 self._pos = 0
193 response = await self._read_response(
194 disable_decoding=disable_decoding, push_request=push_request
195 )
196 # Successfully parsing a response allows us to clear our parsing buffer
197 self._clear()
198 return response
200 async def _read_response(
201 self, disable_decoding: bool = False, push_request: bool = False
202 ) -> Union[EncodableT, ResponseError, None]:
203 if not self._stream or not self.encoder:
204 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
205 raw = await self._readline()
206 response: Any
207 byte, response = raw[:1], raw[1:]
209 # if byte not in (b"-", b"+", b":", b"$", b"*"):
210 # raise InvalidResponse(f"Protocol Error: {raw!r}")
212 # server returned an error
213 if byte in (b"-", b"!"):
214 if byte == b"!":
215 response = await self._read(int(response))
216 response = response.decode("utf-8", errors="replace")
217 error = self.parse_error(response)
218 # if the error is a ConnectionError, raise immediately so the user
219 # is notified
220 if isinstance(error, ConnectionError):
221 self._clear() # Successful parse
222 raise error
223 # otherwise, we're dealing with a ResponseError that might belong
224 # inside a pipeline response. the connection's read_response()
225 # and/or the pipeline's execute() will raise this error if
226 # necessary, so just return the exception instance here.
227 return error
228 # single value
229 elif byte == b"+":
230 pass
231 # null value
232 elif byte == b"_":
233 return None
234 # int and big int values
235 elif byte in (b":", b"("):
236 return int(response)
237 # double value
238 elif byte == b",":
239 return float(response)
240 # bool value
241 elif byte == b"#":
242 return response == b"t"
243 # bulk response
244 elif byte == b"$":
245 response = await self._read(int(response))
246 # verbatim string response
247 elif byte == b"=":
248 response = (await self._read(int(response)))[4:]
249 # array response
250 elif byte == b"*":
251 response = [
252 (await self._read_response(disable_decoding=disable_decoding))
253 for _ in range(int(response))
254 ]
255 # set response
256 elif byte == b"~":
257 # redis can return unhashable types (like dict) in a set,
258 # so we always convert to a list, to have predictable return types
259 response = [
260 (await self._read_response(disable_decoding=disable_decoding))
261 for _ in range(int(response))
262 ]
263 # map response
264 elif byte == b"%":
265 # We cannot use a dict-comprehension to parse stream.
266 # Evaluation order of key:val expression in dict comprehension only
267 # became defined to be left-right in version 3.8
268 resp_dict = {}
269 for _ in range(int(response)):
270 key = await self._read_response(disable_decoding=disable_decoding)
271 resp_dict[key] = await self._read_response(
272 disable_decoding=disable_decoding, push_request=push_request
273 )
274 response = resp_dict
275 # push response
276 elif byte == b">":
277 response = [
278 (
279 await self._read_response(
280 disable_decoding=disable_decoding, push_request=push_request
281 )
282 )
283 for _ in range(int(response))
284 ]
285 response = await self.handle_push_response(response)
286 if not push_request:
287 return await self._read_response(
288 disable_decoding=disable_decoding, push_request=push_request
289 )
290 else:
291 return response
292 else:
293 raise InvalidResponse(f"Protocol Error: {raw!r}")
295 if isinstance(response, bytes) and disable_decoding is False:
296 response = self.encoder.decode(response)
297 return response