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

529 statements  

1import datetime 

2 

3from redis.commands.core import GCRAResponse 

4from redis.utils import str_if_bytes 

5 

6 

7def timestamp_to_datetime(response): 

8 "Converts a unix timestamp to a Python datetime object" 

9 if not response: 

10 return None 

11 try: 

12 response = int(response) 

13 except ValueError: 

14 return None 

15 return datetime.datetime.fromtimestamp(response) 

16 

17 

18def parse_debug_object(response): 

19 "Parse the results of Redis's DEBUG OBJECT command into a Python dict" 

20 # The 'type' of the object is the first item in the response, but isn't 

21 # prefixed with a name 

22 response = str_if_bytes(response) 

23 response = "type:" + response 

24 response = dict(kv.split(":") for kv in response.split()) 

25 

26 # parse some expected int values from the string response 

27 # note: this cmd isn't spec'd so these may not appear in all redis versions 

28 int_fields = ("refcount", "serializedlength", "lru", "lru_seconds_idle") 

29 for field in int_fields: 

30 if field in response: 

31 response[field] = int(response[field]) 

32 

33 return response 

34 

35 

36def parse_info(response): 

37 """Parse the result of Redis's INFO command into a Python dict""" 

38 info = {} 

39 response = str_if_bytes(response) 

40 

41 def get_value(value): 

42 if "," not in value and "=" not in value: 

43 try: 

44 if "." in value: 

45 return float(value) 

46 else: 

47 return int(value) 

48 except ValueError: 

49 return value 

50 elif "=" not in value: 

51 return [get_value(v) for v in value.split(",") if v] 

52 else: 

53 sub_dict = {} 

54 for item in value.split(","): 

55 if not item: 

56 continue 

57 if "=" in item: 

58 k, v = item.rsplit("=", 1) 

59 sub_dict[k] = get_value(v) 

60 else: 

61 sub_dict[item] = True 

62 return sub_dict 

63 

64 for line in response.splitlines(): 

65 if line and not line.startswith("#"): 

66 if line.find(":") != -1: 

67 # Split, the info fields keys and values. 

68 # Note that the value may contain ':'. but the 'host:' 

69 # pseudo-command is the only case where the key contains ':' 

70 key, value = line.split(":", 1) 

71 if key == "cmdstat_host": 

72 key, value = line.rsplit(":", 1) 

73 

74 if key == "module": 

75 # Hardcode a list for key 'modules' since there could be 

76 # multiple lines that started with 'module' 

77 info.setdefault("modules", []).append(get_value(value)) 

78 else: 

79 info[key] = get_value(value) 

80 else: 

81 # if the line isn't splittable, append it to the "__raw__" key 

82 info.setdefault("__raw__", []).append(line) 

83 

84 return info 

85 

86 

87def parse_memory_stats(response, **kwargs): 

88 """Parse the results of MEMORY STATS""" 

89 stats = pairs_to_dict(response, decode_keys=True) 

90 for key, value in stats.items(): 

91 if key.startswith("db.") and isinstance(value, list): 

92 stats[key] = pairs_to_dict(value, decode_keys=True) 

93 return stats 

94 

95 

96def parse_memory_stats_resp3(response, **kwargs): 

97 """Parse MEMORY STATS for RESP3 — decode keys to str, preserve native values.""" 

98 stats = {str_if_bytes(key): value for key, value in response.items()} 

99 for key, value in stats.items(): 

100 if key.startswith("db.") and isinstance(value, dict): 

101 stats[key] = {str_if_bytes(k): v for k, v in value.items()} 

102 return stats 

103 

104 

105SENTINEL_STATE_TYPES = { 

106 "can-failover-its-master": int, 

107 "config-epoch": int, 

108 "down-after-milliseconds": int, 

109 "failover-timeout": int, 

110 "info-refresh": int, 

111 "last-hello-message": int, 

112 "last-ok-ping-reply": int, 

113 "last-ping-reply": int, 

114 "last-ping-sent": int, 

115 "master-link-down-time": int, 

116 "master-port": int, 

117 "num-other-sentinels": int, 

118 "num-slaves": int, 

119 "o-down-time": int, 

120 "pending-commands": int, 

121 "parallel-syncs": int, 

122 "port": int, 

123 "quorum": int, 

124 "role-reported-time": int, 

125 "s-down-time": int, 

126 "slave-priority": int, 

127 "slave-repl-offset": int, 

128 "voted-leader-epoch": int, 

129} 

130 

131 

132def parse_sentinel_state(item): 

133 result = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES) 

134 flags = set(result["flags"].split(",")) 

135 result["flags"] = flags 

136 for name, flag in ( 

137 ("is_master", "master"), 

138 ("is_slave", "slave"), 

139 ("is_sdown", "s_down"), 

140 ("is_odown", "o_down"), 

141 ("is_sentinel", "sentinel"), 

142 ("is_disconnected", "disconnected"), 

143 ("is_master_down", "master_down"), 

144 ): 

145 result[name] = flag in flags 

146 return result 

147 

148 

149def parse_sentinel_master(response, **options): 

150 return parse_sentinel_state(map(str_if_bytes, response)) 

151 

152 

153def parse_sentinel_state_resp3(response, **options): 

154 result = {} 

155 for key in response: 

156 try: 

157 value = SENTINEL_STATE_TYPES[str_if_bytes(key)](str_if_bytes(response[key])) 

158 result[str_if_bytes(key)] = value 

159 except Exception: 

160 result[str_if_bytes(key)] = str_if_bytes(response[key]) 

161 flags = set(result["flags"].split(",")) 

162 result["flags"] = flags 

163 for name, flag in ( 

164 ("is_master", "master"), 

165 ("is_slave", "slave"), 

166 ("is_sdown", "s_down"), 

167 ("is_odown", "o_down"), 

168 ("is_sentinel", "sentinel"), 

169 ("is_disconnected", "disconnected"), 

170 ("is_master_down", "master_down"), 

171 ): 

172 result[name] = flag in flags 

173 return result 

174 

175 

176def parse_sentinel_masters(response, **options): 

177 result = {} 

178 for item in response: 

179 state = parse_sentinel_state(map(str_if_bytes, item)) 

180 result[state["name"]] = state 

181 return result 

182 

183 

184def parse_sentinel_masters_resp3(response, **options): 

185 result = {} 

186 for master in response: 

187 state = parse_sentinel_state_resp3(master) 

188 result[state["name"]] = state 

189 return result 

190 

191 

192def parse_sentinel_slaves_and_sentinels(response, **options): 

193 return [parse_sentinel_state(map(str_if_bytes, item)) for item in response] 

194 

195 

196def parse_sentinel_slaves_and_sentinels_resp3(response, **options): 

197 return [parse_sentinel_state_resp3(item, **options) for item in response] 

198 

199 

200def parse_sentinel_get_master(response, **options): 

201 return response and (response[0], int(response[1])) or None 

202 

203 

204def pairs_to_dict(response, decode_keys=False, decode_string_values=False): 

205 """Create a dict given a list of key/value pairs""" 

206 if response is None: 

207 return {} 

208 if decode_keys or decode_string_values: 

209 # the iter form is faster, but I don't know how to make that work 

210 # with a str_if_bytes() map 

211 keys = response[::2] 

212 if decode_keys: 

213 keys = map(str_if_bytes, keys) 

214 values = response[1::2] 

215 if decode_string_values: 

216 values = map(str_if_bytes, values) 

217 return dict(zip(keys, values)) 

218 else: 

219 it = iter(response) 

220 return dict(zip(it, it)) 

221 

222 

223def pairs_to_dict_typed(response, type_info): 

224 it = iter(response) 

225 result = {} 

226 for key, value in zip(it, it): 

227 if key in type_info: 

228 try: 

229 value = type_info[key](value) 

230 except Exception: 

231 # if for some reason the value can't be coerced, just use 

232 # the string value 

233 pass 

234 result[key] = value 

235 return result 

236 

237 

238def _wrap_score_cast_func(score_cast_func): 

239 """Wrap score_cast_func to handle scientific notation in RESP2 byte strings. 

240 

241 Redis returns scores as byte strings in RESP2, and large numbers may use 

242 scientific notation (e.g., b'1.7732526297292595e+18'). Python's int() cannot 

243 parse scientific notation directly. Rather than unconditionally routing 

244 through float() (which would change the input type for every custom 

245 callable), we try the original function first and only fall back to 

246 converting through float() on ValueError. 

247 """ 

248 if score_cast_func is float: 

249 return score_cast_func 

250 

251 def _safe_cast(x): 

252 try: 

253 return score_cast_func(x) 

254 except (ValueError, TypeError): 

255 return score_cast_func(float(x)) 

256 

257 return _safe_cast 

258 

259 

260def zset_score_pairs(response, **options): 

261 """ 

262 If ``withscores`` is specified in the options, return the response as 

263 a list of [value, score] pairs 

264 """ 

265 if not response or not options.get("withscores"): 

266 return response 

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

268 it = iter(response) 

269 # Normalise RESP2 byte-string scores to float before applying the cast 

270 # so that score_cast_func receives the same type as in RESP3. 

271 return [[val, score_cast_func(float(score))] for val, score in zip(it, it)] 

272 

273 

274def zset_score_for_rank(response, **options): 

275 """ 

276 If ``withscores`` is specified in the options, return the response as 

277 a [value, score] pair 

278 """ 

279 if not response or not options.get("withscore"): 

280 return response 

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

282 # Normalise RESP2 byte-string scores to float before applying the cast 

283 # so that score_cast_func receives the same type as in RESP3. 

284 return [response[0], score_cast_func(float(response[1]))] 

285 

286 

287def zset_score_pairs_resp3(response, **options): 

288 """ 

289 If ``withscores`` is specified in the options, return the response as 

290 a list of [value, score] pairs 

291 """ 

292 if not response or not options.get("withscores"): 

293 return response 

294 score_cast_func = options.get("score_cast_func", float) 

295 return [[name, score_cast_func(val)] for name, val in response] 

296 

297 

298def hrandfield_pairs(response, **options): 

299 """ 

300 If ``withvalues`` is specified in the options, return the response as 

301 a list of [field, value] pairs (pairing flat interleaved list). 

302 """ 

303 if not response or not options.get("withvalues"): 

304 return response 

305 it = iter(response) 

306 return [[field, val] for field, val in zip(it, it)] 

307 

308 

309def parse_zmpop(response, **options): 

310 """ 

311 Parse ZMPOP/BZMPOP response, casting scores to float. 

312 Response format: [key, [[member, score], ...]] or None. 

313 """ 

314 if response is None: 

315 return None 

316 key, members = response 

317 return [ 

318 key, 

319 [ 

320 [member, score if isinstance(score, float) else float(score)] 

321 for member, score in members 

322 ], 

323 ] 

324 

325 

326def parse_lcs(response, **options): 

327 """ 

328 Parse LCS response. Without modifiers returns the raw string. 

329 With LEN returns an integer. With IDX returns a dict with string keys. 

330 RESP2 with IDX returns a flat list [key, val, key, val, ...] 

331 which we convert to a dict. RESP3 returns a native dict. 

332 Both have keys normalized to strings. 

333 """ 

334 if isinstance(response, list): 

335 it = iter(response) 

336 return {str_if_bytes(k): v for k, v in zip(it, it)} 

337 if isinstance(response, dict): 

338 return {str_if_bytes(k): v for k, v in response.items()} 

339 return response 

340 

341 

342def zpop_score_pairs(response, **options): 

343 """ 

344 Handle ZPOPMAX/ZPOPMIN RESP2 responses. 

345 RESP2 always returns a flat array: [member1, score1, member2, score2, ...] 

346 Scores are byte strings that need to be cast to float. 

347 Always pairs and casts scores — no ``withscores`` gate required because 

348 ZPOPMAX/ZPOPMIN always include scores in their response. 

349 """ 

350 if not response: 

351 return response 

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

353 it = iter(response) 

354 return [[val, score_cast_func(float(score))] for val, score in zip(it, it)] 

355 

356 

357def zpop_score_pairs_resp3(response, **options): 

358 """ 

359 Handle ZPOPMAX/ZPOPMIN RESP3 responses which differ based on count: 

360 - Without count: flat [member, score] 

361 - With count: nested [[member, score], ...] 

362 Normalizes both to list of [member, score] pairs with score_cast_func applied. 

363 """ 

364 if not response: 

365 return response 

366 score_cast_func = options.get("score_cast_func", float) 

367 # Detect flat vs nested: if first element is a list, it's nested (with count) 

368 if isinstance(response[0], list): 

369 return [[name, score_cast_func(val)] for name, val in response] 

370 else: 

371 return [[response[0], score_cast_func(response[1])]] 

372 

373 

374def zset_score_for_rank_resp3(response, **options): 

375 """ 

376 If ``withscores`` is specified in the options, return the response as 

377 a [value, score] pair 

378 """ 

379 if not response or not options.get("withscore"): 

380 return response 

381 score_cast_func = options.get("score_cast_func", float) 

382 return [response[0], score_cast_func(response[1])] 

383 

384 

385def sort_return_tuples(response, **options): 

386 """ 

387 If ``groups`` is specified, return the response as a list of 

388 n-element tuples with n being the value found in options['groups'] 

389 """ 

390 if not response or not options.get("groups"): 

391 return response 

392 n = options["groups"] 

393 return list(zip(*[response[i::n] for i in range(n)])) 

394 

395 

396def parse_stream_list(response, **options): 

397 if response is None: 

398 return None 

399 data = [] 

400 for r in response: 

401 if r is not None: 

402 if "claim_min_idle_time" in options: 

403 data.append((r[0], pairs_to_dict(r[1]), *r[2:])) 

404 else: 

405 data.append((r[0], pairs_to_dict(r[1]))) 

406 else: 

407 data.append((None, None)) 

408 return data 

409 

410 

411def pairs_to_dict_with_str_keys(response): 

412 return pairs_to_dict(response, decode_keys=True) 

413 

414 

415def parse_list_of_dicts(response): 

416 return list(map(pairs_to_dict_with_str_keys, response)) 

417 

418 

419def parse_xclaim(response, **options): 

420 if options.get("parse_justid", False): 

421 return response 

422 return parse_stream_list(response) 

423 

424 

425def parse_xautoclaim(response, **options): 

426 if options.get("parse_justid", False): 

427 return response[1] 

428 response[1] = parse_stream_list(response[1]) 

429 return response 

430 

431 

432def parse_xinfo_stream(response, **options): 

433 if isinstance(response, list): 

434 data = pairs_to_dict(response, decode_keys=True) 

435 else: 

436 data = {str_if_bytes(k): v for k, v in response.items()} 

437 if not options.get("full", False): 

438 first = data.get("first-entry") 

439 if first is not None and first[0] is not None: 

440 data["first-entry"] = (first[0], pairs_to_dict(first[1])) 

441 last = data["last-entry"] 

442 if last is not None and last[0] is not None: 

443 data["last-entry"] = (last[0], pairs_to_dict(last[1])) 

444 else: 

445 data["entries"] = {_id: pairs_to_dict(entry) for _id, entry in data["entries"]} 

446 if len(data["groups"]) > 0 and isinstance(data["groups"][0], list): 

447 data["groups"] = [ 

448 pairs_to_dict(group, decode_keys=True) for group in data["groups"] 

449 ] 

450 for g in data["groups"]: 

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

452 g["consumers"] = [ 

453 pairs_to_dict(c, decode_keys=True) for c in g["consumers"] 

454 ] 

455 else: 

456 data["groups"] = [ 

457 {str_if_bytes(k): v for k, v in group.items()} 

458 for group in data["groups"] 

459 ] 

460 return data 

461 

462 

463def parse_xread(response, **options): 

464 if response is None: 

465 return {} 

466 return {r[0]: parse_stream_list(r[1], **options) for r in response} 

467 

468 

469def parse_xread_resp3(response, **options): 

470 if response is None: 

471 return {} 

472 return {key: parse_stream_list(value, **options) for key, value in response.items()} 

473 

474 

475def parse_xpending(response, **options): 

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

477 return parse_xpending_range(response) 

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

479 return { 

480 "pending": response[0], 

481 "min": response[1], 

482 "max": response[2], 

483 "consumers": consumers, 

484 } 

485 

486 

487def parse_xpending_range(response): 

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

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

490 

491 

492def float_or_none(response): 

493 if response is None: 

494 return None 

495 return float(response) 

496 

497 

498def bool_ok(response, **options): 

499 return str_if_bytes(response) == "OK" 

500 

501 

502def parse_zadd(response, **options): 

503 if response is None: 

504 return None 

505 if options.get("as_score"): 

506 return float(response) 

507 return int(response) 

508 

509 

510def parse_client_list(response, **options): 

511 clients = [] 

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

513 client_dict = {} 

514 tokens = c.split(" ") 

515 last_key = None 

516 for token in tokens: 

517 if "=" in token: 

518 # Values might contain '=' 

519 key, value = token.split("=", 1) 

520 client_dict[key] = value 

521 last_key = key 

522 else: 

523 # Values may include spaces. For instance, when running Redis via a Unix socket — such as 

524 # "/tmp/redis sock/redis.sock" — the addr or laddr field will include a space. 

525 client_dict[last_key] += " " + token 

526 

527 if client_dict: 

528 clients.append(client_dict) 

529 return clients 

530 

531 

532def parse_config_get(response, **options): 

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

534 return response and pairs_to_dict(response) or {} 

535 

536 

537def parse_scan(response, **options): 

538 cursor, r = response 

539 return int(cursor), r 

540 

541 

542def parse_hscan(response, **options): 

543 cursor, r = response 

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

545 if no_values: 

546 payload = r or [] 

547 else: 

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

549 return int(cursor), payload 

550 

551 

552def parse_zscan(response, **options): 

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

554 cursor, r = response 

555 it = iter(r) 

556 # Normalise scores to float before applying the cast so that 

557 # score_cast_func receives the same type regardless of protocol. 

558 return int(cursor), [ 

559 [val, score_cast_func(float(score))] for val, score in zip(it, it) 

560 ] 

561 

562 

563def parse_zmscore(response, **options): 

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

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

566 

567 

568def parse_slowlog_get(response, **options): 

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

570 

571 def parse_item(item): 

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

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

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

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

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

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

578 

579 # These fields are optional, depends on environment. 

580 if len(item) >= 6: 

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

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

583 else: 

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

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

586 

587 # These fields are optional, depends on environment. 

588 if len(item) >= 7: 

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

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

591 

592 return result 

593 

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

595 

596 

597def parse_client_trackinginfo(response, **kwargs): 

598 """ 

599 Parse CLIENT TRACKINGINFO response into a dict with str keys. 

600 RESP2: flat list [key, val, key, val, ...] → dict 

601 RESP3: native dict with bytes keys → dict with str keys 

602 """ 

603 if isinstance(response, list): 

604 data = pairs_to_dict(response, decode_keys=True) 

605 else: 

606 data = {str_if_bytes(k): v for k, v in response.items()} 

607 if "flags" in data: 

608 data["flags"] = [str_if_bytes(f) for f in data["flags"]] 

609 if "prefixes" in data: 

610 data["prefixes"] = [str_if_bytes(p) for p in data["prefixes"]] 

611 return data 

612 

613 

614def parse_stralgo(response, **options): 

615 """ 

616 Parse the response from `STRALGO` command. 

617 Without modifiers the returned value is string. 

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

619 (i.e integer). 

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

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

622 offset for each string, where there are matches. 

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

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

625 """ 

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

627 return int(response) 

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

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

630 matches = [ 

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

632 for match in response[1] 

633 ] 

634 else: 

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

636 return { 

637 str_if_bytes(response[0]): matches, 

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

639 } 

640 return str_if_bytes(response) 

641 

642 

643def parse_stralgo_resp3(response, **options): 

644 """Parse RESP3 ``STRALGO`` response to match RESP2 ``parse_stralgo`` output. 

645 

646 RESP3 returns a dict ``{b"matches": [...], b"len": N}`` for ``idx=True``, 

647 an int for ``len=True``, or a plain string otherwise. The match 

648 restructuring mirrors :func:`parse_stralgo` exactly so both protocols 

649 produce identical results. 

650 """ 

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

652 return int(response) 

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

654 if isinstance(response, dict): 

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

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

657 else: 

658 return str_if_bytes(response) 

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

660 matches = [ 

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

662 for match in raw_matches 

663 ] 

664 else: 

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

666 return { 

667 "matches": matches, 

668 "len": int(raw_len), 

669 } 

670 return str_if_bytes(response) 

671 

672 

673def parse_cluster_links(response, **options): 

674 """Parse CLUSTER LINKS into a list of dicts with str keys. 

675 

676 RESP2 returns a list of flat lists ``[key, val, key, val, ...]``. 

677 RESP3 returns a list of dicts with bytes keys. 

678 Both are normalised to ``[{"direction": ..., "node": ..., ...}, ...]``. 

679 """ 

680 result = [] 

681 for item in response: 

682 if isinstance(item, dict): 

683 result.append({str_if_bytes(k): v for k, v in item.items()}) 

684 else: 

685 result.append(pairs_to_dict(item, decode_keys=True)) 

686 return result 

687 

688 

689def parse_cluster_info(response, **options): 

690 response = str_if_bytes(response) 

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

692 

693 

694def _parse_node_line(line): 

695 line_items = line.split(" ") 

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

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

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

699 node_dict = { 

700 "node_id": node_id, 

701 "hostname": hostname, 

702 "flags": flags, 

703 "master_id": master_id, 

704 "last_ping_sent": ping, 

705 "last_pong_rcvd": pong, 

706 "epoch": epoch, 

707 "slots": [], 

708 "migrations": [], 

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

710 } 

711 if len(line_items) >= 9: 

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

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

714 return ip, node_dict 

715 

716 

717def _parse_slots(slot_ranges): 

718 slots, migrations = [], [] 

719 for s_range in slot_ranges: 

720 if "->-" in s_range: 

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

722 migrations.append( 

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

724 ) 

725 elif "-<-" in s_range: 

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

727 migrations.append( 

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

729 ) 

730 else: 

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

732 slots.append(s_range) 

733 

734 return slots, migrations 

735 

736 

737def parse_cluster_nodes(response, **options): 

738 """ 

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

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

741 """ 

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

743 response = response.splitlines() 

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

745 

746 

747def parse_geosearch_generic(response, **options): 

748 """ 

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

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

751 """ 

752 try: 

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

754 # `store` and `store_dist` cant be combined 

755 # with other command arguments. 

756 # relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER' 

757 return response 

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

759 return response 

760 

761 if not isinstance(response, list): 

762 response_list = [response] 

763 else: 

764 response_list = response 

765 

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

767 # just a bunch of places 

768 return response_list 

769 

770 cast = { 

771 "withdist": float, 

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

773 "withhash": int, 

774 } 

775 

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

777 # the properly native Python value. 

778 f = [lambda x: x] 

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

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

781 

782 

783def parse_command(response, **options): 

784 commands = {} 

785 for command in response: 

786 cmd_dict = {} 

787 cmd_name = str_if_bytes(command[0]) 

788 cmd_dict["name"] = cmd_name 

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

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

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

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

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

794 if len(command) > 6: 

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

796 if len(command) > 7: 

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

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

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

800 commands[cmd_name] = cmd_dict 

801 return commands 

802 

803 

804def parse_command_resp3(response, **options): 

805 commands = {} 

806 for command in response: 

807 cmd_dict = {} 

808 cmd_name = str_if_bytes(command[0]) 

809 cmd_dict["name"] = cmd_name 

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

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

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

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

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

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

816 if len(command) > 7: 

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

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

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

820 

821 commands[cmd_name] = cmd_dict 

822 return commands 

823 

824 

825def parse_pubsub_numsub(response, **options): 

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

827 

828 

829def parse_client_kill(response, **options): 

830 if isinstance(response, int): 

831 return response 

832 return str_if_bytes(response) == "OK" 

833 

834 

835def parse_acl_getuser(response, **options): 

836 if response is None: 

837 return None 

838 if isinstance(response, list): 

839 data = pairs_to_dict(response, decode_keys=True) 

840 else: 

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

842 

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

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

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

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

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

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

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

850 data["keys"] = [] 

851 if "channels" in data: 

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

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

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

855 data["channels"] = [] 

856 if "selectors" in data: 

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

858 # RESP2: flat list [key, val, key, val] → convert to dict 

859 data["selectors"] = [ 

860 pairs_to_dict(selector, decode_keys=True, decode_string_values=True) 

861 for selector in data["selectors"] 

862 ] 

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

864 data["selectors"] = [ 

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

866 for selector in data["selectors"] 

867 ] 

868 

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

870 commands, categories = [], [] 

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

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

873 

874 data["commands"] = commands 

875 data["categories"] = categories 

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

877 return data 

878 

879 

880def parse_acl_log(response, **options): 

881 if response is None: 

882 return None 

883 if isinstance(response, list): 

884 data = [] 

885 for log in response: 

886 log_data = pairs_to_dict(log, True, True) 

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

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

889 

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

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

892 data.append(log_data) 

893 else: 

894 data = bool_ok(response) 

895 return data 

896 

897 

898def parse_acl_log_resp3(response, **options): 

899 """Parse ACL LOG for RESP3 — normalize to match RESP2 semantic richness. 

900 Converts age-seconds to float and client-info to parsed dict.""" 

901 if not isinstance(response, list): 

902 return bool_ok(response) 

903 data = [] 

904 for entry in response: 

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

906 # Ensure age-seconds is float 

907 if "age-seconds" in log_data: 

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

909 # Parse client-info string into dict 

910 if "client-info" in log_data: 

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

912 # Decode remaining string values 

913 for key in log_data: 

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

915 log_data[key] = str_if_bytes(log_data[key]) 

916 data.append(log_data) 

917 return data 

918 

919 

920def parse_client_info(value): 

921 """ 

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

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

924 """ 

925 client_info = {} 

926 for info in str_if_bytes(value).strip().split(): 

927 key, value = info.split("=") 

928 client_info[key] = value 

929 

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

931 for int_key in { 

932 "id", 

933 "age", 

934 "idle", 

935 "db", 

936 "sub", 

937 "psub", 

938 "multi", 

939 "qbuf", 

940 "qbuf-free", 

941 "obl", 

942 "argv-mem", 

943 "oll", 

944 "omem", 

945 "tot-mem", 

946 }: 

947 if int_key in client_info: 

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

949 return client_info 

950 

951 

952def parse_set_result(response, **options): 

953 """ 

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

955 Parsing SET result into: 

956 - BOOL 

957 - String when GET argument is used 

958 """ 

959 if options.get("get"): 

960 # Redis will return a getCommand result. 

961 # See `setGenericCommand` in t_string.c 

962 return response 

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

964 

965 

966def parse_gcra(response, **options): 

967 """ 

968 Parse the GCRA rate limiting command response into a GCRAResponse dataclass. 

969 

970 Response format: [limited, max_req_num, num_avail_req, retry_after, full_burst_after] 

971 """ 

972 return GCRAResponse( 

973 limited=bool(response[0]), 

974 max_req_num=int(response[1]), 

975 num_avail_req=int(response[2]), 

976 retry_after=int(response[3]), 

977 full_burst_after=int(response[4]), 

978 ) 

979 

980 

981def parse_function_list(response): 

982 """Parse FUNCTION LIST response from RESP2 flat lists into nested dicts. 

983 

984 RESP2 returns: [[key, val, key, val, ...], ...] 

985 where nested 'functions' values are also flat lists. 

986 Converts to match RESP3's native dict format. 

987 """ 

988 result = [] 

989 for lib_flat in response: 

990 lib_dict = pairs_to_dict(lib_flat) 

991 # Convert each function's flat list to a dict. 

992 # The key is b"functions" normally, but "functions" when 

993 # decode_responses=True. 

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

995 if func_key in lib_dict: 

996 lib_dict[func_key] = [pairs_to_dict(func) for func in lib_dict[func_key]] 

997 result.append(lib_dict) 

998 return result 

999 

1000 

1001def string_keys_to_dict(key_string, callback): 

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

1003 

1004 

1005_RedisCallbacks = { 

1006 **string_keys_to_dict( 

1007 "AUTH COPY EXPIRE EXPIREAT HEXISTS HMSET MOVE MSETNX PERSIST PSETEX " 

1008 "PEXPIRE PEXPIREAT RENAMENX SETEX SETNX SMOVE", 

1009 bool, 

1010 ), 

1011 **string_keys_to_dict("HINCRBYFLOAT INCRBYFLOAT", float), 

1012 **string_keys_to_dict( 

1013 "ASKING FLUSHALL FLUSHDB LSET LTRIM MSET PFMERGE READONLY READWRITE " 

1014 "RENAME SAVE SELECT SHUTDOWN SLAVEOF SWAPDB WATCH UNWATCH", 

1015 bool_ok, 

1016 ), 

1017 **string_keys_to_dict("XREAD XREADGROUP", parse_xread), 

1018 **string_keys_to_dict( 

1019 "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH", 

1020 parse_geosearch_generic, 

1021 ), 

1022 **string_keys_to_dict("XRANGE XREVRANGE", parse_stream_list), 

1023 "ACL GETUSER": parse_acl_getuser, 

1024 "ACL LOAD": bool_ok, 

1025 "ACL LOG": parse_acl_log, 

1026 "ACL SETUSER": bool_ok, 

1027 "ACL SAVE": bool_ok, 

1028 "CLIENT INFO": parse_client_info, 

1029 "CLIENT KILL": parse_client_kill, 

1030 "CLIENT LIST": parse_client_list, 

1031 "CLIENT PAUSE": bool_ok, 

1032 "CLIENT SETINFO": bool_ok, 

1033 "CLIENT TRACKINGINFO": parse_client_trackinginfo, 

1034 "CLIENT SETNAME": bool_ok, 

1035 "CLIENT UNBLOCK": bool, 

1036 "CLUSTER ADDSLOTS": bool_ok, 

1037 "CLUSTER ADDSLOTSRANGE": bool_ok, 

1038 "CLUSTER DELSLOTS": bool_ok, 

1039 "CLUSTER DELSLOTSRANGE": bool_ok, 

1040 "CLUSTER FAILOVER": bool_ok, 

1041 "CLUSTER FORGET": bool_ok, 

1042 "CLUSTER INFO": parse_cluster_info, 

1043 "CLUSTER LINKS": parse_cluster_links, 

1044 "CLUSTER MEET": bool_ok, 

1045 "CLUSTER NODES": parse_cluster_nodes, 

1046 "CLUSTER REPLICAS": parse_cluster_nodes, 

1047 "CLUSTER REPLICATE": bool_ok, 

1048 "CLUSTER RESET": bool_ok, 

1049 "CLUSTER SAVECONFIG": bool_ok, 

1050 "CLUSTER SET-CONFIG-EPOCH": bool_ok, 

1051 "CLUSTER SETSLOT": bool_ok, 

1052 "CLUSTER SLAVES": parse_cluster_nodes, 

1053 "COMMAND": parse_command, 

1054 "CONFIG RESETSTAT": bool_ok, 

1055 "CONFIG SET": bool_ok, 

1056 "FUNCTION DELETE": bool_ok, 

1057 "FUNCTION FLUSH": bool_ok, 

1058 "FUNCTION RESTORE": bool_ok, 

1059 "GCRA": parse_gcra, 

1060 "GEODIST": float_or_none, 

1061 "HSCAN": parse_hscan, 

1062 "INFO": parse_info, 

1063 "LASTSAVE": timestamp_to_datetime, 

1064 "MEMORY PURGE": bool_ok, 

1065 "MODULE LOAD": bool, 

1066 "MODULE UNLOAD": bool, 

1067 "PING": lambda r: str_if_bytes(r) == "PONG", 

1068 "PUBSUB NUMSUB": parse_pubsub_numsub, 

1069 "PUBSUB SHARDNUMSUB": parse_pubsub_numsub, 

1070 "QUIT": bool_ok, 

1071 "SET": parse_set_result, 

1072 "SCAN": parse_scan, 

1073 "SCRIPT EXISTS": lambda r: list(map(bool, r)), 

1074 "SCRIPT FLUSH": bool_ok, 

1075 "SCRIPT KILL": bool_ok, 

1076 "SCRIPT LOAD": str_if_bytes, 

1077 "SENTINEL CKQUORUM": bool_ok, 

1078 "SENTINEL FAILOVER": bool_ok, 

1079 "SENTINEL FLUSHCONFIG": bool_ok, 

1080 "SENTINEL GET-MASTER-ADDR-BY-NAME": parse_sentinel_get_master, 

1081 "SENTINEL MONITOR": bool_ok, 

1082 "SENTINEL RESET": bool_ok, 

1083 "SENTINEL REMOVE": bool_ok, 

1084 "SENTINEL SET": bool_ok, 

1085 "SLOWLOG GET": parse_slowlog_get, 

1086 "SLOWLOG RESET": bool_ok, 

1087 "SORT": sort_return_tuples, 

1088 "SSCAN": parse_scan, 

1089 "TIME": lambda x: (int(x[0]), int(x[1])), 

1090 "XAUTOCLAIM": parse_xautoclaim, 

1091 "XCLAIM": parse_xclaim, 

1092 "XGROUP CREATE": bool_ok, 

1093 "XGROUP DESTROY": bool, 

1094 "XGROUP SETID": bool_ok, 

1095 "XINFO STREAM": parse_xinfo_stream, 

1096 "XPENDING": parse_xpending, 

1097 "ZSCAN": parse_zscan, 

1098 "LCS": parse_lcs, 

1099} 

1100 

1101 

1102_RedisCallbacksRESP2 = { 

1103 **string_keys_to_dict( 

1104 "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set() 

1105 ), 

1106 **string_keys_to_dict( 

1107 "ZDIFF ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION", 

1108 zset_score_pairs, 

1109 ), 

1110 **string_keys_to_dict( 

1111 "ZPOPMAX ZPOPMIN", 

1112 zpop_score_pairs, 

1113 ), 

1114 **string_keys_to_dict( 

1115 "ZREVRANK ZRANK", 

1116 zset_score_for_rank, 

1117 ), 

1118 **string_keys_to_dict("ZINCRBY ZSCORE", float_or_none), 

1119 **string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True), 

1120 **string_keys_to_dict("BLPOP BRPOP", lambda r: r and list(r) or None), 

1121 **string_keys_to_dict( 

1122 "BZPOPMAX BZPOPMIN", 

1123 lambda r: r and [r[0], r[1], float(r[2])] or None, 

1124 ), 

1125 "ACL CAT": lambda r: list(map(str_if_bytes, r)), 

1126 "ACL GENPASS": str_if_bytes, 

1127 "ACL HELP": lambda r: list(map(str_if_bytes, r)), 

1128 "ACL LIST": lambda r: list(map(str_if_bytes, r)), 

1129 "ACL USERS": lambda r: list(map(str_if_bytes, r)), 

1130 "ACL WHOAMI": str_if_bytes, 

1131 "CLIENT GETNAME": str_if_bytes, 

1132 "CONFIG GET": parse_config_get, 

1133 "DEBUG OBJECT": parse_debug_object, 

1134 "GEOHASH": lambda r: list(map(str_if_bytes, r)), 

1135 "GEOPOS": lambda r: list( 

1136 map(lambda ll: [float(ll[0]), float(ll[1])] if ll is not None else None, r) 

1137 ), 

1138 "HGETALL": lambda r: r and pairs_to_dict(r) or {}, 

1139 "HOTKEYS GET": lambda r: [pairs_to_dict(m) for m in r], 

1140 "MEMORY STATS": parse_memory_stats, 

1141 "MODULE LIST": lambda r: [pairs_to_dict(m) for m in r], 

1142 "RESET": str_if_bytes, 

1143 "SENTINEL MASTER": parse_sentinel_master, 

1144 "SENTINEL MASTERS": parse_sentinel_masters, 

1145 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels, 

1146 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels, 

1147 "STRALGO": parse_stralgo, 

1148 "FUNCTION LIST": parse_function_list, 

1149 "XINFO CONSUMERS": parse_list_of_dicts, 

1150 "XINFO GROUPS": parse_list_of_dicts, 

1151 "HRANDFIELD": hrandfield_pairs, 

1152 "ZADD": parse_zadd, 

1153 "ZMPOP": parse_zmpop, 

1154 "BZMPOP": parse_zmpop, 

1155 "ZMSCORE": parse_zmscore, 

1156 "ZRANDMEMBER": zset_score_pairs, 

1157} 

1158 

1159 

1160_RedisCallbacksRESP3 = { 

1161 **string_keys_to_dict( 

1162 "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set() 

1163 ), 

1164 **string_keys_to_dict( 

1165 "HGETALL", 

1166 lambda r, **kwargs: r, 

1167 ), 

1168 **string_keys_to_dict( 

1169 "ZDIFF ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION", 

1170 zset_score_pairs_resp3, 

1171 ), 

1172 **string_keys_to_dict( 

1173 "ZPOPMAX ZPOPMIN", 

1174 zpop_score_pairs_resp3, 

1175 ), 

1176 **string_keys_to_dict( 

1177 "ZREVRANK ZRANK", 

1178 zset_score_for_rank_resp3, 

1179 ), 

1180 **string_keys_to_dict( 

1181 "BZPOPMAX BZPOPMIN", 

1182 lambda r: r 

1183 and [r[0], r[1], r[2] if isinstance(r[2], float) else float(r[2])] 

1184 or None, 

1185 ), 

1186 **string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3), 

1187 **string_keys_to_dict("ZMPOP BZMPOP", parse_zmpop), 

1188 "ZRANDMEMBER": zset_score_pairs_resp3, 

1189 "ACL CAT": lambda r: list(map(str_if_bytes, r)), 

1190 "ACL GENPASS": str_if_bytes, 

1191 "ACL HELP": lambda r: list(map(str_if_bytes, r)), 

1192 "ACL LIST": lambda r: list(map(str_if_bytes, r)), 

1193 "ACL USERS": lambda r: list(map(str_if_bytes, r)), 

1194 "ACL WHOAMI": str_if_bytes, 

1195 "CLIENT GETNAME": str_if_bytes, 

1196 "GEOHASH": lambda r: list(map(str_if_bytes, r)), 

1197 "RESET": str_if_bytes, 

1198 "ACL LOG": parse_acl_log_resp3, 

1199 "COMMAND": parse_command_resp3, 

1200 "CONFIG GET": lambda r: { 

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

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

1203 ) 

1204 for key, value in r.items() 

1205 }, 

1206 **string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True), 

1207 "DEBUG OBJECT": parse_debug_object, 

1208 "MEMORY STATS": parse_memory_stats_resp3, 

1209 "SENTINEL MASTER": parse_sentinel_state_resp3, 

1210 "SENTINEL MASTERS": parse_sentinel_masters_resp3, 

1211 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3, 

1212 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3, 

1213 "STRALGO": lambda r, **options: parse_stralgo_resp3(r, **options), 

1214 "XINFO CONSUMERS": lambda r: [ 

1215 {str_if_bytes(key): value for key, value in x.items()} for x in r 

1216 ], 

1217 "XINFO GROUPS": lambda r: [ 

1218 {str_if_bytes(key): value for key, value in d.items()} for d in r 

1219 ], 

1220}