1import datetime
2
3from redis.utils import str_if_bytes
4
5
6def timestamp_to_datetime(response):
7 "Converts a unix timestamp to a Python datetime object"
8 if not response:
9 return None
10 try:
11 response = int(response)
12 except ValueError:
13 return None
14 return datetime.datetime.fromtimestamp(response)
15
16
17def parse_debug_object(response):
18 "Parse the results of Redis's DEBUG OBJECT command into a Python dict"
19 # The 'type' of the object is the first item in the response, but isn't
20 # prefixed with a name
21 response = str_if_bytes(response)
22 response = "type:" + response
23 response = dict(kv.split(":") for kv in response.split())
24
25 # parse some expected int values from the string response
26 # note: this cmd isn't spec'd so these may not appear in all redis versions
27 int_fields = ("refcount", "serializedlength", "lru", "lru_seconds_idle")
28 for field in int_fields:
29 if field in response:
30 response[field] = int(response[field])
31
32 return response
33
34
35def parse_info(response):
36 """Parse the result of Redis's INFO command into a Python dict"""
37 info = {}
38 response = str_if_bytes(response)
39
40 def get_value(value):
41 if "," not in value and "=" not in value:
42 try:
43 if "." in value:
44 return float(value)
45 else:
46 return int(value)
47 except ValueError:
48 return value
49 elif "=" not in value:
50 return [get_value(v) for v in value.split(",") if v]
51 else:
52 sub_dict = {}
53 for item in value.split(","):
54 if not item:
55 continue
56 if "=" in item:
57 k, v = item.rsplit("=", 1)
58 sub_dict[k] = get_value(v)
59 else:
60 sub_dict[item] = True
61 return sub_dict
62
63 for line in response.splitlines():
64 if line and not line.startswith("#"):
65 if line.find(":") != -1:
66 # Split, the info fields keys and values.
67 # Note that the value may contain ':'. but the 'host:'
68 # pseudo-command is the only case where the key contains ':'
69 key, value = line.split(":", 1)
70 if key == "cmdstat_host":
71 key, value = line.rsplit(":", 1)
72
73 if key == "module":
74 # Hardcode a list for key 'modules' since there could be
75 # multiple lines that started with 'module'
76 info.setdefault("modules", []).append(get_value(value))
77 else:
78 info[key] = get_value(value)
79 else:
80 # if the line isn't splittable, append it to the "__raw__" key
81 info.setdefault("__raw__", []).append(line)
82
83 return info
84
85
86def parse_memory_stats(response, **kwargs):
87 """Parse the results of MEMORY STATS"""
88 stats = pairs_to_dict(response, decode_keys=True, decode_string_values=True)
89 for key, value in stats.items():
90 if key.startswith("db.") and isinstance(value, list):
91 stats[key] = pairs_to_dict(
92 value, decode_keys=True, decode_string_values=True
93 )
94 return stats
95
96
97SENTINEL_STATE_TYPES = {
98 "can-failover-its-master": int,
99 "config-epoch": int,
100 "down-after-milliseconds": int,
101 "failover-timeout": int,
102 "info-refresh": int,
103 "last-hello-message": int,
104 "last-ok-ping-reply": int,
105 "last-ping-reply": int,
106 "last-ping-sent": int,
107 "master-link-down-time": int,
108 "master-port": int,
109 "num-other-sentinels": int,
110 "num-slaves": int,
111 "o-down-time": int,
112 "pending-commands": int,
113 "parallel-syncs": int,
114 "port": int,
115 "quorum": int,
116 "role-reported-time": int,
117 "s-down-time": int,
118 "slave-priority": int,
119 "slave-repl-offset": int,
120 "voted-leader-epoch": int,
121}
122
123
124def parse_sentinel_state(item):
125 result = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES)
126 flags = set(result["flags"].split(","))
127 for name, flag in (
128 ("is_master", "master"),
129 ("is_slave", "slave"),
130 ("is_sdown", "s_down"),
131 ("is_odown", "o_down"),
132 ("is_sentinel", "sentinel"),
133 ("is_disconnected", "disconnected"),
134 ("is_master_down", "master_down"),
135 ):
136 result[name] = flag in flags
137 return result
138
139
140def parse_sentinel_master(response):
141 return parse_sentinel_state(map(str_if_bytes, response))
142
143
144def parse_sentinel_state_resp3(response):
145 result = {}
146 for key in response:
147 try:
148 value = SENTINEL_STATE_TYPES[key](str_if_bytes(response[key]))
149 result[str_if_bytes(key)] = value
150 except Exception:
151 result[str_if_bytes(key)] = response[str_if_bytes(key)]
152 flags = set(result["flags"].split(","))
153 result["flags"] = flags
154 return result
155
156
157def parse_sentinel_masters(response):
158 result = {}
159 for item in response:
160 state = parse_sentinel_state(map(str_if_bytes, item))
161 result[state["name"]] = state
162 return result
163
164
165def parse_sentinel_masters_resp3(response):
166 return [parse_sentinel_state(master) for master in response]
167
168
169def parse_sentinel_slaves_and_sentinels(response):
170 return [parse_sentinel_state(map(str_if_bytes, item)) for item in response]
171
172
173def parse_sentinel_slaves_and_sentinels_resp3(response):
174 return [parse_sentinel_state_resp3(item) for item in response]
175
176
177def parse_sentinel_get_master(response):
178 return response and (response[0], int(response[1])) or None
179
180
181def pairs_to_dict(response, decode_keys=False, decode_string_values=False):
182 """Create a dict given a list of key/value pairs"""
183 if response is None:
184 return {}
185 if decode_keys or decode_string_values:
186 # the iter form is faster, but I don't know how to make that work
187 # with a str_if_bytes() map
188 keys = response[::2]
189 if decode_keys:
190 keys = map(str_if_bytes, keys)
191 values = response[1::2]
192 if decode_string_values:
193 values = map(str_if_bytes, values)
194 return dict(zip(keys, values))
195 else:
196 it = iter(response)
197 return dict(zip(it, it))
198
199
200def pairs_to_dict_typed(response, type_info):
201 it = iter(response)
202 result = {}
203 for key, value in zip(it, it):
204 if key in type_info:
205 try:
206 value = type_info[key](value)
207 except Exception:
208 # if for some reason the value can't be coerced, just use
209 # the string value
210 pass
211 result[key] = value
212 return result
213
214
215def zset_score_pairs(response, **options):
216 """
217 If ``withscores`` is specified in the options, return the response as
218 a list of (value, score) pairs
219 """
220 if not response or not options.get("withscores"):
221 return response
222 score_cast_func = options.get("score_cast_func", float)
223 it = iter(response)
224 return list(zip(it, map(score_cast_func, it)))
225
226
227def zset_score_for_rank(response, **options):
228 """
229 If ``withscores`` is specified in the options, return the response as
230 a [value, score] pair
231 """
232 if not response or not options.get("withscore"):
233 return response
234 score_cast_func = options.get("score_cast_func", float)
235 return [response[0], score_cast_func(response[1])]
236
237
238def zset_score_pairs_resp3(response, **options):
239 """
240 If ``withscores`` is specified in the options, return the response as
241 a list of [value, score] pairs
242 """
243 if not response or not options.get("withscores"):
244 return response
245 score_cast_func = options.get("score_cast_func", float)
246 return [[name, score_cast_func(val)] for name, val in response]
247
248
249def zset_score_for_rank_resp3(response, **options):
250 """
251 If ``withscores`` is specified in the options, return the response as
252 a [value, score] pair
253 """
254 if not response or not options.get("withscore"):
255 return response
256 score_cast_func = options.get("score_cast_func", float)
257 return [response[0], score_cast_func(response[1])]
258
259
260def sort_return_tuples(response, **options):
261 """
262 If ``groups`` is specified, return the response as a list of
263 n-element tuples with n being the value found in options['groups']
264 """
265 if not response or not options.get("groups"):
266 return response
267 n = options["groups"]
268 return list(zip(*[response[i::n] for i in range(n)]))
269
270
271def parse_stream_list(response):
272 if response is None:
273 return None
274 data = []
275 for r in response:
276 if r is not None:
277 data.append((r[0], pairs_to_dict(r[1])))
278 else:
279 data.append((None, None))
280 return data
281
282
283def pairs_to_dict_with_str_keys(response):
284 return pairs_to_dict(response, decode_keys=True)
285
286
287def parse_list_of_dicts(response):
288 return list(map(pairs_to_dict_with_str_keys, response))
289
290
291def parse_xclaim(response, **options):
292 if options.get("parse_justid", False):
293 return response
294 return parse_stream_list(response)
295
296
297def parse_xautoclaim(response, **options):
298 if options.get("parse_justid", False):
299 return response[1]
300 response[1] = parse_stream_list(response[1])
301 return response
302
303
304def parse_xinfo_stream(response, **options):
305 if isinstance(response, list):
306 data = pairs_to_dict(response, decode_keys=True)
307 else:
308 data = {str_if_bytes(k): v for k, v in response.items()}
309 if not options.get("full", False):
310 first = data.get("first-entry")
311 if first is not None and first[0] is not None:
312 data["first-entry"] = (first[0], pairs_to_dict(first[1]))
313 last = data["last-entry"]
314 if last is not None and last[0] is not None:
315 data["last-entry"] = (last[0], pairs_to_dict(last[1]))
316 else:
317 data["entries"] = {_id: pairs_to_dict(entry) for _id, entry in data["entries"]}
318 if len(data["groups"]) > 0 and isinstance(data["groups"][0], list):
319 data["groups"] = [
320 pairs_to_dict(group, decode_keys=True) for group in data["groups"]
321 ]
322 for g in data["groups"]:
323 if g["consumers"] and g["consumers"][0] is not None:
324 g["consumers"] = [
325 pairs_to_dict(c, decode_keys=True) for c in g["consumers"]
326 ]
327 else:
328 data["groups"] = [
329 {str_if_bytes(k): v for k, v in group.items()}
330 for group in data["groups"]
331 ]
332 return data
333
334
335def parse_xread(response):
336 if response is None:
337 return []
338 return [[r[0], parse_stream_list(r[1])] for r in response]
339
340
341def parse_xread_resp3(response):
342 if response is None:
343 return {}
344 return {key: [parse_stream_list(value)] for key, value in response.items()}
345
346
347def parse_xpending(response, **options):
348 if options.get("parse_detail", False):
349 return parse_xpending_range(response)
350 consumers = [{"name": n, "pending": int(p)} for n, p in response[3] or []]
351 return {
352 "pending": response[0],
353 "min": response[1],
354 "max": response[2],
355 "consumers": consumers,
356 }
357
358
359def parse_xpending_range(response):
360 k = ("message_id", "consumer", "time_since_delivered", "times_delivered")
361 return [dict(zip(k, r)) for r in response]
362
363
364def float_or_none(response):
365 if response is None:
366 return None
367 return float(response)
368
369
370def bool_ok(response, **options):
371 return str_if_bytes(response) == "OK"
372
373
374def parse_zadd(response, **options):
375 if response is None:
376 return None
377 if options.get("as_score"):
378 return float(response)
379 return int(response)
380
381
382def parse_client_list(response, **options):
383 clients = []
384 for c in str_if_bytes(response).splitlines():
385 client_dict = {}
386 tokens = c.split(" ")
387 last_key = None
388 for token in tokens:
389 if "=" in token:
390 # Values might contain '='
391 key, value = token.split("=", 1)
392 client_dict[key] = value
393 last_key = key
394 else:
395 # Values may include spaces. For instance, when running Redis via a Unix socket — such as
396 # "/tmp/redis sock/redis.sock" — the addr or laddr field will include a space.
397 client_dict[last_key] += " " + token
398
399 if client_dict:
400 clients.append(client_dict)
401 return clients
402
403
404def parse_config_get(response, **options):
405 response = [str_if_bytes(i) if i is not None else None for i in response]
406 return response and pairs_to_dict(response) or {}
407
408
409def parse_scan(response, **options):
410 cursor, r = response
411 return int(cursor), r
412
413
414def parse_hscan(response, **options):
415 cursor, r = response
416 no_values = options.get("no_values", False)
417 if no_values:
418 payload = r or []
419 else:
420 payload = r and pairs_to_dict(r) or {}
421 return int(cursor), payload
422
423
424def parse_zscan(response, **options):
425 score_cast_func = options.get("score_cast_func", float)
426 cursor, r = response
427 it = iter(r)
428 return int(cursor), list(zip(it, map(score_cast_func, it)))
429
430
431def parse_zmscore(response, **options):
432 # zmscore: list of scores (double precision floating point number) or nil
433 return [float(score) if score is not None else None for score in response]
434
435
436def parse_slowlog_get(response, **options):
437 space = " " if options.get("decode_responses", False) else b" "
438
439 def parse_item(item):
440 result = {"id": item[0], "start_time": int(item[1]), "duration": int(item[2])}
441 # Redis Enterprise injects another entry at index [3], which has
442 # the complexity info (i.e. the value N in case the command has
443 # an O(N) complexity) instead of the command.
444 if isinstance(item[3], list):
445 result["command"] = space.join(item[3])
446
447 # These fields are optional, depends on environment.
448 if len(item) >= 6:
449 result["client_address"] = item[4]
450 result["client_name"] = item[5]
451 else:
452 result["complexity"] = item[3]
453 result["command"] = space.join(item[4])
454
455 # These fields are optional, depends on environment.
456 if len(item) >= 7:
457 result["client_address"] = item[5]
458 result["client_name"] = item[6]
459
460 return result
461
462 return [parse_item(item) for item in response]
463
464
465def parse_stralgo(response, **options):
466 """
467 Parse the response from `STRALGO` command.
468 Without modifiers the returned value is string.
469 When LEN is given the command returns the length of the result
470 (i.e integer).
471 When IDX is given the command returns a dictionary with the LCS
472 length and all the ranges in both the strings, start and end
473 offset for each string, where there are matches.
474 When WITHMATCHLEN is given, each array representing a match will
475 also have the length of the match at the beginning of the array.
476 """
477 if options.get("len", False):
478 return int(response)
479 if options.get("idx", False):
480 if options.get("withmatchlen", False):
481 matches = [
482 [(int(match[-1]))] + list(map(tuple, match[:-1]))
483 for match in response[1]
484 ]
485 else:
486 matches = [list(map(tuple, match)) for match in response[1]]
487 return {
488 str_if_bytes(response[0]): matches,
489 str_if_bytes(response[2]): int(response[3]),
490 }
491 return str_if_bytes(response)
492
493
494def parse_cluster_info(response, **options):
495 response = str_if_bytes(response)
496 return dict(line.split(":") for line in response.splitlines() if line)
497
498
499def _parse_node_line(line):
500 line_items = line.split(" ")
501 node_id, addr, flags, master_id, ping, pong, epoch, connected = line.split(" ")[:8]
502 ip = addr.split("@")[0]
503 hostname = addr.split("@")[1].split(",")[1] if "@" in addr and "," in addr else ""
504 node_dict = {
505 "node_id": node_id,
506 "hostname": hostname,
507 "flags": flags,
508 "master_id": master_id,
509 "last_ping_sent": ping,
510 "last_pong_rcvd": pong,
511 "epoch": epoch,
512 "slots": [],
513 "migrations": [],
514 "connected": True if connected == "connected" else False,
515 }
516 if len(line_items) >= 9:
517 slots, migrations = _parse_slots(line_items[8:])
518 node_dict["slots"], node_dict["migrations"] = slots, migrations
519 return ip, node_dict
520
521
522def _parse_slots(slot_ranges):
523 slots, migrations = [], []
524 for s_range in slot_ranges:
525 if "->-" in s_range:
526 slot_id, dst_node_id = s_range[1:-1].split("->-", 1)
527 migrations.append(
528 {"slot": slot_id, "node_id": dst_node_id, "state": "migrating"}
529 )
530 elif "-<-" in s_range:
531 slot_id, src_node_id = s_range[1:-1].split("-<-", 1)
532 migrations.append(
533 {"slot": slot_id, "node_id": src_node_id, "state": "importing"}
534 )
535 else:
536 s_range = [sl for sl in s_range.split("-")]
537 slots.append(s_range)
538
539 return slots, migrations
540
541
542def parse_cluster_nodes(response, **options):
543 """
544 @see: https://redis.io/commands/cluster-nodes # string / bytes
545 @see: https://redis.io/commands/cluster-replicas # list of string / bytes
546 """
547 if isinstance(response, (str, bytes)):
548 response = response.splitlines()
549 return dict(_parse_node_line(str_if_bytes(node)) for node in response)
550
551
552def parse_geosearch_generic(response, **options):
553 """
554 Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER'
555 commands according to 'withdist', 'withhash' and 'withcoord' labels.
556 """
557 try:
558 if options["store"] or options["store_dist"]:
559 # `store` and `store_dist` cant be combined
560 # with other command arguments.
561 # relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER'
562 return response
563 except KeyError: # it means the command was sent via execute_command
564 return response
565
566 if not isinstance(response, list):
567 response_list = [response]
568 else:
569 response_list = response
570
571 if not options["withdist"] and not options["withcoord"] and not options["withhash"]:
572 # just a bunch of places
573 return response_list
574
575 cast = {
576 "withdist": float,
577 "withcoord": lambda ll: (float(ll[0]), float(ll[1])),
578 "withhash": int,
579 }
580
581 # zip all output results with each casting function to get
582 # the properly native Python value.
583 f = [lambda x: x]
584 f += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]]
585 return [list(map(lambda fv: fv[0](fv[1]), zip(f, r))) for r in response_list]
586
587
588def parse_command(response, **options):
589 commands = {}
590 for command in response:
591 cmd_dict = {}
592 cmd_name = str_if_bytes(command[0])
593 cmd_dict["name"] = cmd_name
594 cmd_dict["arity"] = int(command[1])
595 cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]]
596 cmd_dict["first_key_pos"] = command[3]
597 cmd_dict["last_key_pos"] = command[4]
598 cmd_dict["step_count"] = command[5]
599 if len(command) > 7:
600 cmd_dict["tips"] = command[7]
601 cmd_dict["key_specifications"] = command[8]
602 cmd_dict["subcommands"] = command[9]
603 commands[cmd_name] = cmd_dict
604 return commands
605
606
607def parse_command_resp3(response, **options):
608 commands = {}
609 for command in response:
610 cmd_dict = {}
611 cmd_name = str_if_bytes(command[0])
612 cmd_dict["name"] = cmd_name
613 cmd_dict["arity"] = command[1]
614 cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]}
615 cmd_dict["first_key_pos"] = command[3]
616 cmd_dict["last_key_pos"] = command[4]
617 cmd_dict["step_count"] = command[5]
618 cmd_dict["acl_categories"] = command[6]
619 if len(command) > 7:
620 cmd_dict["tips"] = command[7]
621 cmd_dict["key_specifications"] = command[8]
622 cmd_dict["subcommands"] = command[9]
623
624 commands[cmd_name] = cmd_dict
625 return commands
626
627
628def parse_pubsub_numsub(response, **options):
629 return list(zip(response[0::2], response[1::2]))
630
631
632def parse_client_kill(response, **options):
633 if isinstance(response, int):
634 return response
635 return str_if_bytes(response) == "OK"
636
637
638def parse_acl_getuser(response, **options):
639 if response is None:
640 return None
641 if isinstance(response, list):
642 data = pairs_to_dict(response, decode_keys=True)
643 else:
644 data = {str_if_bytes(key): value for key, value in response.items()}
645
646 # convert everything but user-defined data in 'keys' to native strings
647 data["flags"] = list(map(str_if_bytes, data["flags"]))
648 data["passwords"] = list(map(str_if_bytes, data["passwords"]))
649 data["commands"] = str_if_bytes(data["commands"])
650 if isinstance(data["keys"], str) or isinstance(data["keys"], bytes):
651 data["keys"] = list(str_if_bytes(data["keys"]).split(" "))
652 if data["keys"] == [""]:
653 data["keys"] = []
654 if "channels" in data:
655 if isinstance(data["channels"], str) or isinstance(data["channels"], bytes):
656 data["channels"] = list(str_if_bytes(data["channels"]).split(" "))
657 if data["channels"] == [""]:
658 data["channels"] = []
659 if "selectors" in data:
660 if data["selectors"] != [] and isinstance(data["selectors"][0], list):
661 data["selectors"] = [
662 list(map(str_if_bytes, selector)) for selector in data["selectors"]
663 ]
664 elif data["selectors"] != []:
665 data["selectors"] = [
666 {str_if_bytes(k): str_if_bytes(v) for k, v in selector.items()}
667 for selector in data["selectors"]
668 ]
669
670 # split 'commands' into separate 'categories' and 'commands' lists
671 commands, categories = [], []
672 for command in data["commands"].split(" "):
673 categories.append(command) if "@" in command else commands.append(command)
674
675 data["commands"] = commands
676 data["categories"] = categories
677 data["enabled"] = "on" in data["flags"]
678 return data
679
680
681def parse_acl_log(response, **options):
682 if response is None:
683 return None
684 if isinstance(response, list):
685 data = []
686 for log in response:
687 log_data = pairs_to_dict(log, True, True)
688 client_info = log_data.get("client-info", "")
689 log_data["client-info"] = parse_client_info(client_info)
690
691 # float() is lossy comparing to the "double" in C
692 log_data["age-seconds"] = float(log_data["age-seconds"])
693 data.append(log_data)
694 else:
695 data = bool_ok(response)
696 return data
697
698
699def parse_client_info(value):
700 """
701 Parsing client-info in ACL Log in following format.
702 "key1=value1 key2=value2 key3=value3"
703 """
704 client_info = {}
705 for info in str_if_bytes(value).strip().split():
706 key, value = info.split("=")
707 client_info[key] = value
708
709 # Those fields are defined as int in networking.c
710 for int_key in {
711 "id",
712 "age",
713 "idle",
714 "db",
715 "sub",
716 "psub",
717 "multi",
718 "qbuf",
719 "qbuf-free",
720 "obl",
721 "argv-mem",
722 "oll",
723 "omem",
724 "tot-mem",
725 }:
726 if int_key in client_info:
727 client_info[int_key] = int(client_info[int_key])
728 return client_info
729
730
731def parse_set_result(response, **options):
732 """
733 Handle SET result since GET argument is available since Redis 6.2.
734 Parsing SET result into:
735 - BOOL
736 - String when GET argument is used
737 """
738 if options.get("get"):
739 # Redis will return a getCommand result.
740 # See `setGenericCommand` in t_string.c
741 return response
742 return response and str_if_bytes(response) == "OK"
743
744
745def string_keys_to_dict(key_string, callback):
746 return dict.fromkeys(key_string.split(), callback)
747
748
749_RedisCallbacks = {
750 **string_keys_to_dict(
751 "AUTH COPY EXPIRE EXPIREAT HEXISTS HMSET MOVE MSETNX PERSIST PSETEX "
752 "PEXPIRE PEXPIREAT RENAMENX SETEX SETNX SMOVE",
753 bool,
754 ),
755 **string_keys_to_dict("HINCRBYFLOAT INCRBYFLOAT", float),
756 **string_keys_to_dict(
757 "ASKING FLUSHALL FLUSHDB LSET LTRIM MSET PFMERGE READONLY READWRITE "
758 "RENAME SAVE SELECT SHUTDOWN SLAVEOF SWAPDB WATCH UNWATCH",
759 bool_ok,
760 ),
761 **string_keys_to_dict("XREAD XREADGROUP", parse_xread),
762 **string_keys_to_dict(
763 "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH",
764 parse_geosearch_generic,
765 ),
766 **string_keys_to_dict("XRANGE XREVRANGE", parse_stream_list),
767 "ACL GETUSER": parse_acl_getuser,
768 "ACL LOAD": bool_ok,
769 "ACL LOG": parse_acl_log,
770 "ACL SETUSER": bool_ok,
771 "ACL SAVE": bool_ok,
772 "CLIENT INFO": parse_client_info,
773 "CLIENT KILL": parse_client_kill,
774 "CLIENT LIST": parse_client_list,
775 "CLIENT PAUSE": bool_ok,
776 "CLIENT SETINFO": bool_ok,
777 "CLIENT SETNAME": bool_ok,
778 "CLIENT UNBLOCK": bool,
779 "CLUSTER ADDSLOTS": bool_ok,
780 "CLUSTER ADDSLOTSRANGE": bool_ok,
781 "CLUSTER DELSLOTS": bool_ok,
782 "CLUSTER DELSLOTSRANGE": bool_ok,
783 "CLUSTER FAILOVER": bool_ok,
784 "CLUSTER FORGET": bool_ok,
785 "CLUSTER INFO": parse_cluster_info,
786 "CLUSTER MEET": bool_ok,
787 "CLUSTER NODES": parse_cluster_nodes,
788 "CLUSTER REPLICAS": parse_cluster_nodes,
789 "CLUSTER REPLICATE": bool_ok,
790 "CLUSTER RESET": bool_ok,
791 "CLUSTER SAVECONFIG": bool_ok,
792 "CLUSTER SET-CONFIG-EPOCH": bool_ok,
793 "CLUSTER SETSLOT": bool_ok,
794 "CLUSTER SLAVES": parse_cluster_nodes,
795 "COMMAND": parse_command,
796 "CONFIG RESETSTAT": bool_ok,
797 "CONFIG SET": bool_ok,
798 "FUNCTION DELETE": bool_ok,
799 "FUNCTION FLUSH": bool_ok,
800 "FUNCTION RESTORE": bool_ok,
801 "GEODIST": float_or_none,
802 "HSCAN": parse_hscan,
803 "INFO": parse_info,
804 "LASTSAVE": timestamp_to_datetime,
805 "MEMORY PURGE": bool_ok,
806 "MODULE LOAD": bool,
807 "MODULE UNLOAD": bool,
808 "PING": lambda r: str_if_bytes(r) == "PONG",
809 "PUBSUB NUMSUB": parse_pubsub_numsub,
810 "PUBSUB SHARDNUMSUB": parse_pubsub_numsub,
811 "QUIT": bool_ok,
812 "SET": parse_set_result,
813 "SCAN": parse_scan,
814 "SCRIPT EXISTS": lambda r: list(map(bool, r)),
815 "SCRIPT FLUSH": bool_ok,
816 "SCRIPT KILL": bool_ok,
817 "SCRIPT LOAD": str_if_bytes,
818 "SENTINEL CKQUORUM": bool_ok,
819 "SENTINEL FAILOVER": bool_ok,
820 "SENTINEL FLUSHCONFIG": bool_ok,
821 "SENTINEL GET-MASTER-ADDR-BY-NAME": parse_sentinel_get_master,
822 "SENTINEL MONITOR": bool_ok,
823 "SENTINEL RESET": bool_ok,
824 "SENTINEL REMOVE": bool_ok,
825 "SENTINEL SET": bool_ok,
826 "SLOWLOG GET": parse_slowlog_get,
827 "SLOWLOG RESET": bool_ok,
828 "SORT": sort_return_tuples,
829 "SSCAN": parse_scan,
830 "TIME": lambda x: (int(x[0]), int(x[1])),
831 "XAUTOCLAIM": parse_xautoclaim,
832 "XCLAIM": parse_xclaim,
833 "XGROUP CREATE": bool_ok,
834 "XGROUP DESTROY": bool,
835 "XGROUP SETID": bool_ok,
836 "XINFO STREAM": parse_xinfo_stream,
837 "XPENDING": parse_xpending,
838 "ZSCAN": parse_zscan,
839}
840
841
842_RedisCallbacksRESP2 = {
843 **string_keys_to_dict(
844 "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
845 ),
846 **string_keys_to_dict(
847 "ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZREVRANGE "
848 "ZREVRANGEBYSCORE ZUNION",
849 zset_score_pairs,
850 ),
851 **string_keys_to_dict(
852 "ZREVRANK ZRANK",
853 zset_score_for_rank,
854 ),
855 **string_keys_to_dict("ZINCRBY ZSCORE", float_or_none),
856 **string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True),
857 **string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None),
858 **string_keys_to_dict(
859 "BZPOPMAX BZPOPMIN", lambda r: r and (r[0], r[1], float(r[2])) or None
860 ),
861 "ACL CAT": lambda r: list(map(str_if_bytes, r)),
862 "ACL GENPASS": str_if_bytes,
863 "ACL HELP": lambda r: list(map(str_if_bytes, r)),
864 "ACL LIST": lambda r: list(map(str_if_bytes, r)),
865 "ACL USERS": lambda r: list(map(str_if_bytes, r)),
866 "ACL WHOAMI": str_if_bytes,
867 "CLIENT GETNAME": str_if_bytes,
868 "CLIENT TRACKINGINFO": lambda r: list(map(str_if_bytes, r)),
869 "CLUSTER GETKEYSINSLOT": lambda r: list(map(str_if_bytes, r)),
870 "COMMAND GETKEYS": lambda r: list(map(str_if_bytes, r)),
871 "CONFIG GET": parse_config_get,
872 "DEBUG OBJECT": parse_debug_object,
873 "GEOHASH": lambda r: list(map(str_if_bytes, r)),
874 "GEOPOS": lambda r: list(
875 map(lambda ll: (float(ll[0]), float(ll[1])) if ll is not None else None, r)
876 ),
877 "HGETALL": lambda r: r and pairs_to_dict(r) or {},
878 "MEMORY STATS": parse_memory_stats,
879 "MODULE LIST": lambda r: [pairs_to_dict(m) for m in r],
880 "RESET": str_if_bytes,
881 "SENTINEL MASTER": parse_sentinel_master,
882 "SENTINEL MASTERS": parse_sentinel_masters,
883 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels,
884 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels,
885 "STRALGO": parse_stralgo,
886 "XINFO CONSUMERS": parse_list_of_dicts,
887 "XINFO GROUPS": parse_list_of_dicts,
888 "ZADD": parse_zadd,
889 "ZMSCORE": parse_zmscore,
890}
891
892
893_RedisCallbacksRESP3 = {
894 **string_keys_to_dict(
895 "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
896 ),
897 **string_keys_to_dict(
898 "ZRANGE ZINTER ZPOPMAX ZPOPMIN HGETALL XREADGROUP",
899 lambda r, **kwargs: r,
900 ),
901 **string_keys_to_dict(
902 "ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
903 zset_score_pairs_resp3,
904 ),
905 **string_keys_to_dict(
906 "ZREVRANK ZRANK",
907 zset_score_for_rank_resp3,
908 ),
909 **string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3),
910 "ACL LOG": lambda r: (
911 [
912 {str_if_bytes(key): str_if_bytes(value) for key, value in x.items()}
913 for x in r
914 ]
915 if isinstance(r, list)
916 else bool_ok(r)
917 ),
918 "COMMAND": parse_command_resp3,
919 "CONFIG GET": lambda r: {
920 str_if_bytes(key) if key is not None else None: (
921 str_if_bytes(value) if value is not None else None
922 )
923 for key, value in r.items()
924 },
925 "MEMORY STATS": lambda r: {str_if_bytes(key): value for key, value in r.items()},
926 "SENTINEL MASTER": parse_sentinel_state_resp3,
927 "SENTINEL MASTERS": parse_sentinel_masters_resp3,
928 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3,
929 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3,
930 "STRALGO": lambda r, **options: (
931 {str_if_bytes(key): str_if_bytes(value) for key, value in r.items()}
932 if isinstance(r, dict)
933 else str_if_bytes(r)
934 ),
935 "XINFO CONSUMERS": lambda r: [
936 {str_if_bytes(key): value for key, value in x.items()} for x in r
937 ],
938 "XINFO GROUPS": lambda r: [
939 {str_if_bytes(key): value for key, value in d.items()} for d in r
940 ],
941}