Beta/venv/lib/python3.12/site-packages/redis/_parsers/helpers.py

1570 lines
52 KiB
Python
Raw Normal View History

2026-06-16 17:09:34 +00:00
import datetime
from redis.utils import str_if_bytes
def timestamp_to_datetime(response):
"Converts a unix timestamp to a Python datetime object"
if not response:
return None
try:
response = int(response)
except ValueError:
return None
return datetime.datetime.fromtimestamp(response)
def parse_debug_object(response):
"Parse the results of Redis's DEBUG OBJECT command into a Python dict"
# The 'type' of the object is the first item in the response, but isn't
# prefixed with a name
response = str_if_bytes(response)
response = "type:" + response
response = dict(kv.split(":") for kv in response.split())
# parse some expected int values from the string response
# note: this cmd isn't spec'd so these may not appear in all redis versions
int_fields = ("refcount", "serializedlength", "lru", "lru_seconds_idle")
for field in int_fields:
if field in response:
response[field] = int(response[field])
return response
def parse_info(response):
"""Parse the result of Redis's INFO command into a Python dict"""
info = {}
response = str_if_bytes(response)
def get_value(value):
if "," not in value and "=" not in value:
try:
if "." in value:
return float(value)
else:
return int(value)
except ValueError:
return value
elif "=" not in value:
return [get_value(v) for v in value.split(",") if v]
else:
sub_dict = {}
for item in value.split(","):
if not item:
continue
if "=" in item:
k, v = item.rsplit("=", 1)
sub_dict[k] = get_value(v)
else:
sub_dict[item] = True
return sub_dict
for line in response.splitlines():
if line and not line.startswith("#"):
if line.find(":") != -1:
# Split, the info fields keys and values.
# Note that the value may contain ':'. but the 'host:'
# pseudo-command is the only case where the key contains ':'
key, value = line.split(":", 1)
if key == "cmdstat_host":
key, value = line.rsplit(":", 1)
if key == "module":
# Hardcode a list for key 'modules' since there could be
# multiple lines that started with 'module'
info.setdefault("modules", []).append(get_value(value))
else:
info[key] = get_value(value)
else:
# if the line isn't splittable, append it to the "__raw__" key
info.setdefault("__raw__", []).append(line)
return info
def parse_memory_stats(response, **kwargs):
"""Parse the results of MEMORY STATS"""
stats = pairs_to_dict(response, decode_keys=True, decode_string_values=True)
for key, value in stats.items():
if key.startswith("db.") and isinstance(value, list):
stats[key] = pairs_to_dict(
value, decode_keys=True, decode_string_values=True
)
return stats
def parse_memory_stats_unified(response, **kwargs):
"""Parse MEMORY STATS for unified RESP2 output.
Unified responses decode structural keys while preserving string-like
values as delivered, matching the approved RESP2/RESP3 unification shape.
"""
stats = pairs_to_dict(response, decode_keys=True)
for key, value in stats.items():
if key.startswith("db.") and isinstance(value, list):
stats[key] = pairs_to_dict(value, decode_keys=True)
return stats
def parse_memory_stats_resp3(response, **kwargs):
"""Parse the results of MEMORY STATS on RESP3 wire.
Each entry arrives as a top-level ``dict`` instead of a flat list of
pairs; decode the keys to ``str`` and recurse into the per-database
``db.*`` sub-dicts so the Python shape matches what
:func:`parse_memory_stats` produces from RESP2 wire.
"""
stats = {str_if_bytes(key): value for key, value in response.items()}
for key, value in stats.items():
if key.startswith("db.") and isinstance(value, dict):
stats[key] = {str_if_bytes(k): v for k, v in value.items()}
return stats
def parse_list_of_dicts_resp3(response, **kwargs):
"""Parse list-of-maps responses on RESP3 wire (e.g. ``XINFO`` family).
Each list entry arrives as a ``dict`` with bytes keys; decode the
keys to ``str`` so the Python shape matches what
:func:`parse_list_of_dicts` produces from RESP2 wire.
"""
return [{str_if_bytes(key): value for key, value in x.items()} for x in response]
SENTINEL_STATE_TYPES = {
"can-failover-its-master": int,
"config-epoch": int,
"down-after-milliseconds": int,
"failover-timeout": int,
"info-refresh": int,
"last-hello-message": int,
"last-ok-ping-reply": int,
"last-ping-reply": int,
"last-ping-sent": int,
"master-link-down-time": int,
"master-port": int,
"num-other-sentinels": int,
"num-slaves": int,
"o-down-time": int,
"pending-commands": int,
"parallel-syncs": int,
"port": int,
"quorum": int,
"role-reported-time": int,
"s-down-time": int,
"slave-priority": int,
"slave-repl-offset": int,
"voted-leader-epoch": int,
}
_SENTINEL_DERIVED_BOOLEANS = (
("is_master", "master"),
("is_slave", "slave"),
("is_sdown", "s_down"),
("is_odown", "o_down"),
("is_sentinel", "sentinel"),
("is_disconnected", "disconnected"),
("is_master_down", "master_down"),
)
def _add_derived_sentinel_booleans(result, flags):
"""Set ``is_master`` / ``is_slave`` / ``is_sdown`` / ``is_odown`` /
``is_sentinel`` / ``is_disconnected`` / ``is_master_down`` on
``result`` based on membership in the ``flags`` set.
"""
for name, flag in _SENTINEL_DERIVED_BOOLEANS:
result[name] = flag in flags
def parse_sentinel_state(item):
result = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES)
flags = set(result["flags"].split(","))
_add_derived_sentinel_booleans(result, flags)
return result
def parse_sentinel_master(response, **options):
return parse_sentinel_state(map(str_if_bytes, response))
def parse_sentinel_state_resp3(response, **options):
result = {}
for key in response:
str_key = str_if_bytes(key)
try:
value = SENTINEL_STATE_TYPES[str_key](str_if_bytes(response[key]))
result[str_key] = value
except Exception:
result[str_key] = str_if_bytes(response[key])
flags = set(result["flags"].split(","))
result["flags"] = flags
_add_derived_sentinel_booleans(result, flags)
return result
def parse_sentinel_masters(response, **options):
result = {}
for item in response:
state = parse_sentinel_state(map(str_if_bytes, item))
result[state["name"]] = state
return result
def parse_sentinel_masters_resp3(response, **options):
result = {}
for master in response:
state = parse_sentinel_state_resp3(master)
result[state["name"]] = state
return result
def parse_sentinel_slaves_and_sentinels(response, **options):
return [parse_sentinel_state(map(str_if_bytes, item)) for item in response]
def parse_sentinel_slaves_and_sentinels_resp3(response, **options):
return [parse_sentinel_state_resp3(item, **options) for item in response]
def _flatten_resp3_state_pairs(state):
"""Yield key/value pairs from a RESP3 sentinel-state map as a flat
iterable suitable for ``parse_sentinel_state``.
"""
for key, value in state.items():
yield key
yield value
def parse_sentinel_master_resp3_to_resp2_legacy(response, **options):
return parse_sentinel_state(map(str_if_bytes, _flatten_resp3_state_pairs(response)))
def parse_sentinel_masters_resp3_to_resp2_legacy(response, **options):
result = {}
for master in response:
state = parse_sentinel_state(
map(str_if_bytes, _flatten_resp3_state_pairs(master))
)
result[state["name"]] = state
return result
def parse_sentinel_slaves_and_sentinels_resp3_to_resp2_legacy(response, **options):
return [
parse_sentinel_state(map(str_if_bytes, _flatten_resp3_state_pairs(item)))
for item in response
]
def parse_sentinel_master_unified(response, **options):
state = parse_sentinel_state(map(str_if_bytes, response))
state["flags"] = set(state["flags"].split(","))
return state
def parse_sentinel_masters_unified(response, **options):
result = {}
for item in response:
state = parse_sentinel_state(map(str_if_bytes, item))
state["flags"] = set(state["flags"].split(","))
result[state["name"]] = state
return result
def parse_sentinel_slaves_and_sentinels_unified(response, **options):
out = []
for item in response:
state = parse_sentinel_state(map(str_if_bytes, item))
state["flags"] = set(state["flags"].split(","))
out.append(state)
return out
def parse_sentinel_master_unified_resp3(response, **options):
state = parse_sentinel_state_resp3(response, **options)
_add_derived_sentinel_booleans(state, state["flags"])
return state
def parse_sentinel_masters_unified_resp3(response, **options):
result = {}
for master in response:
state = parse_sentinel_state_resp3(master)
_add_derived_sentinel_booleans(state, state["flags"])
result[state["name"]] = state
return result
def parse_sentinel_slaves_and_sentinels_unified_resp3(response, **options):
out = []
for item in response:
state = parse_sentinel_state_resp3(item, **options)
_add_derived_sentinel_booleans(state, state["flags"])
out.append(state)
return out
def parse_sentinel_get_master(response, **options):
return response and (response[0], int(response[1])) or None
def pairs_to_dict(response, decode_keys=False, decode_string_values=False):
"""Create a dict given a list of key/value pairs"""
if response is None:
return {}
if decode_keys or decode_string_values:
# the iter form is faster, but I don't know how to make that work
# with a str_if_bytes() map
keys = response[::2]
if decode_keys:
keys = map(str_if_bytes, keys)
values = response[1::2]
if decode_string_values:
values = map(str_if_bytes, values)
return dict(zip(keys, values))
else:
it = iter(response)
return dict(zip(it, it))
def pairs_to_dict_typed(response, type_info):
it = iter(response)
result = {}
for key, value in zip(it, it):
if key in type_info:
try:
value = type_info[key](value)
except Exception:
# if for some reason the value can't be coerced, just use
# the string value
pass
result[key] = value
return result
def _wrap_score_cast_func(score_cast_func):
"""Wrap score_cast_func to handle scientific notation in RESP2 byte strings.
Redis returns scores as byte strings in RESP2, and large numbers may use
scientific notation (e.g., b'1.7732526297292595e+18'). Python's int() cannot
parse scientific notation directly. Rather than unconditionally routing
through float() (which would change the input type for every custom
callable), we try the original function first and only fall back to
converting through float() on ValueError.
"""
if score_cast_func is float:
return score_cast_func
def _safe_cast(x):
try:
return score_cast_func(x)
except (ValueError, TypeError):
return score_cast_func(float(x))
return _safe_cast
def zset_score_pairs(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of (value, score) pairs
"""
if not response or not options.get("withscores"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
it = iter(response)
return list(zip(it, map(score_cast_func, it)))
def zpop_score_pairs(response, **options):
"""RESP2-wire ZPOPMAX/ZPOPMIN -> legacy ``list[(member, score), ...]``.
ZPOPMAX/ZPOPMIN always include scores, so this parser intentionally
does not depend on a ``withscores`` option.
"""
if not response:
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
it = iter(response)
return list(zip(it, map(score_cast_func, it)))
def zset_score_for_rank(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a [value, score] pair
"""
if not response or not options.get("withscore"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
return [response[0], score_cast_func(response[1])]
def zset_score_pairs_resp3(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of [value, score] pairs
"""
if not response or not options.get("withscores"):
return response
score_cast_func = options.get("score_cast_func", float)
return [[name, score_cast_func(val)] for name, val in response]
def zset_score_for_rank_resp3(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a [value, score] pair
"""
if not response or not options.get("withscore"):
return response
score_cast_func = options.get("score_cast_func", float)
return [response[0], score_cast_func(response[1])]
def _score_to_resp2_bytes(value):
"""Re-encode a score back to the bytes form Redis returns on the RESP2
wire so that custom ``score_cast_func`` callables observe the same
input type they would receive on a RESP2 connection when the wire
protocol is RESP3 but legacy response shapes are requested.
"""
if isinstance(value, bytes):
return value
if isinstance(value, str):
return value.encode()
if isinstance(value, bool):
return b"1" if value else b"0"
return format(float(value), ".17g").encode()
def zset_score_pairs_resp3_to_resp2_legacy(response, **options):
"""Convert RESP3 nested ``[[member, score], ...]`` to today's RESP2
``list[(member, score)]`` shape: tuples instead of lists, scores
re-encoded to bytes before being passed to ``score_cast_func`` so the
cast receives the same input as on a RESP2 connection.
"""
if not response or not options.get("withscores"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
return [
(member, score_cast_func(_score_to_resp2_bytes(score)))
for member, score in response
]
def zset_score_pairs_resp3_to_resp2_legacy_flat(response, **options):
"""Convert RESP3 nested ``[[member, score], ...]`` to the flat raw RESP2
wire shape ``[member, score_bytes, ...]`` used by ZDIFF in v8.0.0b1.
ZDIFF historically did not propagate ``withscores`` to the response
callback, so the legacy RESP2 callback was a no-op and the raw flat
wire response was returned to the user. This helper reproduces that
shape on RESP3 wires so ``legacy_responses=True`` keeps emitting the
same Python value regardless of the underlying protocol.
"""
if not response or not options.get("withscores"):
return response
flat = []
for member, score in response:
flat.append(member)
flat.append(_score_to_resp2_bytes(score))
return flat
def zset_score_for_rank_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire ZRANK/ZREVRANK WITHSCORE → legacy RESP2 ``[rank, score]``.
The shape ``[rank, score]`` is identical between RESP2 and RESP3; only
the score is re-encoded to bytes before being passed to
``score_cast_func`` so the cast observes the same input type it would
on a RESP2 connection.
"""
if not response or not options.get("withscore"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
return [response[0], score_cast_func(_score_to_resp2_bytes(response[1]))]
def zset_score_pairs_unified(response, **options):
"""RESP2-wire WITHSCORES → unified ``list[[member, score], ...]``.
Normalises RESP2 byte-string scores through ``float`` before applying
``score_cast_func`` so the cast receives the same input type as on a
RESP3 connection.
"""
if not response or not options.get("withscores"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
it = iter(response)
return [[val, score_cast_func(float(score))] for val, score in zip(it, it)]
def zset_score_for_rank_unified(response, **options):
"""RESP2-wire ZRANK/ZREVRANK WITHSCORE → unified ``[rank, score]``.
Normalises the RESP2 byte-string score through ``float`` before
applying ``score_cast_func`` so the cast receives the same input type
as on a RESP3 connection.
"""
if not response or not options.get("withscore"):
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
return [response[0], score_cast_func(float(response[1]))]
def zpop_score_pairs_unified(response, **options):
"""RESP2-wire ZPOPMAX/ZPOPMIN → unified ``list[[member, score], ...]``.
ZPOPMAX/ZPOPMIN always include scores; no ``withscores`` gate is
required. Scores are normalised through ``float`` before applying
``score_cast_func`` for parity with RESP3.
"""
if not response:
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
it = iter(response)
return [[val, score_cast_func(float(score))] for val, score in zip(it, it)]
def zpop_score_pairs_resp3_unified(response, **options):
"""RESP3-wire ZPOPMAX/ZPOPMIN → unified ``list[[member, score], ...]``.
Without ``count`` RESP3 returns a flat ``[member, score]``; with
``count`` it returns a nested ``[[member, score], ...]``. Both shapes
are normalised to a nested list with ``score_cast_func`` applied.
"""
if not response:
return response
score_cast_func = options.get("score_cast_func", float)
if isinstance(response[0], list):
return [[name, score_cast_func(val)] for name, val in response]
return [[response[0], score_cast_func(response[1])]]
def zpop_score_pairs_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire ZPOPMAX/ZPOPMIN → legacy RESP2 ``list[(member, score), ...]``.
Both RESP3 shapes (flat without ``count``; nested with ``count``) are
converted to a list of tuples. Scores are re-encoded to bytes before
being passed to ``score_cast_func`` so the cast observes the same
input type it would on a RESP2 connection.
"""
if not response:
return response
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
if isinstance(response[0], list):
return [
(member, score_cast_func(_score_to_resp2_bytes(score)))
for member, score in response
]
return [(response[0], score_cast_func(_score_to_resp2_bytes(response[1])))]
def bzpop_score_unified(response, **options):
"""BZPOPMAX/BZPOPMIN → unified ``[key, member, score]``.
Works for both RESP2 (bytes score) and RESP3 (float score) wire shapes.
"""
if not response:
return None
return [response[0], response[1], float(response[2])]
def bzpop_score_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire BZPOPMAX/BZPOPMIN → legacy RESP2 ``(key, member, score)``.
Matches the v8.0.0b1 RESP2-wire callback shape (tuple, ``float`` score).
"""
if not response:
return None
return (response[0], response[1], float(response[2]))
def zmpop_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire ZMPOP/BZMPOP → legacy RESP2 ``[name, [[member, b"score"], ...]]``.
Re-encodes RESP3 native float scores back to the bytes form Redis
returns on the RESP2 wire so callers observe today's RESP2 raw shape.
"""
if not response:
return response
return [
response[0],
[[member, _score_to_resp2_bytes(score)] for member, score in response[1]],
]
def zmpop_unified(response, **options):
"""ZMPOP/BZMPOP → unified ``[name, [[member, float_score], ...]]``.
Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
RESP3's native float-score shape.
"""
if not response:
return response
return [
response[0],
[[member, float(score)] for member, score in response[1]],
]
def hrandfield_unified(response, **options):
"""RESP2-wire HRANDFIELD WITHVALUES → unified ``list[[field, value], ...]``.
Plain (no-values) responses flat list of fields pass through.
The ``withvalues`` option (forwarded by the command method) selects the
pairing branch so the no-values flat result is never misread.
"""
if not response or not options.get("withvalues"):
return response
if isinstance(response[0], list):
return response
it = iter(response)
return [[field, value] for field, value in zip(it, it)]
def hrandfield_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire HRANDFIELD WITHVALUES → legacy RESP2 flat ``[field, value, ...]``.
Plain (no-values) responses flat list of fields pass through.
"""
if not response or not options.get("withvalues"):
return response
if not isinstance(response[0], list):
return response
flat = []
for field, value in response:
flat.append(field)
flat.append(value)
return flat
def parse_geopos_unified(response, **options):
"""GEOPOS → unified ``list[list[float, float] | None]``.
Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
RESP3's native ``list[list]`` shape.
"""
return [[float(ll[0]), float(ll[1])] if ll is not None else None for ll in response]
def parse_geopos_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire GEOPOS → legacy RESP2 ``list[tuple(float, float) | None]``.
Matches today's RESP2-wire callback shape (tuple coordinates).
"""
return [(float(ll[0]), float(ll[1])) if ll is not None else None for ll in response]
def parse_lcs_idx_unified(response, **options):
"""LCS with IDX → unified ``dict``.
Used for the ``legacy_responses=False`` overlay on RESP2 wire to mirror
RESP3's native ``dict`` shape. Non-IDX responses (``bytes`` / ``int``)
pass through unchanged.
"""
if isinstance(response, list):
it = iter(response)
return {str_if_bytes(key): value for key, value in zip(it, it)}
if isinstance(response, dict):
return {str_if_bytes(key): value for key, value in response.items()}
return response
def parse_lcs_idx_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire LCS with IDX → legacy RESP2 flat list shape.
Reproduces today's RESP2 raw output (``[b"matches", [...], b"len", n]``).
Non-IDX responses pass through unchanged.
"""
if not isinstance(response, dict):
return response
out: list = []
for key, value in response.items():
out.append(key)
out.append(value)
return out
def parse_client_trackinginfo_unified(response, **options):
"""CLIENT TRACKINGINFO → unified ``dict[str, Any]``.
Accepts either RESP2's flat ``[label, value, ...]`` list or RESP3's
native ``dict`` and returns a ``dict`` with ``str`` keys.
"""
if isinstance(response, dict):
data = {str_if_bytes(key): value for key, value in response.items()}
else:
data = {
str_if_bytes(key): value
for key, value in zip(response[::2], response[1::2])
}
if "flags" in data:
data["flags"] = [str_if_bytes(flag) for flag in data["flags"]]
if "prefixes" in data:
data["prefixes"] = [str_if_bytes(prefix) for prefix in data["prefixes"]]
return data
def parse_client_trackinginfo_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire CLIENT TRACKINGINFO → legacy RESP2 flat ``list``.
Mirrors today's RESP2-wire callback (``list(map(str_if_bytes, r))``):
labels are decoded to ``str`` while values are preserved as-is.
"""
if not isinstance(response, dict):
return list(map(str_if_bytes, response))
out: list = []
for key, value in response.items():
out.append(str_if_bytes(key))
out.append(value)
return out
def sort_return_tuples(response, **options):
"""
If ``groups`` is specified, return the response as a list of
n-element tuples with n being the value found in options['groups']
"""
if not response or not options.get("groups"):
return response
n = options["groups"]
return list(zip(*[response[i::n] for i in range(n)]))
def parse_stream_list(response, **options):
if response is None:
return None
data = []
for r in response:
if r is not None:
if "claim_min_idle_time" in options:
data.append((r[0], pairs_to_dict(r[1]), *r[2:]))
else:
data.append((r[0], pairs_to_dict(r[1])))
else:
data.append((None, None))
return data
def pairs_to_dict_with_str_keys(response):
return pairs_to_dict(response, decode_keys=True)
def parse_list_of_dicts(response):
return list(map(pairs_to_dict_with_str_keys, response))
def parse_xclaim(response, **options):
if options.get("parse_justid", False):
return response
return parse_stream_list(response)
def parse_xautoclaim(response, **options):
if options.get("parse_justid", False):
return response[1]
response[1] = parse_stream_list(response[1])
return response
def parse_arinfo(response, **options):
if isinstance(response, list):
return pairs_to_dict(response, decode_keys=True)
return {str_if_bytes(k): v for k, v in response.items()}
def parse_xinfo_stream(response, **options):
if isinstance(response, list):
data = pairs_to_dict(response, decode_keys=True)
else:
data = {str_if_bytes(k): v for k, v in response.items()}
if not options.get("full", False):
first = data.get("first-entry")
if first is not None and first[0] is not None:
data["first-entry"] = (first[0], pairs_to_dict(first[1]))
last = data["last-entry"]
if last is not None and last[0] is not None:
data["last-entry"] = (last[0], pairs_to_dict(last[1]))
else:
data["entries"] = {_id: pairs_to_dict(entry) for _id, entry in data["entries"]}
if len(data["groups"]) > 0 and isinstance(data["groups"][0], list):
data["groups"] = [
pairs_to_dict(group, decode_keys=True) for group in data["groups"]
]
for g in data["groups"]:
if g["consumers"] and g["consumers"][0] is not None:
g["consumers"] = [
pairs_to_dict(c, decode_keys=True) for c in g["consumers"]
]
else:
data["groups"] = [
{str_if_bytes(k): v for k, v in group.items()}
for group in data["groups"]
]
return data
def parse_xread(response, **options):
if response is None:
return []
return [[r[0], parse_stream_list(r[1], **options)] for r in response]
def parse_xread_resp3(response, **options):
if response is None:
return {}
return {
key: [parse_stream_list(value, **options)] for key, value in response.items()
}
def parse_xread_unified(response, **options):
"""XREAD/XREADGROUP → unified ``dict[stream, list[tuple[id, dict]]]``.
Accepts either RESP2 (``list[[stream, entries]]``) or RESP3
(``dict[stream, entries]``) wire shape. Empty result is ``{}``.
"""
if not response:
return {}
if isinstance(response, dict):
return {
key: parse_stream_list(value, **options) for key, value in response.items()
}
return {
stream: parse_stream_list(entries, **options) for stream, entries in response
}
def parse_xread_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire XREAD/XREADGROUP → legacy RESP2 ``list[[stream, entries]]``.
Empty result ``{}`` is converted to ``[]`` to match today's RESP2 shape.
"""
if not response:
return []
return [
[key, parse_stream_list(value, **options)] for key, value in response.items()
]
def parse_xpending(response, **options):
if options.get("parse_detail", False):
return parse_xpending_range(response)
consumers = [{"name": n, "pending": int(p)} for n, p in response[3] or []]
return {
"pending": response[0],
"min": response[1],
"max": response[2],
"consumers": consumers,
}
def parse_xpending_range(response):
k = ("message_id", "consumer", "time_since_delivered", "times_delivered")
return [dict(zip(k, r)) for r in response]
def float_or_none(response):
if response is None:
return None
return float(response)
def bool_ok(response, **options):
return str_if_bytes(response) == "OK"
def parse_zadd(response, **options):
if response is None:
return None
if options.get("as_score"):
return float(response)
return int(response)
def parse_client_list(response, **options):
clients = []
for c in str_if_bytes(response).splitlines():
client_dict = {}
tokens = c.split(" ")
last_key = None
for token in tokens:
if "=" in token:
# Values might contain '='
key, value = token.split("=", 1)
client_dict[key] = value
last_key = key
else:
# Values may include spaces. For instance, when running Redis via a Unix socket — such as
# "/tmp/redis sock/redis.sock" — the addr or laddr field will include a space.
client_dict[last_key] += " " + token
if client_dict:
clients.append(client_dict)
return clients
def parse_config_get(response, **options):
response = [str_if_bytes(i) if i is not None else None for i in response]
return response and pairs_to_dict(response) or {}
def parse_config_get_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire CONFIG GET → today's RESP2 ``dict[str, str]`` shape.
On RESP3 the server returns a map; convert both keys and values via
``str_if_bytes`` so callers using ``r.config_get()["timeout"]``
keep working when the wire is RESP3 with ``legacy_responses=True``.
"""
if not response:
return {}
return {
str_if_bytes(key) if key is not None else None: (
str_if_bytes(value) if value is not None else None
)
for key, value in response.items()
}
def parse_scan(response, **options):
cursor, r = response
return int(cursor), r
def parse_hscan(response, **options):
cursor, r = response
no_values = options.get("no_values", False)
if no_values:
payload = r or []
else:
payload = r and pairs_to_dict(r) or {}
return int(cursor), payload
def parse_zscan(response, **options):
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
cursor, r = response
it = iter(r)
return int(cursor), list(zip(it, map(score_cast_func, it)))
def parse_zscan_unified(response, **options):
score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float))
cursor, r = response
it = iter(r)
return int(cursor), [
[value, score_cast_func(float(score))] for value, score in zip(it, it)
]
def parse_zmscore(response, **options):
# zmscore: list of scores (double precision floating point number) or nil
return [float(score) if score is not None else None for score in response]
def parse_slowlog_get(response, **options):
space = " " if options.get("decode_responses", False) else b" "
def parse_item(item):
result = {"id": item[0], "start_time": int(item[1]), "duration": int(item[2])}
# Redis Enterprise injects another entry at index [3], which has
# the complexity info (i.e. the value N in case the command has
# an O(N) complexity) instead of the command.
if isinstance(item[3], list):
result["command"] = space.join(item[3])
# These fields are optional, depends on environment.
if len(item) >= 6:
result["client_address"] = item[4]
result["client_name"] = item[5]
else:
result["complexity"] = item[3]
result["command"] = space.join(item[4])
# These fields are optional, depends on environment.
if len(item) >= 7:
result["client_address"] = item[5]
result["client_name"] = item[6]
return result
return [parse_item(item) for item in response]
def parse_stralgo(response, **options):
"""
Parse the response from `STRALGO` command.
Without modifiers the returned value is string.
When LEN is given the command returns the length of the result
(i.e integer).
When IDX is given the command returns a dictionary with the LCS
length and all the ranges in both the strings, start and end
offset for each string, where there are matches.
When WITHMATCHLEN is given, each array representing a match will
also have the length of the match at the beginning of the array.
"""
if options.get("len", False):
return int(response)
if options.get("idx", False):
if options.get("withmatchlen", False):
matches = [
[(int(match[-1]))] + list(map(tuple, match[:-1]))
for match in response[1]
]
else:
matches = [list(map(tuple, match)) for match in response[1]]
return {
str_if_bytes(response[0]): matches,
str_if_bytes(response[2]): int(response[3]),
}
return str_if_bytes(response)
def parse_stralgo_unified(response, **options):
"""
Parse STRALGO into the approved unified shape.
The legacy parser returns tuple ranges for IDX responses. Unified
responses use list ranges so RESP2 and RESP3 produce the same value.
"""
if options.get("len", False):
return int(response)
if options.get("idx", False):
if options.get("withmatchlen", False):
matches = [
[int(match[-1])] + [list(m) for m in match[:-1]]
for match in response[1]
]
else:
matches = [[list(m) for m in match] for match in response[1]]
return {
str_if_bytes(response[0]): matches,
str_if_bytes(response[2]): int(response[3]),
}
return str_if_bytes(response)
def parse_stralgo_resp3_unified(response, **options):
"""Parse RESP3 STRALGO into the same value as ``parse_stralgo_unified``."""
if options.get("len", False):
return int(response)
if options.get("idx", False):
if not isinstance(response, dict):
return str_if_bytes(response)
raw_matches = response.get("matches", response.get(b"matches", []))
raw_len = response.get("len", response.get(b"len", 0))
if options.get("withmatchlen", False):
matches = [
[int(match[-1])] + [list(m) for m in match[:-1]]
for match in raw_matches
]
else:
matches = [[list(m) for m in match] for match in raw_matches]
return {"matches": matches, "len": int(raw_len)}
return str_if_bytes(response)
def parse_cluster_info(response, **options):
response = str_if_bytes(response)
return dict(line.split(":") for line in response.splitlines() if line)
def _parse_node_line(line):
line_items = line.split(" ")
node_id, addr, flags, master_id, ping, pong, epoch, connected = line.split(" ")[:8]
ip = addr.split("@")[0]
hostname = addr.split("@")[1].split(",")[1] if "@" in addr and "," in addr else ""
node_dict = {
"node_id": node_id,
"hostname": hostname,
"flags": flags,
"master_id": master_id,
"last_ping_sent": ping,
"last_pong_rcvd": pong,
"epoch": epoch,
"slots": [],
"migrations": [],
"connected": True if connected == "connected" else False,
}
if len(line_items) >= 9:
slots, migrations = _parse_slots(line_items[8:])
node_dict["slots"], node_dict["migrations"] = slots, migrations
return ip, node_dict
def _parse_slots(slot_ranges):
slots, migrations = [], []
for s_range in slot_ranges:
if "->-" in s_range:
slot_id, dst_node_id = s_range[1:-1].split("->-", 1)
migrations.append(
{"slot": slot_id, "node_id": dst_node_id, "state": "migrating"}
)
elif "-<-" in s_range:
slot_id, src_node_id = s_range[1:-1].split("-<-", 1)
migrations.append(
{"slot": slot_id, "node_id": src_node_id, "state": "importing"}
)
else:
s_range = [sl for sl in s_range.split("-")]
slots.append(s_range)
return slots, migrations
def parse_cluster_nodes(response, **options):
"""
@see: https://redis.io/commands/cluster-nodes # string / bytes
@see: https://redis.io/commands/cluster-replicas # list of string / bytes
"""
if isinstance(response, (str, bytes)):
response = response.splitlines()
return dict(_parse_node_line(str_if_bytes(node)) for node in response)
def parse_geosearch_generic(response, **options):
"""
Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER'
commands according to 'withdist', 'withhash' and 'withcoord' labels.
"""
try:
if options["store"] or options["store_dist"]:
# `store` and `store_dist` can't be combined
# with other command arguments.
# relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER'
return response
except KeyError: # it means the command was sent via execute_command
return response
if not isinstance(response, list):
response_list = [response]
else:
response_list = response
if not options["withdist"] and not options["withcoord"] and not options["withhash"]:
# just a bunch of places
return response_list
cast = {
"withdist": float,
"withcoord": lambda ll: (float(ll[0]), float(ll[1])),
"withhash": int,
}
# zip all output results with each casting function to get
# the properly native Python value.
f = [lambda x: x]
f += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]]
return [list(map(lambda fv: fv[0](fv[1]), zip(f, r))) for r in response_list]
def parse_geosearch_generic_unified(response, **options):
"""
Parse GEOSEARCH/GEORADIUS responses using tuple coordinates.
"""
try:
if options["store"] or options["store_dist"]:
return response
except KeyError:
return response
response_list = response if isinstance(response, list) else [response]
if not options["withdist"] and not options["withcoord"] and not options["withhash"]:
return response_list
cast = {
"withdist": float,
"withcoord": lambda ll: (float(ll[0]), float(ll[1])),
"withhash": int,
}
funcs = [lambda x: x]
funcs += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]]
return [list(map(lambda fv: fv[0](fv[1]), zip(funcs, r))) for r in response_list]
def parse_command(response, **options):
commands = {}
for command in response:
cmd_dict = {}
cmd_name = str_if_bytes(command[0])
cmd_dict["name"] = cmd_name
cmd_dict["arity"] = int(command[1])
cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]]
cmd_dict["first_key_pos"] = command[3]
cmd_dict["last_key_pos"] = command[4]
cmd_dict["step_count"] = command[5]
if len(command) > 6:
cmd_dict["acl_categories"] = [
str_if_bytes(category) for category in command[6]
]
if len(command) > 7:
cmd_dict["tips"] = command[7]
cmd_dict["key_specifications"] = command[8]
cmd_dict["subcommands"] = command[9]
commands[cmd_name] = cmd_dict
return commands
def parse_command_unified(response, **options):
commands = {}
for command in response:
cmd_dict = {}
cmd_name = str_if_bytes(command[0])
cmd_dict["name"] = cmd_name
cmd_dict["arity"] = int(command[1])
cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]}
cmd_dict["first_key_pos"] = command[3]
cmd_dict["last_key_pos"] = command[4]
cmd_dict["step_count"] = command[5]
if len(command) > 6:
cmd_dict["acl_categories"] = {str_if_bytes(c) for c in command[6]}
if len(command) > 7:
cmd_dict["tips"] = command[7]
cmd_dict["key_specifications"] = command[8]
cmd_dict["subcommands"] = command[9]
commands[cmd_name] = cmd_dict
return commands
def parse_command_resp3(response, **options):
commands = {}
for command in response:
cmd_dict = {}
cmd_name = str_if_bytes(command[0])
cmd_dict["name"] = cmd_name
cmd_dict["arity"] = command[1]
cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]}
cmd_dict["first_key_pos"] = command[3]
cmd_dict["last_key_pos"] = command[4]
cmd_dict["step_count"] = command[5]
cmd_dict["acl_categories"] = command[6]
if len(command) > 7:
cmd_dict["tips"] = command[7]
cmd_dict["key_specifications"] = command[8]
cmd_dict["subcommands"] = command[9]
commands[cmd_name] = cmd_dict
return commands
def parse_pubsub_numsub(response, **options):
return list(zip(response[0::2], response[1::2]))
def parse_client_kill(response, **options):
if isinstance(response, int):
return response
return str_if_bytes(response) == "OK"
def parse_acl_getuser(response, **options):
if response is None:
return None
if isinstance(response, list):
data = pairs_to_dict(response, decode_keys=True)
else:
data = {str_if_bytes(key): value for key, value in response.items()}
# convert everything but user-defined data in 'keys' to native strings
data["flags"] = list(map(str_if_bytes, data["flags"]))
data["passwords"] = list(map(str_if_bytes, data["passwords"]))
data["commands"] = str_if_bytes(data["commands"])
if isinstance(data["keys"], str) or isinstance(data["keys"], bytes):
data["keys"] = list(str_if_bytes(data["keys"]).split(" "))
if data["keys"] == [""]:
data["keys"] = []
if "channels" in data:
if isinstance(data["channels"], str) or isinstance(data["channels"], bytes):
data["channels"] = list(str_if_bytes(data["channels"]).split(" "))
if data["channels"] == [""]:
data["channels"] = []
if "selectors" in data:
if data["selectors"] != [] and isinstance(data["selectors"][0], list):
data["selectors"] = [
list(map(str_if_bytes, selector)) for selector in data["selectors"]
]
elif data["selectors"] != []:
data["selectors"] = [
{str_if_bytes(k): str_if_bytes(v) for k, v in selector.items()}
for selector in data["selectors"]
]
# split 'commands' into separate 'categories' and 'commands' lists
commands, categories = [], []
for command in data["commands"].split(" "):
categories.append(command) if "@" in command else commands.append(command)
data["commands"] = commands
data["categories"] = categories
data["enabled"] = "on" in data["flags"]
return data
def parse_acl_log(response, **options):
if response is None:
return None
if isinstance(response, list):
data = []
for log in response:
log_data = pairs_to_dict(log, True, True)
client_info = log_data.get("client-info", "")
log_data["client-info"] = parse_client_info(client_info)
# float() is lossy comparing to the "double" in C
log_data["age-seconds"] = float(log_data["age-seconds"])
data.append(log_data)
else:
data = bool_ok(response)
return data
def parse_acl_log_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire ACL LOG → today's RESP2 parsed shape.
Each log entry arrives as a ``dict`` on RESP3 wire instead of a flat
list of pairs; convert ``client-info`` from a string blob into the
parsed ``dict`` and ``age-seconds`` to ``float`` so the Python shape
matches what :func:`parse_acl_log` produces from RESP2 wire.
Also used as the unified callback (Set D): the legacy and unified
shapes coincide for ACL LOG.
"""
if response is None:
return None
if not isinstance(response, list):
return bool_ok(response)
data = []
for log in response:
if isinstance(log, dict):
log_data = {str_if_bytes(k): v for k, v in log.items()}
else:
log_data = pairs_to_dict(log, True, True)
client_info = log_data.get("client-info", "")
log_data["client-info"] = parse_client_info(client_info)
log_data["age-seconds"] = float(log_data["age-seconds"])
data.append(log_data)
return data
def parse_acl_log_resp3_unified(response, **options):
"""Parse RESP3 ACL LOG into the approved unified shape."""
if response is None:
return None
if not isinstance(response, list):
return bool_ok(response)
data = []
for entry in response:
if isinstance(entry, dict):
log_data = {str_if_bytes(k): v for k, v in entry.items()}
else:
log_data = pairs_to_dict(entry, True, True)
if "age-seconds" in log_data:
log_data["age-seconds"] = float(log_data["age-seconds"])
if "client-info" in log_data:
log_data["client-info"] = parse_client_info(log_data["client-info"])
for key, value in list(log_data.items()):
if key not in ("age-seconds", "client-info"):
log_data[key] = str_if_bytes(value)
data.append(log_data)
return data
def parse_acl_getuser_unified(response, **options):
"""ACL GETUSER → unified shape with selectors as ``list[dict]``.
On RESP2 wire each selector arrives as a flat ``[k, v, k, v, ]``
list; pair them into dicts to match the RESP3 wire shape.
"""
data = parse_acl_getuser(response, **options)
if data is None:
return data
selectors = data.get("selectors")
if selectors and isinstance(selectors[0], list):
data["selectors"] = [
dict(zip(selector[0::2], selector[1::2])) for selector in selectors
]
return data
def parse_acl_getuser_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire ACL GETUSER → today's RESP2 selectors as flat lists.
Each selector arrives as a ``dict`` on RESP3 wire; flatten back to
the interleaved ``[k, v, k, v, ]`` form produced by RESP2 wire.
"""
data = parse_acl_getuser(response, **options)
if data is None:
return data
selectors = data.get("selectors")
if selectors and isinstance(selectors[0], dict):
data["selectors"] = [
[item for kv in selector.items() for item in kv] for selector in selectors
]
return data
def parse_client_info(value):
"""
Parsing client-info in ACL Log in following format.
"key1=value1 key2=value2 key3=value3"
"""
client_info = {}
for info in str_if_bytes(value).strip().split():
key, value = info.split("=")
client_info[key] = value
# Those fields are defined as int in networking.c
for int_key in {
"id",
"age",
"idle",
"db",
"sub",
"psub",
"multi",
"qbuf",
"qbuf-free",
"obl",
"argv-mem",
"oll",
"omem",
"tot-mem",
}:
if int_key in client_info:
client_info[int_key] = int(client_info[int_key])
return client_info
def parse_set_result(response, **options):
"""
Handle SET result since GET argument is available since Redis 6.2.
Parsing SET result into:
- BOOL
- String when GET argument is used
"""
if options.get("get"):
# Redis will return a getCommand result.
# See `setGenericCommand` in t_string.c
return response
return response and str_if_bytes(response) == "OK"
def parse_function_list_unified(response, **options):
"""FUNCTION LIST → unified ``list[dict]`` with bytes keys.
Accepts either RESP2 wire (``list[list]`` of flat ``[k, v, k, v, ]``
pairs, with the nested ``b"functions"`` value also a flat list of
flat lists) or RESP3 wire (``list[dict]`` already in nested-map
form). Both are normalised to ``list[dict]``.
"""
if response is None:
return None
result = []
for lib in response:
if isinstance(lib, dict):
result.append(lib)
continue
lib_dict = pairs_to_dict(lib)
func_key = b"functions" if b"functions" in lib_dict else "functions"
if func_key in lib_dict:
functions = lib_dict[func_key]
lib_dict[func_key] = [
func if isinstance(func, dict) else pairs_to_dict(func)
for func in functions
]
result.append(lib_dict)
return result
def parse_function_list_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire FUNCTION LIST → today's RESP2 ``list[list]`` shape.
Each library and each nested function arrives as a ``dict``; flatten
them back to interleaved ``[k, v, k, v, ]`` lists so the Python
shape matches what RESP2 wire produces natively.
"""
if response is None:
return None
result = []
for lib in response:
if not isinstance(lib, dict):
result.append(lib)
continue
flat = []
for key, value in lib.items():
flat.append(key)
if key == b"functions" or key == "functions":
flat.append(
[
[item for kv in func.items() for item in kv]
if isinstance(func, dict)
else func
for func in value
]
)
else:
flat.append(value)
result.append(flat)
return result
def parse_cluster_links_unified(response, **options):
"""CLUSTER LINKS → unified ``list[dict]`` with string keys.
Accepts either RESP2 wire (``list[list]`` of flat pairs) or RESP3
wire (``list[dict]``). Both are normalised to ``list[dict]``.
"""
if response is None:
return None
return [
{str_if_bytes(k): v for k, v in item.items()}
if isinstance(item, dict)
else pairs_to_dict(item, decode_keys=True)
for item in response
]
def parse_cluster_links_resp3_to_resp2_legacy(response, **options):
"""RESP3-wire CLUSTER LINKS → today's RESP2 ``list[list]`` shape.
Each link arrives as a ``dict`` with bytes keys; flatten back to
interleaved ``[k, v, k, v, ]`` lists so the Python shape matches
what RESP2 wire produces natively.
"""
if response is None:
return None
return [
[item for kv in link.items() for item in kv] if isinstance(link, dict) else link
for link in response
]
def string_keys_to_dict(key_string, callback):
return dict.fromkeys(key_string.split(), callback)
# The command-to-callback mapping dictionaries (``_RedisCallbacks``,
# ``_RedisCallbacksRESP2``, ``_RedisCallbacksRESP3``, …) and the
# ``get_response_callbacks`` selector live in
# ``redis/_parsers/response_callbacks.py``. They are re-exported below for
# backward compatibility so existing imports of the form
# ``from redis._parsers.helpers import _RedisCallbacks`` keep working. The
# import is placed at module bottom to avoid a circular import (the
# response_callbacks module imports parser helpers defined above).
# isort: off
from .response_callbacks import ( # noqa: E402, F401
_RedisCallbacks,
_RedisCallbacksRESP2,
_RedisCallbacksRESP2Unified,
_RedisCallbacksRESP3,
_RedisCallbacksRESP3Unified,
_RedisCallbacksRESP3toRESP2Legacy,
get_response_callbacks,
)
# isort: on