Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/_parsers/helpers.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

817 statements  

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 for g in data["groups"]: 

809 if g["consumers"] and g["consumers"][0] is not None: 

810 g["consumers"] = [ 

811 {str_if_bytes(k): v for k, v in c.items()} 

812 for c in g["consumers"] 

813 ] 

814 return data 

815 

816 

817def parse_xread(response, **options): 

818 if response is None: 

819 return [] 

820 return [[r[0], parse_stream_list(r[1], **options)] for r in response] 

821 

822 

823def parse_xread_resp3(response, **options): 

824 if response is None: 

825 return {} 

826 return { 

827 key: [parse_stream_list(value, **options)] for key, value in response.items() 

828 } 

829 

830 

831def parse_xread_unified(response, **options): 

832 """XREAD/XREADGROUP → unified ``dict[stream, list[tuple[id, dict]]]``. 

833 

834 Accepts either RESP2 (``list[[stream, entries]]``) or RESP3 

835 (``dict[stream, entries]``) wire shape. Empty result is ``{}``. 

836 """ 

837 if not response: 

838 return {} 

839 if isinstance(response, dict): 

840 return { 

841 key: parse_stream_list(value, **options) for key, value in response.items() 

842 } 

843 return { 

844 stream: parse_stream_list(entries, **options) for stream, entries in response 

845 } 

846 

847 

848def parse_xread_resp3_to_resp2_legacy(response, **options): 

849 """RESP3-wire XREAD/XREADGROUP → legacy RESP2 ``list[[stream, entries]]``. 

850 

851 Empty result ``{}`` is converted to ``[]`` to match today's RESP2 shape. 

852 """ 

853 if not response: 

854 return [] 

855 return [ 

856 [key, parse_stream_list(value, **options)] for key, value in response.items() 

857 ] 

858 

859 

860def parse_xpending(response, **options): 

861 if options.get("parse_detail", False): 

862 return parse_xpending_range(response) 

863 consumers = [{"name": n, "pending": int(p)} for n, p in response[3] or []] 

864 return { 

865 "pending": response[0], 

866 "min": response[1], 

867 "max": response[2], 

868 "consumers": consumers, 

869 } 

870 

871 

872def parse_xpending_range(response): 

873 k = ("message_id", "consumer", "time_since_delivered", "times_delivered") 

874 return [dict(zip(k, r)) for r in response] 

875 

876 

877def float_or_none(response): 

878 if response is None: 

879 return None 

880 return float(response) 

881 

882 

883def bool_ok(response, **options): 

884 return str_if_bytes(response) == "OK" 

885 

886 

887def parse_zadd(response, **options): 

888 if response is None: 

889 return None 

890 if options.get("as_score"): 

891 return float(response) 

892 return int(response) 

893 

894 

895def _parse_client_info_fields(value): 

896 """Tokenize a single ``key=value`` client-info blob into a dict. 

897 

898 The server emits this space-separated format for each client in 

899 ``CLIENT INFO`` and ``CLIENT LIST``, and inside ``ACL LOG`` entries. 

900 Two quirks have to be preserved: a value may contain ``=`` (a client 

901 name set to ``foo=bar``), so only the first ``=`` splits key from value; 

902 and a value may contain spaces (a Unix-socket ``addr``/``laddr`` path 

903 such as ``/tmp/redis sock/redis.sock``), so a token with no ``=`` is 

904 reattached to the previous value. ``last_key is None`` guards the leading 

905 and empty/whitespace-only cases, which yield an empty dict rather than 

906 raising. 

907 """ 

908 fields = {} 

909 last_key = None 

910 for token in value.split(" "): 

911 if "=" in token: 

912 key, val = token.split("=", 1) 

913 fields[key] = val 

914 last_key = key 

915 elif last_key is not None: 

916 # A token without ``=`` continues the previous value, since a 

917 # Unix-socket path may contain spaces. Empty tokens (from two or 

918 # more consecutive spaces in the path) are reattached the same 

919 # way, so a run of spaces round-trips verbatim instead of being 

920 # collapsed. Tokens before the first ``key=`` (``last_key is 

921 # None``, i.e. a leading space or an empty/whitespace-only blob) 

922 # are dropped, yielding an empty dict rather than raising. 

923 fields[last_key] += " " + token 

924 return fields 

925 

926 

927def parse_client_list(response, **options): 

928 if options.pop("as_iter", False): 

929 return _client_list_iter(response) 

930 clients = [] 

931 for c in str_if_bytes(response).splitlines(): 

932 client_dict = _parse_client_info_fields(c) 

933 if client_dict: 

934 clients.append(client_dict) 

935 return clients 

936 

937 

938def _client_list_iter(response): 

939 """ 

940 Yield one CLIENT LIST record at a time instead of building the full 

941 ``list[dict]`` up front, so a caller processing records one at a time 

942 doesn't need all of them held in memory simultaneously. The full reply 

943 is still buffered off the socket before this runs - that part is 

944 unavoidable given how RESP bulk-string framing (and hiredis) works - 

945 so this only trims the per-record dict/string overhead, not the raw 

946 reply size. 

947 

948 Line boundaries are scanned for lazily (rather than via ``splitlines()``) 

949 so the generator never materializes a list of all lines either - that 

950 list would otherwise be held for the generator's whole lifetime, right 

951 alongside the decoded reply, defeating the point of iterating. 

952 """ 

953 text = str_if_bytes(response) 

954 # With decode_responses=False, str_if_bytes decodes a full copy of the 

955 # reply. response is a parameter of this generator, so it stays alive 

956 # in this frame for as long as the generator does; drop it once decoded 

957 # so the original buffer doesn't sit alongside the decoded one for the 

958 # whole iteration. 

959 response = None 

960 end = len(text) 

961 start = 0 

962 while start < end: 

963 newline = text.find("\n", start) 

964 if newline == -1: 

965 newline = end 

966 client_dict = _parse_client_info_fields(text[start:newline]) 

967 start = newline + 1 

968 if client_dict: 

969 yield client_dict 

970 

971 

972def parse_config_get(response, **options): 

973 response = [str_if_bytes(i) if i is not None else None for i in response] 

974 return response and pairs_to_dict(response) or {} 

975 

976 

977def parse_config_get_resp3_to_resp2_legacy(response, **options): 

978 """RESP3-wire CONFIG GET → today's RESP2 ``dict[str, str]`` shape. 

979 

980 On RESP3 the server returns a map; convert both keys and values via 

981 ``str_if_bytes`` so callers using ``r.config_get()["timeout"]`` 

982 keep working when the wire is RESP3 with ``legacy_responses=True``. 

983 """ 

984 if not response: 

985 return {} 

986 return { 

987 str_if_bytes(key) if key is not None else None: ( 

988 str_if_bytes(value) if value is not None else None 

989 ) 

990 for key, value in response.items() 

991 } 

992 

993 

994def parse_scan(response, **options): 

995 cursor, r = response 

996 return int(cursor), r 

997 

998 

999def parse_hscan(response, **options): 

1000 cursor, r = response 

1001 no_values = options.get("no_values", False) 

1002 if no_values: 

1003 payload = r or [] 

1004 else: 

1005 payload = r and pairs_to_dict(r) or {} 

1006 return int(cursor), payload 

1007 

1008 

1009def parse_zscan(response, **options): 

1010 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float)) 

1011 cursor, r = response 

1012 it = iter(r) 

1013 return int(cursor), list(zip(it, map(score_cast_func, it))) 

1014 

1015 

1016def parse_zscan_unified(response, **options): 

1017 score_cast_func = _wrap_score_cast_func(options.get("score_cast_func", float)) 

1018 cursor, r = response 

1019 it = iter(r) 

1020 return int(cursor), [ 

1021 [value, score_cast_func(float(score))] for value, score in zip(it, it) 

1022 ] 

1023 

1024 

1025def parse_zmscore(response, **options): 

1026 # zmscore: list of scores (double precision floating point number) or nil 

1027 return [float(score) if score is not None else None for score in response] 

1028 

1029 

1030def parse_slowlog_get(response, **options): 

1031 space = " " if options.get("decode_responses", False) else b" " 

1032 

1033 def parse_item(item): 

1034 result = {"id": item[0], "start_time": int(item[1]), "duration": int(item[2])} 

1035 # Redis Enterprise injects another entry at index [3], which has 

1036 # the complexity info (i.e. the value N in case the command has 

1037 # an O(N) complexity) instead of the command. 

1038 if isinstance(item[3], list): 

1039 result["command"] = space.join(item[3]) 

1040 

1041 # These fields are optional, depends on environment. 

1042 if len(item) >= 6: 

1043 result["client_address"] = item[4] 

1044 result["client_name"] = item[5] 

1045 else: 

1046 result["complexity"] = item[3] 

1047 result["command"] = space.join(item[4]) 

1048 

1049 # These fields are optional, depends on environment. 

1050 if len(item) >= 7: 

1051 result["client_address"] = item[5] 

1052 result["client_name"] = item[6] 

1053 

1054 return result 

1055 

1056 return [parse_item(item) for item in response] 

1057 

1058 

1059def parse_stralgo(response, **options): 

1060 """ 

1061 Parse the response from `STRALGO` command. 

1062 Without modifiers the returned value is string. 

1063 When LEN is given the command returns the length of the result 

1064 (i.e integer). 

1065 When IDX is given the command returns a dictionary with the LCS 

1066 length and all the ranges in both the strings, start and end 

1067 offset for each string, where there are matches. 

1068 When WITHMATCHLEN is given, each array representing a match will 

1069 also have the length of the match at the beginning of the array. 

1070 """ 

1071 if options.get("len", False): 

1072 return int(response) 

1073 if options.get("idx", False): 

1074 if options.get("withmatchlen", False): 

1075 matches = [ 

1076 [(int(match[-1]))] + list(map(tuple, match[:-1])) 

1077 for match in response[1] 

1078 ] 

1079 else: 

1080 matches = [list(map(tuple, match)) for match in response[1]] 

1081 return { 

1082 str_if_bytes(response[0]): matches, 

1083 str_if_bytes(response[2]): int(response[3]), 

1084 } 

1085 return str_if_bytes(response) 

1086 

1087 

1088def parse_stralgo_unified(response, **options): 

1089 """ 

1090 Parse STRALGO into the approved unified shape. 

1091 

1092 The legacy parser returns tuple ranges for IDX responses. Unified 

1093 responses use list ranges so RESP2 and RESP3 produce the same value. 

1094 """ 

1095 if options.get("len", False): 

1096 return int(response) 

1097 if options.get("idx", False): 

1098 if options.get("withmatchlen", False): 

1099 matches = [ 

1100 [int(match[-1])] + [list(m) for m in match[:-1]] 

1101 for match in response[1] 

1102 ] 

1103 else: 

1104 matches = [[list(m) for m in match] for match in response[1]] 

1105 return { 

1106 str_if_bytes(response[0]): matches, 

1107 str_if_bytes(response[2]): int(response[3]), 

1108 } 

1109 return str_if_bytes(response) 

1110 

1111 

1112def parse_stralgo_resp3_unified(response, **options): 

1113 """Parse RESP3 STRALGO into the same value as ``parse_stralgo_unified``.""" 

1114 if options.get("len", False): 

1115 return int(response) 

1116 if options.get("idx", False): 

1117 if not isinstance(response, dict): 

1118 return str_if_bytes(response) 

1119 raw_matches = response.get("matches", response.get(b"matches", [])) 

1120 raw_len = response.get("len", response.get(b"len", 0)) 

1121 if options.get("withmatchlen", False): 

1122 matches = [ 

1123 [int(match[-1])] + [list(m) for m in match[:-1]] 

1124 for match in raw_matches 

1125 ] 

1126 else: 

1127 matches = [[list(m) for m in match] for match in raw_matches] 

1128 return {"matches": matches, "len": int(raw_len)} 

1129 return str_if_bytes(response) 

1130 

1131 

1132def parse_cluster_info(response, **options): 

1133 response = str_if_bytes(response) 

1134 return dict(line.split(":") for line in response.splitlines() if line) 

1135 

1136 

1137def _parse_node_line(line): 

1138 line_items = line.split(" ") 

1139 node_id, addr, flags, master_id, ping, pong, epoch, connected = line.split(" ")[:8] 

1140 ip = addr.split("@")[0] 

1141 hostname = addr.split("@")[1].split(",")[1] if "@" in addr and "," in addr else "" 

1142 node_dict = { 

1143 "node_id": node_id, 

1144 "hostname": hostname, 

1145 "flags": flags, 

1146 "master_id": master_id, 

1147 "last_ping_sent": ping, 

1148 "last_pong_rcvd": pong, 

1149 "epoch": epoch, 

1150 "slots": [], 

1151 "migrations": [], 

1152 "connected": True if connected == "connected" else False, 

1153 } 

1154 if len(line_items) >= 9: 

1155 slots, migrations = _parse_slots(line_items[8:]) 

1156 node_dict["slots"], node_dict["migrations"] = slots, migrations 

1157 return ip, node_dict 

1158 

1159 

1160def _parse_slots(slot_ranges): 

1161 slots, migrations = [], [] 

1162 for s_range in slot_ranges: 

1163 if "->-" in s_range: 

1164 slot_id, dst_node_id = s_range[1:-1].split("->-", 1) 

1165 migrations.append( 

1166 {"slot": slot_id, "node_id": dst_node_id, "state": "migrating"} 

1167 ) 

1168 elif "-<-" in s_range: 

1169 slot_id, src_node_id = s_range[1:-1].split("-<-", 1) 

1170 migrations.append( 

1171 {"slot": slot_id, "node_id": src_node_id, "state": "importing"} 

1172 ) 

1173 else: 

1174 s_range = [sl for sl in s_range.split("-")] 

1175 slots.append(s_range) 

1176 

1177 return slots, migrations 

1178 

1179 

1180def parse_cluster_nodes(response, **options): 

1181 """ 

1182 @see: https://redis.io/commands/cluster-nodes # string / bytes 

1183 @see: https://redis.io/commands/cluster-replicas # list of string / bytes 

1184 """ 

1185 if isinstance(response, (str, bytes)): 

1186 response = response.splitlines() 

1187 return dict(_parse_node_line(str_if_bytes(node)) for node in response) 

1188 

1189 

1190def parse_geosearch_generic(response, **options): 

1191 """ 

1192 Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER' 

1193 commands according to 'withdist', 'withhash' and 'withcoord' labels. 

1194 """ 

1195 try: 

1196 if options["store"] or options["store_dist"]: 

1197 # `store` and `store_dist` can't be combined 

1198 # with other command arguments. 

1199 # relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER' 

1200 return response 

1201 except KeyError: # it means the command was sent via execute_command 

1202 return response 

1203 

1204 if not isinstance(response, list): 

1205 response_list = [response] 

1206 else: 

1207 response_list = response 

1208 

1209 if not options["withdist"] and not options["withcoord"] and not options["withhash"]: 

1210 # just a bunch of places 

1211 return response_list 

1212 

1213 cast = { 

1214 "withdist": float, 

1215 "withcoord": lambda ll: (float(ll[0]), float(ll[1])), 

1216 "withhash": int, 

1217 } 

1218 

1219 # zip all output results with each casting function to get 

1220 # the properly native Python value. 

1221 f = [lambda x: x] 

1222 f += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]] 

1223 return [list(map(lambda fv: fv[0](fv[1]), zip(f, r))) for r in response_list] 

1224 

1225 

1226def parse_geosearch_generic_unified(response, **options): 

1227 """ 

1228 Parse GEOSEARCH/GEORADIUS responses using tuple coordinates. 

1229 """ 

1230 try: 

1231 if options["store"] or options["store_dist"]: 

1232 return response 

1233 except KeyError: 

1234 return response 

1235 

1236 response_list = response if isinstance(response, list) else [response] 

1237 

1238 if not options["withdist"] and not options["withcoord"] and not options["withhash"]: 

1239 return response_list 

1240 

1241 cast = { 

1242 "withdist": float, 

1243 "withcoord": lambda ll: (float(ll[0]), float(ll[1])), 

1244 "withhash": int, 

1245 } 

1246 funcs = [lambda x: x] 

1247 funcs += [cast[o] for o in ["withdist", "withhash", "withcoord"] if options[o]] 

1248 return [list(map(lambda fv: fv[0](fv[1]), zip(funcs, r))) for r in response_list] 

1249 

1250 

1251def parse_command(response, **options): 

1252 commands = {} 

1253 for command in response: 

1254 cmd_dict = {} 

1255 cmd_name = str_if_bytes(command[0]) 

1256 cmd_dict["name"] = cmd_name 

1257 cmd_dict["arity"] = int(command[1]) 

1258 cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]] 

1259 cmd_dict["first_key_pos"] = command[3] 

1260 cmd_dict["last_key_pos"] = command[4] 

1261 cmd_dict["step_count"] = command[5] 

1262 if len(command) > 6: 

1263 cmd_dict["acl_categories"] = [ 

1264 str_if_bytes(category) for category in command[6] 

1265 ] 

1266 if len(command) > 7: 

1267 cmd_dict["tips"] = command[7] 

1268 cmd_dict["key_specifications"] = command[8] 

1269 cmd_dict["subcommands"] = command[9] 

1270 commands[cmd_name] = cmd_dict 

1271 return commands 

1272 

1273 

1274def parse_command_unified(response, **options): 

1275 commands = {} 

1276 for command in response: 

1277 cmd_dict = {} 

1278 cmd_name = str_if_bytes(command[0]) 

1279 cmd_dict["name"] = cmd_name 

1280 cmd_dict["arity"] = int(command[1]) 

1281 cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]} 

1282 cmd_dict["first_key_pos"] = command[3] 

1283 cmd_dict["last_key_pos"] = command[4] 

1284 cmd_dict["step_count"] = command[5] 

1285 if len(command) > 6: 

1286 cmd_dict["acl_categories"] = {str_if_bytes(c) for c in command[6]} 

1287 if len(command) > 7: 

1288 cmd_dict["tips"] = command[7] 

1289 cmd_dict["key_specifications"] = command[8] 

1290 cmd_dict["subcommands"] = command[9] 

1291 commands[cmd_name] = cmd_dict 

1292 return commands 

1293 

1294 

1295def parse_command_resp3(response, **options): 

1296 commands = {} 

1297 for command in response: 

1298 cmd_dict = {} 

1299 cmd_name = str_if_bytes(command[0]) 

1300 cmd_dict["name"] = cmd_name 

1301 cmd_dict["arity"] = command[1] 

1302 cmd_dict["flags"] = {str_if_bytes(flag) for flag in command[2]} 

1303 cmd_dict["first_key_pos"] = command[3] 

1304 cmd_dict["last_key_pos"] = command[4] 

1305 cmd_dict["step_count"] = command[5] 

1306 cmd_dict["acl_categories"] = command[6] 

1307 if len(command) > 7: 

1308 cmd_dict["tips"] = command[7] 

1309 cmd_dict["key_specifications"] = command[8] 

1310 cmd_dict["subcommands"] = command[9] 

1311 

1312 commands[cmd_name] = cmd_dict 

1313 return commands 

1314 

1315 

1316def parse_pubsub_numsub(response, **options): 

1317 return list(zip(response[0::2], response[1::2])) 

1318 

1319 

1320def parse_client_kill(response, **options): 

1321 if isinstance(response, int): 

1322 return response 

1323 return str_if_bytes(response) == "OK" 

1324 

1325 

1326def parse_acl_getuser(response, **options): 

1327 if response is None: 

1328 return None 

1329 if isinstance(response, list): 

1330 data = pairs_to_dict(response, decode_keys=True) 

1331 else: 

1332 data = {str_if_bytes(key): value for key, value in response.items()} 

1333 

1334 # convert everything but user-defined data in 'keys' to native strings 

1335 data["flags"] = list(map(str_if_bytes, data["flags"])) 

1336 data["passwords"] = list(map(str_if_bytes, data["passwords"])) 

1337 data["commands"] = str_if_bytes(data["commands"]) 

1338 if isinstance(data["keys"], str) or isinstance(data["keys"], bytes): 

1339 data["keys"] = list(str_if_bytes(data["keys"]).split(" ")) 

1340 if data["keys"] == [""]: 

1341 data["keys"] = [] 

1342 if "channels" in data: 

1343 if isinstance(data["channels"], str) or isinstance(data["channels"], bytes): 

1344 data["channels"] = list(str_if_bytes(data["channels"]).split(" ")) 

1345 if data["channels"] == [""]: 

1346 data["channels"] = [] 

1347 if "selectors" in data: 

1348 if data["selectors"] != [] and isinstance(data["selectors"][0], list): 

1349 data["selectors"] = [ 

1350 list(map(str_if_bytes, selector)) for selector in data["selectors"] 

1351 ] 

1352 elif data["selectors"] != []: 

1353 data["selectors"] = [ 

1354 {str_if_bytes(k): str_if_bytes(v) for k, v in selector.items()} 

1355 for selector in data["selectors"] 

1356 ] 

1357 

1358 # split 'commands' into separate 'categories' and 'commands' lists 

1359 commands, categories = [], [] 

1360 for command in data["commands"].split(" "): 

1361 categories.append(command) if "@" in command else commands.append(command) 

1362 

1363 data["commands"] = commands 

1364 data["categories"] = categories 

1365 data["enabled"] = "on" in data["flags"] 

1366 return data 

1367 

1368 

1369def parse_acl_log(response, **options): 

1370 if response is None: 

1371 return None 

1372 if isinstance(response, list): 

1373 data = [] 

1374 for log in response: 

1375 log_data = pairs_to_dict(log, True, True) 

1376 client_info = log_data.get("client-info", "") 

1377 log_data["client-info"] = parse_client_info(client_info) 

1378 

1379 # float() is lossy comparing to the "double" in C 

1380 log_data["age-seconds"] = float(log_data["age-seconds"]) 

1381 data.append(log_data) 

1382 else: 

1383 data = bool_ok(response) 

1384 return data 

1385 

1386 

1387def parse_acl_log_resp3_to_resp2_legacy(response, **options): 

1388 """RESP3-wire ACL LOG → today's RESP2 parsed shape. 

1389 

1390 Each log entry arrives as a ``dict`` on RESP3 wire instead of a flat 

1391 list of pairs; convert ``client-info`` from a string blob into the 

1392 parsed ``dict`` and ``age-seconds`` to ``float`` so the Python shape 

1393 matches what :func:`parse_acl_log` produces from RESP2 wire. 

1394 

1395 Also used as the unified callback (Set D): the legacy and unified 

1396 shapes coincide for ACL LOG. 

1397 """ 

1398 if response is None: 

1399 return None 

1400 if not isinstance(response, list): 

1401 return bool_ok(response) 

1402 data = [] 

1403 for log in response: 

1404 if isinstance(log, dict): 

1405 log_data = {str_if_bytes(k): str_if_bytes(v) for k, v in log.items()} 

1406 else: 

1407 log_data = pairs_to_dict(log, True, True) 

1408 client_info = log_data.get("client-info", "") 

1409 log_data["client-info"] = parse_client_info(client_info) 

1410 log_data["age-seconds"] = float(log_data["age-seconds"]) 

1411 data.append(log_data) 

1412 return data 

1413 

1414 

1415def parse_acl_log_resp3_unified(response, **options): 

1416 """Parse RESP3 ACL LOG into the approved unified shape.""" 

1417 if response is None: 

1418 return None 

1419 if not isinstance(response, list): 

1420 return bool_ok(response) 

1421 data = [] 

1422 for entry in response: 

1423 if isinstance(entry, dict): 

1424 log_data = {str_if_bytes(k): v for k, v in entry.items()} 

1425 else: 

1426 log_data = pairs_to_dict(entry, True, True) 

1427 if "age-seconds" in log_data: 

1428 log_data["age-seconds"] = float(log_data["age-seconds"]) 

1429 if "client-info" in log_data: 

1430 log_data["client-info"] = parse_client_info(log_data["client-info"]) 

1431 for key, value in list(log_data.items()): 

1432 if key not in ("age-seconds", "client-info"): 

1433 log_data[key] = str_if_bytes(value) 

1434 data.append(log_data) 

1435 return data 

1436 

1437 

1438def parse_acl_getuser_unified(response, **options): 

1439 """ACL GETUSER → unified shape with selectors as ``list[dict]``. 

1440 

1441 On RESP2 wire each selector arrives as a flat ``[k, v, k, v, …]`` 

1442 list; pair them into dicts to match the RESP3 wire shape. 

1443 """ 

1444 data = parse_acl_getuser(response, **options) 

1445 if data is None: 

1446 return data 

1447 selectors = data.get("selectors") 

1448 if selectors and isinstance(selectors[0], list): 

1449 data["selectors"] = [ 

1450 dict(zip(selector[0::2], selector[1::2])) for selector in selectors 

1451 ] 

1452 return data 

1453 

1454 

1455def parse_acl_getuser_resp3_to_resp2_legacy(response, **options): 

1456 """RESP3-wire ACL GETUSER → today's RESP2 selectors as flat lists. 

1457 

1458 Each selector arrives as a ``dict`` on RESP3 wire; flatten back to 

1459 the interleaved ``[k, v, k, v, …]`` form produced by RESP2 wire. 

1460 """ 

1461 data = parse_acl_getuser(response, **options) 

1462 if data is None: 

1463 return data 

1464 selectors = data.get("selectors") 

1465 if selectors and isinstance(selectors[0], dict): 

1466 data["selectors"] = [ 

1467 [item for kv in selector.items() for item in kv] for selector in selectors 

1468 ] 

1469 return data 

1470 

1471 

1472def parse_client_info(value): 

1473 """ 

1474 Parsing client-info in ACL Log in following format. 

1475 "key1=value1 key2=value2 key3=value3" 

1476 """ 

1477 client_info = _parse_client_info_fields(str_if_bytes(value).strip()) 

1478 

1479 # Those fields are defined as int in networking.c 

1480 for int_key in { 

1481 "id", 

1482 "age", 

1483 "idle", 

1484 "db", 

1485 "sub", 

1486 "psub", 

1487 "multi", 

1488 "qbuf", 

1489 "qbuf-free", 

1490 "obl", 

1491 "argv-mem", 

1492 "oll", 

1493 "omem", 

1494 "tot-mem", 

1495 }: 

1496 if int_key in client_info: 

1497 client_info[int_key] = int(client_info[int_key]) 

1498 return client_info 

1499 

1500 

1501def parse_set_result(response, **options): 

1502 """ 

1503 Handle SET result since GET argument is available since Redis 6.2. 

1504 Parsing SET result into: 

1505 - BOOL 

1506 - String when GET argument is used 

1507 """ 

1508 if options.get("get"): 

1509 # Redis will return a getCommand result. 

1510 # See `setGenericCommand` in t_string.c 

1511 return response 

1512 return response and str_if_bytes(response) == "OK" 

1513 

1514 

1515def parse_function_list_unified(response, **options): 

1516 """FUNCTION LIST → unified ``list[dict]`` with bytes keys. 

1517 

1518 Accepts either RESP2 wire (``list[list]`` of flat ``[k, v, k, v, …]`` 

1519 pairs, with the nested ``b"functions"`` value also a flat list of 

1520 flat lists) or RESP3 wire (``list[dict]`` already in nested-map 

1521 form). Both are normalised to ``list[dict]``. 

1522 """ 

1523 if response is None: 

1524 return None 

1525 result = [] 

1526 for lib in response: 

1527 if isinstance(lib, dict): 

1528 result.append(lib) 

1529 continue 

1530 lib_dict = pairs_to_dict(lib) 

1531 func_key = b"functions" if b"functions" in lib_dict else "functions" 

1532 if func_key in lib_dict: 

1533 functions = lib_dict[func_key] 

1534 lib_dict[func_key] = [ 

1535 func if isinstance(func, dict) else pairs_to_dict(func) 

1536 for func in functions 

1537 ] 

1538 result.append(lib_dict) 

1539 return result 

1540 

1541 

1542def parse_function_list_resp3_to_resp2_legacy(response, **options): 

1543 """RESP3-wire FUNCTION LIST → today's RESP2 ``list[list]`` shape. 

1544 

1545 Each library and each nested function arrives as a ``dict``; flatten 

1546 them back to interleaved ``[k, v, k, v, …]`` lists so the Python 

1547 shape matches what RESP2 wire produces natively. 

1548 """ 

1549 if response is None: 

1550 return None 

1551 result = [] 

1552 for lib in response: 

1553 if not isinstance(lib, dict): 

1554 result.append(lib) 

1555 continue 

1556 flat = [] 

1557 for key, value in lib.items(): 

1558 flat.append(key) 

1559 if key == b"functions" or key == "functions": 

1560 flat.append( 

1561 [ 

1562 [item for kv in func.items() for item in kv] 

1563 if isinstance(func, dict) 

1564 else func 

1565 for func in value 

1566 ] 

1567 ) 

1568 else: 

1569 flat.append(value) 

1570 result.append(flat) 

1571 return result 

1572 

1573 

1574def parse_cluster_links_unified(response, **options): 

1575 """CLUSTER LINKS → unified ``list[dict]`` with string keys. 

1576 

1577 Accepts either RESP2 wire (``list[list]`` of flat pairs) or RESP3 

1578 wire (``list[dict]``). Both are normalised to ``list[dict]``. 

1579 """ 

1580 if response is None: 

1581 return None 

1582 return [ 

1583 {str_if_bytes(k): v for k, v in item.items()} 

1584 if isinstance(item, dict) 

1585 else pairs_to_dict(item, decode_keys=True) 

1586 for item in response 

1587 ] 

1588 

1589 

1590def parse_cluster_links_resp3_to_resp2_legacy(response, **options): 

1591 """RESP3-wire CLUSTER LINKS → today's RESP2 ``list[list]`` shape. 

1592 

1593 Each link arrives as a ``dict`` with bytes keys; flatten back to 

1594 interleaved ``[k, v, k, v, …]`` lists so the Python shape matches 

1595 what RESP2 wire produces natively. 

1596 """ 

1597 if response is None: 

1598 return None 

1599 return [ 

1600 [item for kv in link.items() for item in kv] if isinstance(link, dict) else link 

1601 for link in response 

1602 ] 

1603 

1604 

1605def string_keys_to_dict(key_string, callback): 

1606 return dict.fromkeys(key_string.split(), callback) 

1607 

1608 

1609# The command-to-callback mapping dictionaries (``_RedisCallbacks``, 

1610# ``_RedisCallbacksRESP2``, ``_RedisCallbacksRESP3``, …) and the 

1611# ``get_response_callbacks`` selector live in 

1612# ``redis/_parsers/response_callbacks.py``. They are re-exported below for 

1613# backward compatibility so existing imports of the form 

1614# ``from redis._parsers.helpers import _RedisCallbacks`` keep working. The 

1615# import is placed at module bottom to avoid a circular import (the 

1616# response_callbacks module imports parser helpers defined above). 

1617# isort: off 

1618from .response_callbacks import ( # noqa: E402, F401 

1619 _RedisCallbacks, 

1620 _RedisCallbacksRESP2, 

1621 _RedisCallbacksRESP2Unified, 

1622 _RedisCallbacksRESP3, 

1623 _RedisCallbacksRESP3Unified, 

1624 _RedisCallbacksRESP3toRESP2Legacy, 

1625 get_response_callbacks, 

1626) 

1627# isort: on