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
97def parse_memory_stats_unified(response, **kwargs):
98 """Parse MEMORY STATS for unified RESP2 output.
99
100 Unified responses decode structural keys while preserving string-like
101 values as delivered, matching the approved RESP2/RESP3 unification shape.
102 """
103 stats = pairs_to_dict(response, decode_keys=True)
104 for key, value in stats.items():
105 if key.startswith("db.") and isinstance(value, list):
106 stats[key] = pairs_to_dict(value, decode_keys=True)
107 return stats
108
109
110def parse_memory_stats_resp3(response, **kwargs):
111 """Parse the results of MEMORY STATS on RESP3 wire.
112
113 Each entry arrives as a top-level ``dict`` instead of a flat list of
114 pairs; decode the keys to ``str`` and recurse into the per-database
115 ``db.*`` sub-dicts so the Python shape matches what
116 :func:`parse_memory_stats` produces from RESP2 wire.
117 """
118 stats = {str_if_bytes(key): value for key, value in response.items()}
119 for key, value in stats.items():
120 if key.startswith("db.") and isinstance(value, dict):
121 stats[key] = {str_if_bytes(k): v for k, v in value.items()}
122 return stats
123
124
125def parse_list_of_dicts_resp3(response, **kwargs):
126 """Parse list-of-maps responses on RESP3 wire (e.g. ``XINFO`` family).
127
128 Each list entry arrives as a ``dict`` with bytes keys; decode the
129 keys to ``str`` so the Python shape matches what
130 :func:`parse_list_of_dicts` produces from RESP2 wire.
131 """
132 return [{str_if_bytes(key): value for key, value in x.items()} for x in response]
133
134
135SENTINEL_STATE_TYPES = {
136 "can-failover-its-master": int,
137 "config-epoch": int,
138 "down-after-milliseconds": int,
139 "failover-timeout": int,
140 "info-refresh": int,
141 "last-hello-message": int,
142 "last-ok-ping-reply": int,
143 "last-ping-reply": int,
144 "last-ping-sent": int,
145 "master-link-down-time": int,
146 "master-port": int,
147 "num-other-sentinels": int,
148 "num-slaves": int,
149 "o-down-time": int,
150 "pending-commands": int,
151 "parallel-syncs": int,
152 "port": int,
153 "quorum": int,
154 "role-reported-time": int,
155 "s-down-time": int,
156 "slave-priority": int,
157 "slave-repl-offset": int,
158 "voted-leader-epoch": int,
159}
160
161
162_SENTINEL_DERIVED_BOOLEANS = (
163 ("is_master", "master"),
164 ("is_slave", "slave"),
165 ("is_sdown", "s_down"),
166 ("is_odown", "o_down"),
167 ("is_sentinel", "sentinel"),
168 ("is_disconnected", "disconnected"),
169 ("is_master_down", "master_down"),
170)
171
172
173def _add_derived_sentinel_booleans(result, flags):
174 """Set ``is_master`` / ``is_slave`` / ``is_sdown`` / ``is_odown`` /
175 ``is_sentinel`` / ``is_disconnected`` / ``is_master_down`` on
176 ``result`` based on membership in the ``flags`` set.
177 """
178 for name, flag in _SENTINEL_DERIVED_BOOLEANS:
179 result[name] = flag in flags
180
181
182def parse_sentinel_state(item):
183 result = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES)
184 flags = set(result["flags"].split(","))
185 _add_derived_sentinel_booleans(result, flags)
186 return result
187
188
189def parse_sentinel_master(response, **options):
190 return parse_sentinel_state(map(str_if_bytes, response))
191
192
193def parse_sentinel_state_resp3(response, **options):
194 result = {}
195 for key in response:
196 str_key = str_if_bytes(key)
197 try:
198 value = SENTINEL_STATE_TYPES[str_key](str_if_bytes(response[key]))
199 result[str_key] = value
200 except Exception:
201 result[str_key] = str_if_bytes(response[key])
202 flags = set(result["flags"].split(","))
203 result["flags"] = flags
204 _add_derived_sentinel_booleans(result, flags)
205 return result
206
207
208def parse_sentinel_masters(response, **options):
209 result = {}
210 for item in response:
211 state = parse_sentinel_state(map(str_if_bytes, item))
212 result[state["name"]] = state
213 return result
214
215
216def parse_sentinel_masters_resp3(response, **options):
217 result = {}
218 for master in response:
219 state = parse_sentinel_state_resp3(master)
220 result[state["name"]] = state
221 return result
222
223
224def parse_sentinel_slaves_and_sentinels(response, **options):
225 return [parse_sentinel_state(map(str_if_bytes, item)) for item in response]
226
227
228def parse_sentinel_slaves_and_sentinels_resp3(response, **options):
229 return [parse_sentinel_state_resp3(item, **options) for item in response]
230
231
232def _flatten_resp3_state_pairs(state):
233 """Yield key/value pairs from a RESP3 sentinel-state map as a flat
234 iterable suitable for ``parse_sentinel_state``.
235 """
236 for key, value in state.items():
237 yield key
238 yield value
239
240
241def parse_sentinel_master_resp3_to_resp2_legacy(response, **options):
242 return parse_sentinel_state(map(str_if_bytes, _flatten_resp3_state_pairs(response)))
243
244
245def parse_sentinel_masters_resp3_to_resp2_legacy(response, **options):
246 result = {}
247 for master in response:
248 state = parse_sentinel_state(
249 map(str_if_bytes, _flatten_resp3_state_pairs(master))
250 )
251 result[state["name"]] = state
252 return result
253
254
255def parse_sentinel_slaves_and_sentinels_resp3_to_resp2_legacy(response, **options):
256 return [
257 parse_sentinel_state(map(str_if_bytes, _flatten_resp3_state_pairs(item)))
258 for item in response
259 ]
260
261
262def parse_sentinel_master_unified(response, **options):
263 state = parse_sentinel_state(map(str_if_bytes, response))
264 state["flags"] = set(state["flags"].split(","))
265 return state
266
267
268def parse_sentinel_masters_unified(response, **options):
269 result = {}
270 for item in response:
271 state = parse_sentinel_state(map(str_if_bytes, item))
272 state["flags"] = set(state["flags"].split(","))
273 result[state["name"]] = state
274 return result
275
276
277def parse_sentinel_slaves_and_sentinels_unified(response, **options):
278 out = []
279 for item in response:
280 state = parse_sentinel_state(map(str_if_bytes, item))
281 state["flags"] = set(state["flags"].split(","))
282 out.append(state)
283 return out
284
285
286def parse_sentinel_master_unified_resp3(response, **options):
287 state = parse_sentinel_state_resp3(response, **options)
288 _add_derived_sentinel_booleans(state, state["flags"])
289 return state
290
291
292def parse_sentinel_masters_unified_resp3(response, **options):
293 result = {}
294 for master in response:
295 state = parse_sentinel_state_resp3(master)
296 _add_derived_sentinel_booleans(state, state["flags"])
297 result[state["name"]] = state
298 return result
299
300
301def parse_sentinel_slaves_and_sentinels_unified_resp3(response, **options):
302 out = []
303 for item in response:
304 state = parse_sentinel_state_resp3(item, **options)
305 _add_derived_sentinel_booleans(state, state["flags"])
306 out.append(state)
307 return out
308
309
310def parse_sentinel_get_master(response, **options):
311 return response and (response[0], int(response[1])) or None
312
313
314def pairs_to_dict(response, decode_keys=False, decode_string_values=False):
315 """Create a dict given a list of key/value pairs"""
316 if response is None:
317 return {}
318 if decode_keys or decode_string_values:
319 # the iter form is faster, but I don't know how to make that work
320 # with a str_if_bytes() map
321 keys = response[::2]
322 if decode_keys:
323 keys = map(str_if_bytes, keys)
324 values = response[1::2]
325 if decode_string_values:
326 values = map(str_if_bytes, values)
327 return dict(zip(keys, values))
328 else:
329 it = iter(response)
330 return dict(zip(it, it))
331
332
333def pairs_to_dict_typed(response, type_info):
334 it = iter(response)
335 result = {}
336 for key, value in zip(it, it):
337 if key in type_info:
338 try:
339 value = type_info[key](value)
340 except Exception:
341 # if for some reason the value can't be coerced, just use
342 # the string value
343 pass
344 result[key] = value
345 return result
346
347
348def _wrap_score_cast_func(score_cast_func):
349 """Wrap score_cast_func to handle scientific notation in RESP2 byte strings.
350
351 Redis returns scores as byte strings in RESP2, and large numbers may use
352 scientific notation (e.g., b'1.7732526297292595e+18'). Python's int() cannot
353 parse scientific notation directly. Rather than unconditionally routing
354 through float() (which would change the input type for every custom
355 callable), we try the original function first and only fall back to
356 converting through float() on ValueError.
357 """
358 if score_cast_func is float:
359 return score_cast_func
360
361 def _safe_cast(x):
362 try:
363 return score_cast_func(x)
364 except (ValueError, TypeError):
365 return score_cast_func(float(x))
366
367 return _safe_cast
368
369
370def zset_score_pairs(response, **options):
371 """
372 If ``withscores`` is specified in the options, return the response as
373 a list of (value, score) pairs
374 """
375 if not response or not options.get("withscores"):
376 return response
377 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
378 it = iter(response)
379 return list(zip(it, map(score_cast_func, it)))
380
381
382def zpop_score_pairs(response, **options):
383 """RESP2-wire ZPOPMAX/ZPOPMIN -> legacy ``list[(member, score), ...]``.
384
385 ZPOPMAX/ZPOPMIN always include scores, so this parser intentionally
386 does not depend on a ``withscores`` option.
387 """
388 if not response:
389 return response
390 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
391 it = iter(response)
392 return list(zip(it, map(score_cast_func, it)))
393
394
395def zset_score_for_rank(response, **options):
396 """
397 If ``withscores`` is specified in the options, return the response as
398 a [value, score] pair
399 """
400 if not response or not options.get("withscore"):
401 return response
402 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
403 return [response[0], score_cast_func(response[1])]
404
405
406def zset_score_pairs_resp3(response, **options):
407 """
408 If ``withscores`` is specified in the options, return the response as
409 a list of [value, score] pairs
410 """
411 if not response or not options.get("withscores"):
412 return response
413 score_cast_func = options.get("score_cast_func", float)
414 return [[name, score_cast_func(val)] for name, val in response]
415
416
417def zset_score_for_rank_resp3(response, **options):
418 """
419 If ``withscores`` is specified in the options, return the response as
420 a [value, score] pair
421 """
422 if not response or not options.get("withscore"):
423 return response
424 score_cast_func = options.get("score_cast_func", float)
425 return [response[0], score_cast_func(response[1])]
426
427
428def _score_to_resp2_bytes(value):
429 """Re-encode a score back to the bytes form Redis returns on the RESP2
430 wire so that custom ``score_cast_func`` callables observe the same
431 input type they would receive on a RESP2 connection when the wire
432 protocol is RESP3 but legacy response shapes are requested.
433 """
434 if isinstance(value, bytes):
435 return value
436 if isinstance(value, str):
437 return value.encode()
438 if isinstance(value, bool):
439 return b"1" if value else b"0"
440 return format(float(value), ".17g").encode()
441
442
443def zset_score_pairs_resp3_to_resp2_legacy(response, **options):
444 """Convert RESP3 nested ``[[member, score], ...]`` to today's RESP2
445 ``list[(member, score)]`` shape: tuples instead of lists, scores
446 re-encoded to bytes before being passed to ``score_cast_func`` so the
447 cast receives the same input as on a RESP2 connection.
448 """
449 if not response or not options.get("withscores"):
450 return response
451 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
452 return [
453 (member, score_cast_func(_score_to_resp2_bytes(score)))
454 for member, score in response
455 ]
456
457
458def zset_score_pairs_resp3_to_resp2_legacy_flat(response, **options):
459 """Convert RESP3 nested ``[[member, score], ...]`` to the flat raw RESP2
460 wire shape ``[member, score_bytes, ...]`` used by ZDIFF in v8.0.0b1.
461
462 ZDIFF historically did not propagate ``withscores`` to the response
463 callback, so the legacy RESP2 callback was a no-op and the raw flat
464 wire response was returned to the user. This helper reproduces that
465 shape on RESP3 wires so ``legacy_responses=True`` keeps emitting the
466 same Python value regardless of the underlying protocol.
467 """
468 if not response or not options.get("withscores"):
469 return response
470 flat = []
471 for member, score in response:
472 flat.append(member)
473 flat.append(_score_to_resp2_bytes(score))
474 return flat
475
476
477def zset_score_for_rank_resp3_to_resp2_legacy(response, **options):
478 """RESP3-wire ZRANK/ZREVRANK WITHSCORE → legacy RESP2 ``[rank, score]``.
479
480 The shape ``[rank, score]`` is identical between RESP2 and RESP3; only
481 the score is re-encoded to bytes before being passed to
482 ``score_cast_func`` so the cast observes the same input type it would
483 on a RESP2 connection.
484 """
485 if not response or not options.get("withscore"):
486 return response
487 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
488 return [response[0], score_cast_func(_score_to_resp2_bytes(response[1]))]
489
490
491def zset_score_pairs_unified(response, **options):
492 """RESP2-wire WITHSCORES → unified ``list[[member, score], ...]``.
493
494 Normalises RESP2 byte-string scores through ``float`` before applying
495 ``score_cast_func`` so the cast receives the same input type as on a
496 RESP3 connection.
497 """
498 if not response or not options.get("withscores"):
499 return response
500 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
501 it = iter(response)
502 return [[val, score_cast_func(float(score))] for val, score in zip(it, it)]
503
504
505def zset_score_for_rank_unified(response, **options):
506 """RESP2-wire ZRANK/ZREVRANK WITHSCORE → unified ``[rank, score]``.
507
508 Normalises the RESP2 byte-string score through ``float`` before
509 applying ``score_cast_func`` so the cast receives the same input type
510 as on a RESP3 connection.
511 """
512 if not response or not options.get("withscore"):
513 return response
514 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
515 return [response[0], score_cast_func(float(response[1]))]
516
517
518def zpop_score_pairs_unified(response, **options):
519 """RESP2-wire ZPOPMAX/ZPOPMIN → unified ``list[[member, score], ...]``.
520
521 ZPOPMAX/ZPOPMIN always include scores; no ``withscores`` gate is
522 required. Scores are normalised through ``float`` before applying
523 ``score_cast_func`` for parity with RESP3.
524 """
525 if not response:
526 return response
527 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
528 it = iter(response)
529 return [[val, score_cast_func(float(score))] for val, score in zip(it, it)]
530
531
532def zpop_score_pairs_resp3_unified(response, **options):
533 """RESP3-wire ZPOPMAX/ZPOPMIN → unified ``list[[member, score], ...]``.
534
535 Without ``count`` RESP3 returns a flat ``[member, score]``; with
536 ``count`` it returns a nested ``[[member, score], ...]``. Both shapes
537 are normalised to a nested list with ``score_cast_func`` applied.
538 """
539 if not response:
540 return response
541 score_cast_func = options.get("score_cast_func", float)
542 if isinstance(response[0], list):
543 return [[name, score_cast_func(val)] for name, val in response]
544 return [[response[0], score_cast_func(response[1])]]
545
546
547def zpop_score_pairs_resp3_to_resp2_legacy(response, **options):
548 """RESP3-wire ZPOPMAX/ZPOPMIN → legacy RESP2 ``list[(member, score), ...]``.
549
550 Both RESP3 shapes (flat without ``count``; nested with ``count``) are
551 converted to a list of tuples. Scores are re-encoded to bytes before
552 being passed to ``score_cast_func`` so the cast observes the same
553 input type it would on a RESP2 connection.
554 """
555 if not response:
556 return response
557 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
558 if isinstance(response[0], list):
559 return [
560 (member, score_cast_func(_score_to_resp2_bytes(score)))
561 for member, score in response
562 ]
563 return [(response[0], score_cast_func(_score_to_resp2_bytes(response[1])))]
564
565
566def bzpop_score_unified(response, **options):
567 """BZPOPMAX/BZPOPMIN → unified ``[key, member, score]``.
568
569 Works for both RESP2 (bytes score) and RESP3 (float score) wire shapes.
570 """
571 if not response:
572 return None
573 return [response[0], response[1], float(response[2])]
574
575
576def bzpop_score_resp3_to_resp2_legacy(response, **options):
577 """RESP3-wire BZPOPMAX/BZPOPMIN → legacy RESP2 ``(key, member, score)``.
578
579 Matches the v8.0.0b1 RESP2-wire callback shape (tuple, ``float`` score).
580 """
581 if not response:
582 return None
583 return (response[0], response[1], float(response[2]))
584
585
586def zmpop_resp3_to_resp2_legacy(response, **options):
587 """RESP3-wire ZMPOP/BZMPOP → legacy RESP2 ``[name, [[member, b"score"], ...]]``.
588
589 Re-encodes RESP3 native float scores back to the bytes form Redis
590 returns on the RESP2 wire so callers observe today's RESP2 raw shape.
591 """
592 if not response:
593 return response
594 return [
595 response[0],
596 [[member, _score_to_resp2_bytes(score)] for member, score in response[1]],
597 ]
598
599
600def zmpop_unified(response, **options):
601 """ZMPOP/BZMPOP → unified ``[name, [[member, float_score], ...]]``.
602
603 Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
604 RESP3's native float-score shape.
605 """
606 if not response:
607 return response
608 return [
609 response[0],
610 [[member, float(score)] for member, score in response[1]],
611 ]
612
613
614def hrandfield_unified(response, **options):
615 """RESP2-wire HRANDFIELD WITHVALUES → unified ``list[[field, value], ...]``.
616
617 Plain (no-values) responses — flat list of fields — pass through.
618 The ``withvalues`` option (forwarded by the command method) selects the
619 pairing branch so the no-values flat result is never misread.
620 """
621 if not response or not options.get("withvalues"):
622 return response
623 if isinstance(response[0], list):
624 return response
625 it = iter(response)
626 return [[field, value] for field, value in zip(it, it)]
627
628
629def hrandfield_resp3_to_resp2_legacy(response, **options):
630 """RESP3-wire HRANDFIELD WITHVALUES → legacy RESP2 flat ``[field, value, ...]``.
631
632 Plain (no-values) responses — flat list of fields — pass through.
633 """
634 if not response or not options.get("withvalues"):
635 return response
636 if not isinstance(response[0], list):
637 return response
638 flat = []
639 for field, value in response:
640 flat.append(field)
641 flat.append(value)
642 return flat
643
644
645def parse_geopos_unified(response, **options):
646 """GEOPOS → unified ``list[list[float, float] | None]``.
647
648 Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
649 RESP3's native ``list[list]`` shape.
650 """
651 return [[float(ll[0]), float(ll[1])] if ll is not None else None for ll in response]
652
653
654def parse_geopos_resp3_to_resp2_legacy(response, **options):
655 """RESP3-wire GEOPOS → legacy RESP2 ``list[tuple(float, float) | None]``.
656
657 Matches today's RESP2-wire callback shape (tuple coordinates).
658 """
659 return [(float(ll[0]), float(ll[1])) if ll is not None else None for ll in response]
660
661
662def parse_lcs_idx_unified(response, **options):
663 """LCS with IDX → unified ``dict``.
664
665 Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
666 RESP3's native ``dict`` shape. Non-IDX responses (``bytes`` / ``int``)
667 pass through unchanged.
668 """
669 if isinstance(response, list):
670 it = iter(response)
671 return {str_if_bytes(key): value for key, value in zip(it, it)}
672 if isinstance(response, dict):
673 return {str_if_bytes(key): value for key, value in response.items()}
674 return response
675
676
677def parse_lcs_idx_resp3_to_resp2_legacy(response, **options):
678 """RESP3-wire LCS with IDX → legacy RESP2 flat list shape.
679
680 Reproduces today's RESP2 raw output (``[b"matches", [...], b"len", n]``).
681 Non-IDX responses pass through unchanged.
682 """
683 if not isinstance(response, dict):
684 return response
685 out: list = []
686 for key, value in response.items():
687 out.append(key)
688 out.append(value)
689 return out
690
691
692def parse_client_trackinginfo_unified(response, **options):
693 """CLIENT TRACKINGINFO → unified ``dict[str, Any]``.
694
695 Accepts either RESP2's flat ``[label, value, ...]`` list or RESP3's
696 native ``dict`` and returns a ``dict`` with ``str`` keys.
697 """
698 if isinstance(response, dict):
699 data = {str_if_bytes(key): value for key, value in response.items()}
700 else:
701 data = {
702 str_if_bytes(key): value
703 for key, value in zip(response[::2], response[1::2])
704 }
705 if "flags" in data:
706 data["flags"] = [str_if_bytes(flag) for flag in data["flags"]]
707 if "prefixes" in data:
708 data["prefixes"] = [str_if_bytes(prefix) for prefix in data["prefixes"]]
709 return data
710
711
712def parse_client_trackinginfo_resp3_to_resp2_legacy(response, **options):
713 """RESP3-wire CLIENT TRACKINGINFO → legacy RESP2 flat ``list``.
714
715 Mirrors today's RESP2-wire callback (``list(map(str_if_bytes, r))``):
716 labels are decoded to ``str`` while values are preserved as-is.
717 """
718 if not isinstance(response, dict):
719 return list(map(str_if_bytes, response))
720 out: list = []
721 for key, value in response.items():
722 out.append(str_if_bytes(key))
723 out.append(value)
724 return out
725
726
727def sort_return_tuples(response, **options):
728 """
729 If ``groups`` is specified, return the response as a list of
730 n-element tuples with n being the value found in options['groups']
731 """
732 if not response or not options.get("groups"):
733 return response
734 n = options["groups"]
735 return list(zip(*[response[i::n] for i in range(n)]))
736
737
738def parse_stream_list(response, **options):
739 if response is None:
740 return None
741 data = []
742 for r in response:
743 if r is not None:
744 if "claim_min_idle_time" in options:
745 data.append((r[0], pairs_to_dict(r[1]), *r[2:]))
746 else:
747 data.append((r[0], pairs_to_dict(r[1])))
748 else:
749 data.append((None, None))
750 return data
751
752
753def pairs_to_dict_with_str_keys(response):
754 return pairs_to_dict(response, decode_keys=True)
755
756
757def parse_list_of_dicts(response):
758 return list(map(pairs_to_dict_with_str_keys, response))
759
760
761def parse_xclaim(response, **options):
762 if options.get("parse_justid", False):
763 return response
764 return parse_stream_list(response)
765
766
767def parse_xautoclaim(response, **options):
768 if options.get("parse_justid", False):
769 return response[1]
770 response[1] = parse_stream_list(response[1])
771 return response
772
773
774def parse_arinfo(response, **options):
775 if isinstance(response, list):
776 return pairs_to_dict(response, decode_keys=True)
777 return {str_if_bytes(k): v for k, v in response.items()}
778
779
780def parse_xinfo_stream(response, **options):
781 if isinstance(response, list):
782 data = pairs_to_dict(response, decode_keys=True)
783 else:
784 data = {str_if_bytes(k): v for k, v in response.items()}
785 if not options.get("full", False):
786 first = data.get("first-entry")
787 if first is not None and first[0] is not None:
788 data["first-entry"] = (first[0], pairs_to_dict(first[1]))
789 last = data["last-entry"]
790 if last is not None and last[0] is not None:
791 data["last-entry"] = (last[0], pairs_to_dict(last[1]))
792 else:
793 data["entries"] = {_id: pairs_to_dict(entry) for _id, entry in data["entries"]}
794 if len(data["groups"]) > 0 and isinstance(data["groups"][0], list):
795 data["groups"] = [
796 pairs_to_dict(group, decode_keys=True) for group in data["groups"]
797 ]
798 for g in data["groups"]:
799 if g["consumers"] and g["consumers"][0] is not None:
800 g["consumers"] = [
801 pairs_to_dict(c, decode_keys=True) for c in g["consumers"]
802 ]
803 else:
804 data["groups"] = [
805 {str_if_bytes(k): v for k, v in group.items()}
806 for group in data["groups"]
807 ]
808 return data
809
810
811def parse_xread(response, **options):
812 if response is None:
813 return []
814 return [[r[0], parse_stream_list(r[1], **options)] for r in response]
815
816
817def parse_xread_resp3(response, **options):
818 if response is None:
819 return {}
820 return {
821 key: [parse_stream_list(value, **options)] for key, value in response.items()
822 }
823
824
825def parse_xread_unified(response, **options):
826 """XREAD/XREADGROUP → unified ``dict[stream, list[tuple[id, dict]]]``.
827
828 Accepts either RESP2 (``list[[stream, entries]]``) or RESP3
829 (``dict[stream, entries]``) wire shape. Empty result is ``{}``.
830 """
831 if not response:
832 return {}
833 if isinstance(response, dict):
834 return {
835 key: parse_stream_list(value, **options) for key, value in response.items()
836 }
837 return {
838 stream: parse_stream_list(entries, **options) for stream, entries in response
839 }
840
841
842def parse_xread_resp3_to_resp2_legacy(response, **options):
843 """RESP3-wire XREAD/XREADGROUP → legacy RESP2 ``list[[stream, entries]]``.
844
845 Empty result ``{}`` is converted to ``[]`` to match today's RESP2 shape.
846 """
847 if not response:
848 return []
849 return [
850 [key, parse_stream_list(value, **options)] for key, value in response.items()
851 ]
852
853
854def parse_xpending(response, **options):
855 if options.get("parse_detail", False):
856 return parse_xpending_range(response)
857 consumers = [{"name": n, "pending": int(p)} for n, p in response[3] or []]
858 return {
859 "pending": response[0],
860 "min": response[1],
861 "max": response[2],
862 "consumers": consumers,
863 }
864
865
866def parse_xpending_range(response):
867 k = ("message_id", "consumer", "time_since_delivered", "times_delivered")
868 return [dict(zip(k, r)) for r in response]
869
870
871def float_or_none(response):
872 if response is None:
873 return None
874 return float(response)
875
876
877def bool_ok(response, **options):
878 return str_if_bytes(response) == "OK"
879
880
881def parse_zadd(response, **options):
882 if response is None:
883 return None
884 if options.get("as_score"):
885 return float(response)
886 return int(response)
887
888
889def _parse_client_info_fields(value):
890 """Tokenize a single ``key=value`` client-info blob into a dict.
891
892 The server emits this space-separated format for each client in
893 ``CLIENT INFO`` and ``CLIENT LIST``, and inside ``ACL LOG`` entries.
894 Two quirks have to be preserved: a value may contain ``=`` (a client
895 name set to ``foo=bar``), so only the first ``=`` splits key from value;
896 and a value may contain spaces (a Unix-socket ``addr``/``laddr`` path
897 such as ``/tmp/redis sock/redis.sock``), so a token with no ``=`` is
898 reattached to the previous value. ``last_key is None`` guards the leading
899 and empty/whitespace-only cases, which yield an empty dict rather than
900 raising.
901 """
902 fields = {}
903 last_key = None
904 for token in value.split(" "):
905 if "=" in token:
906 key, val = token.split("=", 1)
907 fields[key] = val
908 last_key = key
909 elif last_key is not None:
910 # A token without ``=`` continues the previous value, since a
911 # Unix-socket path may contain spaces. Empty tokens (from two or
912 # more consecutive spaces in the path) are reattached the same
913 # way, so a run of spaces round-trips verbatim instead of being
914 # collapsed. Tokens before the first ``key=`` (``last_key is
915 # None``, i.e. a leading space or an empty/whitespace-only blob)
916 # are dropped, yielding an empty dict rather than raising.
917 fields[last_key] += " " + token
918 return fields
919
920
921def parse_client_list(response, **options):
922 if options.pop("as_iter", False):
923 return _client_list_iter(response)
924 clients = []
925 for c in str_if_bytes(response).splitlines():
926 client_dict = _parse_client_info_fields(c)
927 if client_dict:
928 clients.append(client_dict)
929 return clients
930
931
932def _client_list_iter(response):
933 """
934 Yield one CLIENT LIST record at a time instead of building the full
935 ``list[dict]`` up front, so a caller processing records one at a time
936 doesn't need all of them held in memory simultaneously. The full reply
937 is still buffered off the socket before this runs - that part is
938 unavoidable given how RESP bulk-string framing (and hiredis) works -
939 so this only trims the per-record dict/string overhead, not the raw
940 reply size.
941
942 Line boundaries are scanned for lazily (rather than via ``splitlines()``)
943 so the generator never materializes a list of all lines either - that
944 list would otherwise be held for the generator's whole lifetime, right
945 alongside the decoded reply, defeating the point of iterating.
946 """
947 text = str_if_bytes(response)
948 # With decode_responses=False, str_if_bytes decodes a full copy of the
949 # reply. response is a parameter of this generator, so it stays alive
950 # in this frame for as long as the generator does; drop it once decoded
951 # so the original buffer doesn't sit alongside the decoded one for the
952 # whole iteration.
953 response = None
954 end = len(text)
955 start = 0
956 while start < end:
957 newline = text.find("\n", start)
958 if newline == -1:
959 newline = end
960 client_dict = _parse_client_info_fields(text[start:newline])
961 start = newline + 1
962 if client_dict:
963 yield client_dict
964
965
966def parse_config_get(response, **options):
967 response = [str_if_bytes(i) if i is not None else None for i in response]
968 return response and pairs_to_dict(response) or {}
969
970
971def parse_config_get_resp3_to_resp2_legacy(response, **options):
972 """RESP3-wire CONFIG GET → today's RESP2 ``dict[str, str]`` shape.
973
974 On RESP3 the server returns a map; convert both keys and values via
975 ``str_if_bytes`` so callers using ``r.config_get()["timeout"]``
976 keep working when the wire is RESP3 with ``legacy_responses=True``.
977 """
978 if not response:
979 return {}
980 return {
981 str_if_bytes(key) if key is not None else None: (
982 str_if_bytes(value) if value is not None else None
983 )
984 for key, value in response.items()
985 }
986
987
988def parse_scan(response, **options):
989 cursor, r = response
990 return int(cursor), r
991
992
993def parse_hscan(response, **options):
994 cursor, r = response
995 no_values = options.get("no_values", False)
996 if no_values:
997 payload = r or []
998 else:
999 payload = r and pairs_to_dict(r) or {}
1000 return int(cursor), payload
1001
1002
1003def parse_zscan(response, **options):
1004 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
1005 cursor, r = response
1006 it = iter(r)
1007 return int(cursor), list(zip(it, map(score_cast_func, it)))
1008
1009
1010def parse_zscan_unified(response, **options):
1011 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
1012 cursor, r = response
1013 it = iter(r)
1014 return int(cursor), [
1015 [value, score_cast_func(float(score))] for value, score in zip(it, it)
1016 ]
1017
1018
1019def parse_zmscore(response, **options):
1020 # zmscore: list of scores (double precision floating point number) or nil
1021 return [float(score) if score is not None else None for score in response]
1022
1023
1024def parse_slowlog_get(response, **options):
1025 space = " " if options.get("decode_responses", False) else b" "
1026
1027 def parse_item(item):
1028 result = {"id": item[0], "start_time": int(item[1]), "duration": int(item[2])}
1029 # Redis Enterprise injects another entry at index [3], which has
1030 # the complexity info (i.e. the value N in case the command has
1031 # an O(N) complexity) instead of the command.
1032 if isinstance(item[3], list):
1033 result["command"] = space.join(item[3])
1034
1035 # These fields are optional, depends on environment.
1036 if len(item) >= 6:
1037 result["client_address"] = item[4]
1038 result["client_name"] = item[5]
1039 else:
1040 result["complexity"] = item[3]
1041 result["command"] = space.join(item[4])
1042
1043 # These fields are optional, depends on environment.
1044 if len(item) >= 7:
1045 result["client_address"] = item[5]
1046 result["client_name"] = item[6]
1047
1048 return result
1049
1050 return [parse_item(item) for item in response]
1051
1052
1053def parse_stralgo(response, **options):
1054 """
1055 Parse the response from `STRALGO` command.
1056 Without modifiers the returned value is string.
1057 When LEN is given the command returns the length of the result
1058 (i.e integer).
1059 When IDX is given the command returns a dictionary with the LCS
1060 length and all the ranges in both the strings, start and end
1061 offset for each string, where there are matches.
1062 When WITHMATCHLEN is given, each array representing a match will
1063 also have the length of the match at the beginning of the array.
1064 """
1065 if options.get("len", False):
1066 return int(response)
1067 if options.get("idx", False):
1068 if options.get("withmatchlen", False):
1069 matches = [
1070 [(int(match[-1]))] + list(map(tuple, match[:-1]))
1071 for match in response[1]
1072 ]
1073 else:
1074 matches = [list(map(tuple, match)) for match in response[1]]
1075 return {
1076 str_if_bytes(response[0]): matches,
1077 str_if_bytes(response[2]): int(response[3]),
1078 }
1079 return str_if_bytes(response)
1080
1081
1082def parse_stralgo_unified(response, **options):
1083 """
1084 Parse STRALGO into the approved unified shape.
1085
1086 The legacy parser returns tuple ranges for IDX responses. Unified
1087 responses use list ranges so RESP2 and RESP3 produce the same value.
1088 """
1089 if options.get("len", False):
1090 return int(response)
1091 if options.get("idx", False):
1092 if options.get("withmatchlen", False):
1093 matches = [
1094 [int(match[-1])] + [list(m) for m in match[:-1]]
1095 for match in response[1]
1096 ]
1097 else:
1098 matches = [[list(m) for m in match] for match in response[1]]
1099 return {
1100 str_if_bytes(response[0]): matches,
1101 str_if_bytes(response[2]): int(response[3]),
1102 }
1103 return str_if_bytes(response)
1104
1105
1106def parse_stralgo_resp3_unified(response, **options):
1107 """Parse RESP3 STRALGO into the same value as ``parse_stralgo_unified``."""
1108 if options.get("len", False):
1109 return int(response)
1110 if options.get("idx", False):
1111 if not isinstance(response, dict):
1112 return str_if_bytes(response)
1113 raw_matches = response.get("matches", response.get(b"matches", []))
1114 raw_len = response.get("len", response.get(b"len", 0))
1115 if options.get("withmatchlen", False):
1116 matches = [
1117 [int(match[-1])] + [list(m) for m in match[:-1]]
1118 for match in raw_matches
1119 ]
1120 else:
1121 matches = [[list(m) for m in match] for match in raw_matches]
1122 return {"matches": matches, "len": int(raw_len)}
1123 return str_if_bytes(response)
1124
1125
1126def parse_cluster_info(response, **options):
1127 response = str_if_bytes(response)
1128 return dict(line.split(":") for line in response.splitlines() if line)
1129
1130
1131def _parse_node_line(line):
1132 line_items = line.split(" ")
1133 node_id, addr, flags, master_id, ping, pong, epoch, connected = line.split(" ")[:8]
1134 ip = addr.split("@")[0]
1135 hostname = addr.split("@")[1].split(",")[1] if "@" in addr and "," in addr else ""
1136 node_dict = {
1137 "node_id": node_id,
1138 "hostname": hostname,
1139 "flags": flags,
1140 "master_id": master_id,
1141 "last_ping_sent": ping,
1142 "last_pong_rcvd": pong,
1143 "epoch": epoch,
1144 "slots": [],
1145 "migrations": [],
1146 "connected": True if connected == "connected" else False,
1147 }
1148 if len(line_items) >= 9:
1149 slots, migrations = _parse_slots(line_items[8:])
1150 node_dict["slots"], node_dict["migrations"] = slots, migrations
1151 return ip, node_dict
1152
1153
1154def _parse_slots(slot_ranges):
1155 slots, migrations = [], []
1156 for s_range in slot_ranges:
1157 if "->-" in s_range:
1158 slot_id, dst_node_id = s_range[1:-1].split("->-", 1)
1159 migrations.append(
1160 {"slot": slot_id, "node_id": dst_node_id, "state": "migrating"}
1161 )
1162 elif "-<-" in s_range:
1163 slot_id, src_node_id = s_range[1:-1].split("-<-", 1)
1164 migrations.append(
1165 {"slot": slot_id, "node_id": src_node_id, "state": "importing"}
1166 )
1167 else:
1168 s_range = [sl for sl in s_range.split("-")]
1169 slots.append(s_range)
1170
1171 return slots, migrations
1172
1173
1174def parse_cluster_nodes(response, **options):
1175 """
1176 @see: https://redis.io/commands/cluster-nodes # string / bytes
1177 @see: https://redis.io/commands/cluster-replicas # list of string / bytes
1178 """
1179 if isinstance(response, (str, bytes)):
1180 response = response.splitlines()
1181 return dict(_parse_node_line(str_if_bytes(node)) for node in response)
1182
1183
1184def parse_geosearch_generic(response, **options):
1185 """
1186 Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER'
1187 commands according to 'withdist', 'withhash' and 'withcoord' labels.
1188 """
1189 try:
1190 if options["store"] or options["store_dist"]:
1191 # `store` and `store_dist` can't be combined
1192 # with other command arguments.
1193 # relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER'
1194 return response
1195 except KeyError: # it means the command was sent via execute_command
1196 return response
1197
1198 if not isinstance(response, list):
1199 response_list = [response]
1200 else:
1201 response_list = response
1202
1203 if not options["withdist"] and not options["withcoord"] and not options["withhash"]:
1204 # just a bunch of places
1205 return response_list
1206
1207 cast = {
1208 "withdist": float,
1209 "withcoord": lambda ll: (float(ll[0]), float(ll[1])),
1210 "withhash": int,
1211 }
1212
1213 # zip all output results with each casting function to get
1214 # the properly native Python value.
1215 f = [lambda x: x]
1216 f += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]]
1217 return [list(map(lambda fv: fv[0](fv[1]), zip(f, r))) for r in response_list]
1218
1219
1220def parse_geosearch_generic_unified(response, **options):
1221 """
1222 Parse GEOSEARCH/GEORADIUS responses using tuple coordinates.
1223 """
1224 try:
1225 if options["store"] or options["store_dist"]:
1226 return response
1227 except KeyError:
1228 return response
1229
1230 response_list = response if isinstance(response, list) else [response]
1231
1232 if not options["withdist"] and not options["withcoord"] and not options["withhash"]:
1233 return response_list
1234
1235 cast = {
1236 "withdist": float,
1237 "withcoord": lambda ll: (float(ll[0]), float(ll[1])),
1238 "withhash": int,
1239 }
1240 funcs = [lambda x: x]
1241 funcs += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]]
1242 return [list(map(lambda fv: fv[0](fv[1]), zip(funcs, r))) for r in response_list]
1243
1244
1245def parse_command(response, **options):
1246 commands = {}
1247 for command in response:
1248 cmd_dict = {}
1249 cmd_name = str_if_bytes(command[0])
1250 cmd_dict["name"] = cmd_name
1251 cmd_dict["arity"] = int(command[1])
1252 cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]]
1253 cmd_dict["first_key_pos"] = command[3]
1254 cmd_dict["last_key_pos"] = command[4]
1255 cmd_dict["step_count"] = command[5]
1256 if len(command) > 6:
1257 cmd_dict["acl_categories"] = [
1258 str_if_bytes(category) for category in command[6]
1259 ]
1260 if len(command) > 7:
1261 cmd_dict["tips"] = command[7]
1262 cmd_dict["key_specifications"] = command[8]
1263 cmd_dict["subcommands"] = command[9]
1264 commands[cmd_name] = cmd_dict
1265 return commands
1266
1267
1268def parse_command_unified(response, **options):
1269 commands = {}
1270 for command in response:
1271 cmd_dict = {}
1272 cmd_name = str_if_bytes(command[0])
1273 cmd_dict["name"] = cmd_name
1274 cmd_dict["arity"] = int(command[1])
1275 cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]}
1276 cmd_dict["first_key_pos"] = command[3]
1277 cmd_dict["last_key_pos"] = command[4]
1278 cmd_dict["step_count"] = command[5]
1279 if len(command) > 6:
1280 cmd_dict["acl_categories"] = {str_if_bytes(c) for c in command[6]}
1281 if len(command) > 7:
1282 cmd_dict["tips"] = command[7]
1283 cmd_dict["key_specifications"] = command[8]
1284 cmd_dict["subcommands"] = command[9]
1285 commands[cmd_name] = cmd_dict
1286 return commands
1287
1288
1289def parse_command_resp3(response, **options):
1290 commands = {}
1291 for command in response:
1292 cmd_dict = {}
1293 cmd_name = str_if_bytes(command[0])
1294 cmd_dict["name"] = cmd_name
1295 cmd_dict["arity"] = command[1]
1296 cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]}
1297 cmd_dict["first_key_pos"] = command[3]
1298 cmd_dict["last_key_pos"] = command[4]
1299 cmd_dict["step_count"] = command[5]
1300 cmd_dict["acl_categories"] = command[6]
1301 if len(command) > 7:
1302 cmd_dict["tips"] = command[7]
1303 cmd_dict["key_specifications"] = command[8]
1304 cmd_dict["subcommands"] = command[9]
1305
1306 commands[cmd_name] = cmd_dict
1307 return commands
1308
1309
1310def parse_pubsub_numsub(response, **options):
1311 return list(zip(response[0::2], response[1::2]))
1312
1313
1314def parse_client_kill(response, **options):
1315 if isinstance(response, int):
1316 return response
1317 return str_if_bytes(response) == "OK"
1318
1319
1320def parse_acl_getuser(response, **options):
1321 if response is None:
1322 return None
1323 if isinstance(response, list):
1324 data = pairs_to_dict(response, decode_keys=True)
1325 else:
1326 data = {str_if_bytes(key): value for key, value in response.items()}
1327
1328 # convert everything but user-defined data in 'keys' to native strings
1329 data["flags"] = list(map(str_if_bytes, data["flags"]))
1330 data["passwords"] = list(map(str_if_bytes, data["passwords"]))
1331 data["commands"] = str_if_bytes(data["commands"])
1332 if isinstance(data["keys"], str) or isinstance(data["keys"], bytes):
1333 data["keys"] = list(str_if_bytes(data["keys"]).split(" "))
1334 if data["keys"] == [""]:
1335 data["keys"] = []
1336 if "channels" in data:
1337 if isinstance(data["channels"], str) or isinstance(data["channels"], bytes):
1338 data["channels"] = list(str_if_bytes(data["channels"]).split(" "))
1339 if data["channels"] == [""]:
1340 data["channels"] = []
1341 if "selectors" in data:
1342 if data["selectors"] != [] and isinstance(data["selectors"][0], list):
1343 data["selectors"] = [
1344 list(map(str_if_bytes, selector)) for selector in data["selectors"]
1345 ]
1346 elif data["selectors"] != []:
1347 data["selectors"] = [
1348 {str_if_bytes(k): str_if_bytes(v) for k, v in selector.items()}
1349 for selector in data["selectors"]
1350 ]
1351
1352 # split 'commands' into separate 'categories' and 'commands' lists
1353 commands, categories = [], []
1354 for command in data["commands"].split(" "):
1355 categories.append(command) if "@" in command else commands.append(command)
1356
1357 data["commands"] = commands
1358 data["categories"] = categories
1359 data["enabled"] = "on" in data["flags"]
1360 return data
1361
1362
1363def parse_acl_log(response, **options):
1364 if response is None:
1365 return None
1366 if isinstance(response, list):
1367 data = []
1368 for log in response:
1369 log_data = pairs_to_dict(log, True, True)
1370 client_info = log_data.get("client-info", "")
1371 log_data["client-info"] = parse_client_info(client_info)
1372
1373 # float() is lossy comparing to the "double" in C
1374 log_data["age-seconds"] = float(log_data["age-seconds"])
1375 data.append(log_data)
1376 else:
1377 data = bool_ok(response)
1378 return data
1379
1380
1381def parse_acl_log_resp3_to_resp2_legacy(response, **options):
1382 """RESP3-wire ACL LOG → today's RESP2 parsed shape.
1383
1384 Each log entry arrives as a ``dict`` on RESP3 wire instead of a flat
1385 list of pairs; convert ``client-info`` from a string blob into the
1386 parsed ``dict`` and ``age-seconds`` to ``float`` so the Python shape
1387 matches what :func:`parse_acl_log` produces from RESP2 wire.
1388
1389 Also used as the unified callback (Set D): the legacy and unified
1390 shapes coincide for ACL LOG.
1391 """
1392 if response is None:
1393 return None
1394 if not isinstance(response, list):
1395 return bool_ok(response)
1396 data = []
1397 for log in response:
1398 if isinstance(log, dict):
1399 log_data = {str_if_bytes(k): str_if_bytes(v) for k, v in log.items()}
1400 else:
1401 log_data = pairs_to_dict(log, True, True)
1402 client_info = log_data.get("client-info", "")
1403 log_data["client-info"] = parse_client_info(client_info)
1404 log_data["age-seconds"] = float(log_data["age-seconds"])
1405 data.append(log_data)
1406 return data
1407
1408
1409def parse_acl_log_resp3_unified(response, **options):
1410 """Parse RESP3 ACL LOG into the approved unified shape."""
1411 if response is None:
1412 return None
1413 if not isinstance(response, list):
1414 return bool_ok(response)
1415 data = []
1416 for entry in response:
1417 if isinstance(entry, dict):
1418 log_data = {str_if_bytes(k): v for k, v in entry.items()}
1419 else:
1420 log_data = pairs_to_dict(entry, True, True)
1421 if "age-seconds" in log_data:
1422 log_data["age-seconds"] = float(log_data["age-seconds"])
1423 if "client-info" in log_data:
1424 log_data["client-info"] = parse_client_info(log_data["client-info"])
1425 for key, value in list(log_data.items()):
1426 if key not in ("age-seconds", "client-info"):
1427 log_data[key] = str_if_bytes(value)
1428 data.append(log_data)
1429 return data
1430
1431
1432def parse_acl_getuser_unified(response, **options):
1433 """ACL GETUSER → unified shape with selectors as ``list[dict]``.
1434
1435 On RESP2 wire each selector arrives as a flat ``[k, v, k, v, …]``
1436 list; pair them into dicts to match the RESP3 wire shape.
1437 """
1438 data = parse_acl_getuser(response, **options)
1439 if data is None:
1440 return data
1441 selectors = data.get("selectors")
1442 if selectors and isinstance(selectors[0], list):
1443 data["selectors"] = [
1444 dict(zip(selector[0::2], selector[1::2])) for selector in selectors
1445 ]
1446 return data
1447
1448
1449def parse_acl_getuser_resp3_to_resp2_legacy(response, **options):
1450 """RESP3-wire ACL GETUSER → today's RESP2 selectors as flat lists.
1451
1452 Each selector arrives as a ``dict`` on RESP3 wire; flatten back to
1453 the interleaved ``[k, v, k, v, …]`` form produced by RESP2 wire.
1454 """
1455 data = parse_acl_getuser(response, **options)
1456 if data is None:
1457 return data
1458 selectors = data.get("selectors")
1459 if selectors and isinstance(selectors[0], dict):
1460 data["selectors"] = [
1461 [item for kv in selector.items() for item in kv] for selector in selectors
1462 ]
1463 return data
1464
1465
1466def parse_client_info(value):
1467 """
1468 Parsing client-info in ACL Log in following format.
1469 "key1=value1 key2=value2 key3=value3"
1470 """
1471 client_info = _parse_client_info_fields(str_if_bytes(value).strip())
1472
1473 # Those fields are defined as int in networking.c
1474 for int_key in {
1475 "id",
1476 "age",
1477 "idle",
1478 "db",
1479 "sub",
1480 "psub",
1481 "multi",
1482 "qbuf",
1483 "qbuf-free",
1484 "obl",
1485 "argv-mem",
1486 "oll",
1487 "omem",
1488 "tot-mem",
1489 }:
1490 if int_key in client_info:
1491 client_info[int_key] = int(client_info[int_key])
1492 return client_info
1493
1494
1495def parse_set_result(response, **options):
1496 """
1497 Handle SET result since GET argument is available since Redis 6.2.
1498 Parsing SET result into:
1499 - BOOL
1500 - String when GET argument is used
1501 """
1502 if options.get("get"):
1503 # Redis will return a getCommand result.
1504 # See `setGenericCommand` in t_string.c
1505 return response
1506 return response and str_if_bytes(response) == "OK"
1507
1508
1509def parse_function_list_unified(response, **options):
1510 """FUNCTION LIST → unified ``list[dict]`` with bytes keys.
1511
1512 Accepts either RESP2 wire (``list[list]`` of flat ``[k, v, k, v, …]``
1513 pairs, with the nested ``b"functions"`` value also a flat list of
1514 flat lists) or RESP3 wire (``list[dict]`` already in nested-map
1515 form). Both are normalised to ``list[dict]``.
1516 """
1517 if response is None:
1518 return None
1519 result = []
1520 for lib in response:
1521 if isinstance(lib, dict):
1522 result.append(lib)
1523 continue
1524 lib_dict = pairs_to_dict(lib)
1525 func_key = b"functions" if b"functions" in lib_dict else "functions"
1526 if func_key in lib_dict:
1527 functions = lib_dict[func_key]
1528 lib_dict[func_key] = [
1529 func if isinstance(func, dict) else pairs_to_dict(func)
1530 for func in functions
1531 ]
1532 result.append(lib_dict)
1533 return result
1534
1535
1536def parse_function_list_resp3_to_resp2_legacy(response, **options):
1537 """RESP3-wire FUNCTION LIST → today's RESP2 ``list[list]`` shape.
1538
1539 Each library and each nested function arrives as a ``dict``; flatten
1540 them back to interleaved ``[k, v, k, v, …]`` lists so the Python
1541 shape matches what RESP2 wire produces natively.
1542 """
1543 if response is None:
1544 return None
1545 result = []
1546 for lib in response:
1547 if not isinstance(lib, dict):
1548 result.append(lib)
1549 continue
1550 flat = []
1551 for key, value in lib.items():
1552 flat.append(key)
1553 if key == b"functions" or key == "functions":
1554 flat.append(
1555 [
1556 [item for kv in func.items() for item in kv]
1557 if isinstance(func, dict)
1558 else func
1559 for func in value
1560 ]
1561 )
1562 else:
1563 flat.append(value)
1564 result.append(flat)
1565 return result
1566
1567
1568def parse_cluster_links_unified(response, **options):
1569 """CLUSTER LINKS → unified ``list[dict]`` with string keys.
1570
1571 Accepts either RESP2 wire (``list[list]`` of flat pairs) or RESP3
1572 wire (``list[dict]``). Both are normalised to ``list[dict]``.
1573 """
1574 if response is None:
1575 return None
1576 return [
1577 {str_if_bytes(k): v for k, v in item.items()}
1578 if isinstance(item, dict)
1579 else pairs_to_dict(item, decode_keys=True)
1580 for item in response
1581 ]
1582
1583
1584def parse_cluster_links_resp3_to_resp2_legacy(response, **options):
1585 """RESP3-wire CLUSTER LINKS → today's RESP2 ``list[list]`` shape.
1586
1587 Each link arrives as a ``dict`` with bytes keys; flatten back to
1588 interleaved ``[k, v, k, v, …]`` lists so the Python shape matches
1589 what RESP2 wire produces natively.
1590 """
1591 if response is None:
1592 return None
1593 return [
1594 [item for kv in link.items() for item in kv] if isinstance(link, dict) else link
1595 for link in response
1596 ]
1597
1598
1599def string_keys_to_dict(key_string, callback):
1600 return dict.fromkeys(key_string.split(), callback)
1601
1602
1603# The command-to-callback mapping dictionaries (``_RedisCallbacks``,
1604# ``_RedisCallbacksRESP2``, ``_RedisCallbacksRESP3``, …) and the
1605# ``get_response_callbacks`` selector live in
1606# ``redis/_parsers/response_callbacks.py``. They are re-exported below for
1607# backward compatibility so existing imports of the form
1608# ``from redis._parsers.helpers import _RedisCallbacks`` keep working. The
1609# import is placed at module bottom to avoid a circular import (the
1610# response_callbacks module imports parser helpers defined above).
1611# isort: off
1612from .response_callbacks import ( # noqa: E402, F401
1613 _RedisCallbacks,
1614 _RedisCallbacksRESP2,
1615 _RedisCallbacksRESP2Unified,
1616 _RedisCallbacksRESP3,
1617 _RedisCallbacksRESP3Unified,
1618 _RedisCallbacksRESP3toRESP2Legacy,
1619 get_response_callbacks,
1620)
1621# isort: on