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

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

431 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, decode_string_values=True) 

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

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

92 stats[key] = pairs_to_dict( 

93 value, decode_keys=True, decode_string_values=True 

94 ) 

95 return stats 

96 

97 

98SENTINEL_STATE_TYPES = { 

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

100 "config-epoch": int, 

101 "down-after-milliseconds": int, 

102 "failover-timeout": int, 

103 "info-refresh": int, 

104 "last-hello-message": int, 

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

106 "last-ping-reply": int, 

107 "last-ping-sent": int, 

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

109 "master-port": int, 

110 "num-other-sentinels": int, 

111 "num-slaves": int, 

112 "o-down-time": int, 

113 "pending-commands": int, 

114 "parallel-syncs": int, 

115 "port": int, 

116 "quorum": int, 

117 "role-reported-time": int, 

118 "s-down-time": int, 

119 "slave-priority": int, 

120 "slave-repl-offset": int, 

121 "voted-leader-epoch": int, 

122} 

123 

124 

125def parse_sentinel_state(item): 

126 result = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES) 

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

128 for name, flag in ( 

129 ("is_master", "master"), 

130 ("is_slave", "slave"), 

131 ("is_sdown", "s_down"), 

132 ("is_odown", "o_down"), 

133 ("is_sentinel", "sentinel"), 

134 ("is_disconnected", "disconnected"), 

135 ("is_master_down", "master_down"), 

136 ): 

137 result[name] = flag in flags 

138 return result 

139 

140 

141def parse_sentinel_master(response, **options): 

142 return parse_sentinel_state(map(str_if_bytes, response)) 

143 

144 

145def parse_sentinel_state_resp3(response, **options): 

146 result = {} 

147 for key in response: 

148 try: 

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

150 result[str_if_bytes(key)] = value 

151 except Exception: 

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

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

154 result["flags"] = flags 

155 return result 

156 

157 

158def parse_sentinel_masters(response, **options): 

159 result = {} 

160 for item in response: 

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

162 result[state["name"]] = state 

163 return result 

164 

165 

166def parse_sentinel_masters_resp3(response, **options): 

167 return [parse_sentinel_state_resp3(master) for master in response] 

168 

169 

170def parse_sentinel_slaves_and_sentinels(response, **options): 

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

172 

173 

174def parse_sentinel_slaves_and_sentinels_resp3(response, **options): 

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

176 

177 

178def parse_sentinel_get_master(response, **options): 

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

180 

181 

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

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

184 if response is None: 

185 return {} 

186 if decode_keys or decode_string_values: 

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

188 # with a str_if_bytes() map 

189 keys = response[::2] 

190 if decode_keys: 

191 keys = map(str_if_bytes, keys) 

192 values = response[1::2] 

193 if decode_string_values: 

194 values = map(str_if_bytes, values) 

195 return dict(zip(keys, values)) 

196 else: 

197 it = iter(response) 

198 return dict(zip(it, it)) 

199 

200 

201def pairs_to_dict_typed(response, type_info): 

202 it = iter(response) 

203 result = {} 

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

205 if key in type_info: 

206 try: 

207 value = type_info[key](value) 

208 except Exception: 

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

210 # the string value 

211 pass 

212 result[key] = value 

213 return result 

214 

215 

216def _wrap_score_cast_func(score_cast_func): 

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

218 

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

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

221 parse scientific notation directly. Rather than unconditionally routing 

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

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

224 converting through float() on ValueError. 

225 """ 

226 if score_cast_func is float: 

227 return score_cast_func 

228 

229 def _safe_cast(x): 

230 try: 

231 return score_cast_func(x) 

232 except (ValueError, TypeError): 

233 return score_cast_func(float(x)) 

234 

235 return _safe_cast 

236 

237 

238def zset_score_pairs(response, **options): 

239 """ 

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

241 a list of (value, score) pairs 

242 """ 

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

244 return response 

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

246 it = iter(response) 

247 return list(zip(it, map(score_cast_func, it))) 

248 

249 

250def zset_score_for_rank(response, **options): 

251 """ 

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

253 a [value, score] pair 

254 """ 

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

256 return response 

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

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

259 

260 

261def zset_score_pairs_resp3(response, **options): 

262 """ 

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

264 a list of [value, score] pairs 

265 """ 

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

267 return response 

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

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

270 

271 

272def zset_score_for_rank_resp3(response, **options): 

273 """ 

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

275 a [value, score] pair 

276 """ 

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

278 return response 

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

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

281 

282 

283def sort_return_tuples(response, **options): 

284 """ 

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

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

287 """ 

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

289 return response 

290 n = options["groups"] 

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

292 

293 

294def parse_stream_list(response, **options): 

295 if response is None: 

296 return None 

297 data = [] 

298 for r in response: 

299 if r is not None: 

300 if "claim_min_idle_time" in options: 

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

302 else: 

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

304 else: 

305 data.append((None, None)) 

306 return data 

307 

308 

309def pairs_to_dict_with_str_keys(response): 

310 return pairs_to_dict(response, decode_keys=True) 

311 

312 

313def parse_list_of_dicts(response): 

314 return list(map(pairs_to_dict_with_str_keys, response)) 

315 

316 

317def parse_xclaim(response, **options): 

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

319 return response 

320 return parse_stream_list(response) 

321 

322 

323def parse_xautoclaim(response, **options): 

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

325 return response[1] 

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

327 return response 

328 

329 

330def parse_xinfo_stream(response, **options): 

331 if isinstance(response, list): 

332 data = pairs_to_dict(response, decode_keys=True) 

333 else: 

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

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

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

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

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

339 last = data["last-entry"] 

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

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

342 else: 

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

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

345 data["groups"] = [ 

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

347 ] 

348 for g in data["groups"]: 

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

350 g["consumers"] = [ 

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

352 ] 

353 else: 

354 data["groups"] = [ 

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

356 for group in data["groups"] 

357 ] 

358 return data 

359 

360 

361def parse_xread(response, **options): 

362 if response is None: 

363 return [] 

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

365 

366 

367def parse_xread_resp3(response, **options): 

368 if response is None: 

369 return {} 

370 return { 

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

372 } 

373 

374 

375def parse_xpending(response, **options): 

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

377 return parse_xpending_range(response) 

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

379 return { 

380 "pending": response[0], 

381 "min": response[1], 

382 "max": response[2], 

383 "consumers": consumers, 

384 } 

385 

386 

387def parse_xpending_range(response): 

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

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

390 

391 

392def float_or_none(response): 

393 if response is None: 

394 return None 

395 return float(response) 

396 

397 

398def bool_ok(response, **options): 

399 return str_if_bytes(response) == "OK" 

400 

401 

402def parse_zadd(response, **options): 

403 if response is None: 

404 return None 

405 if options.get("as_score"): 

406 return float(response) 

407 return int(response) 

408 

409 

410def parse_client_list(response, **options): 

411 clients = [] 

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

413 client_dict = {} 

414 tokens = c.split(" ") 

415 last_key = None 

416 for token in tokens: 

417 if "=" in token: 

418 # Values might contain '=' 

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

420 client_dict[key] = value 

421 last_key = key 

422 else: 

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

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

425 client_dict[last_key] += " " + token 

426 

427 if client_dict: 

428 clients.append(client_dict) 

429 return clients 

430 

431 

432def parse_config_get(response, **options): 

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

434 return response and pairs_to_dict(response) or {} 

435 

436 

437def parse_scan(response, **options): 

438 cursor, r = response 

439 return int(cursor), r 

440 

441 

442def parse_hscan(response, **options): 

443 cursor, r = response 

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

445 if no_values: 

446 payload = r or [] 

447 else: 

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

449 return int(cursor), payload 

450 

451 

452def parse_zscan(response, **options): 

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

454 cursor, r = response 

455 it = iter(r) 

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

457 

458 

459def parse_zmscore(response, **options): 

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

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

462 

463 

464def parse_slowlog_get(response, **options): 

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

466 

467 def parse_item(item): 

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

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

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

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

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

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

474 

475 # These fields are optional, depends on environment. 

476 if len(item) >= 6: 

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

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

479 else: 

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

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

482 

483 # These fields are optional, depends on environment. 

484 if len(item) >= 7: 

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

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

487 

488 return result 

489 

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

491 

492 

493def parse_stralgo(response, **options): 

494 """ 

495 Parse the response from `STRALGO` command. 

496 Without modifiers the returned value is string. 

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

498 (i.e integer). 

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

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

501 offset for each string, where there are matches. 

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

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

504 """ 

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

506 return int(response) 

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

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

509 matches = [ 

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

511 for match in response[1] 

512 ] 

513 else: 

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

515 return { 

516 str_if_bytes(response[0]): matches, 

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

518 } 

519 return str_if_bytes(response) 

520 

521 

522def parse_cluster_info(response, **options): 

523 response = str_if_bytes(response) 

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

525 

526 

527def _parse_node_line(line): 

528 line_items = line.split(" ") 

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

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

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

532 node_dict = { 

533 "node_id": node_id, 

534 "hostname": hostname, 

535 "flags": flags, 

536 "master_id": master_id, 

537 "last_ping_sent": ping, 

538 "last_pong_rcvd": pong, 

539 "epoch": epoch, 

540 "slots": [], 

541 "migrations": [], 

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

543 } 

544 if len(line_items) >= 9: 

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

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

547 return ip, node_dict 

548 

549 

550def _parse_slots(slot_ranges): 

551 slots, migrations = [], [] 

552 for s_range in slot_ranges: 

553 if "->-" in s_range: 

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

555 migrations.append( 

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

557 ) 

558 elif "-<-" in s_range: 

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

560 migrations.append( 

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

562 ) 

563 else: 

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

565 slots.append(s_range) 

566 

567 return slots, migrations 

568 

569 

570def parse_cluster_nodes(response, **options): 

571 """ 

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

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

574 """ 

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

576 response = response.splitlines() 

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

578 

579 

580def parse_geosearch_generic(response, **options): 

581 """ 

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

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

584 """ 

585 try: 

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

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

588 # with other command arguments. 

589 # relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER' 

590 return response 

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

592 return response 

593 

594 if not isinstance(response, list): 

595 response_list = [response] 

596 else: 

597 response_list = response 

598 

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

600 # just a bunch of places 

601 return response_list 

602 

603 cast = { 

604 "withdist": float, 

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

606 "withhash": int, 

607 } 

608 

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

610 # the properly native Python value. 

611 f = [lambda x: x] 

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

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

614 

615 

616def parse_command(response, **options): 

617 commands = {} 

618 for command in response: 

619 cmd_dict = {} 

620 cmd_name = str_if_bytes(command[0]) 

621 cmd_dict["name"] = cmd_name 

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

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

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

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

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

627 if len(command) > 7: 

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

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

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

631 commands[cmd_name] = cmd_dict 

632 return commands 

633 

634 

635def parse_command_resp3(response, **options): 

636 commands = {} 

637 for command in response: 

638 cmd_dict = {} 

639 cmd_name = str_if_bytes(command[0]) 

640 cmd_dict["name"] = cmd_name 

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

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

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

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

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

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

647 if len(command) > 7: 

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

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

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

651 

652 commands[cmd_name] = cmd_dict 

653 return commands 

654 

655 

656def parse_pubsub_numsub(response, **options): 

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

658 

659 

660def parse_client_kill(response, **options): 

661 if isinstance(response, int): 

662 return response 

663 return str_if_bytes(response) == "OK" 

664 

665 

666def parse_acl_getuser(response, **options): 

667 if response is None: 

668 return None 

669 if isinstance(response, list): 

670 data = pairs_to_dict(response, decode_keys=True) 

671 else: 

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

673 

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

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

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

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

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

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

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

681 data["keys"] = [] 

682 if "channels" in data: 

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

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

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

686 data["channels"] = [] 

687 if "selectors" in data: 

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

689 data["selectors"] = [ 

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

691 ] 

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

693 data["selectors"] = [ 

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

695 for selector in data["selectors"] 

696 ] 

697 

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

699 commands, categories = [], [] 

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

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

702 

703 data["commands"] = commands 

704 data["categories"] = categories 

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

706 return data 

707 

708 

709def parse_acl_log(response, **options): 

710 if response is None: 

711 return None 

712 if isinstance(response, list): 

713 data = [] 

714 for log in response: 

715 log_data = pairs_to_dict(log, True, True) 

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

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

718 

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

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

721 data.append(log_data) 

722 else: 

723 data = bool_ok(response) 

724 return data 

725 

726 

727def parse_client_info(value): 

728 """ 

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

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

731 """ 

732 client_info = {} 

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

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

735 client_info[key] = value 

736 

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

738 for int_key in { 

739 "id", 

740 "age", 

741 "idle", 

742 "db", 

743 "sub", 

744 "psub", 

745 "multi", 

746 "qbuf", 

747 "qbuf-free", 

748 "obl", 

749 "argv-mem", 

750 "oll", 

751 "omem", 

752 "tot-mem", 

753 }: 

754 if int_key in client_info: 

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

756 return client_info 

757 

758 

759def parse_set_result(response, **options): 

760 """ 

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

762 Parsing SET result into: 

763 - BOOL 

764 - String when GET argument is used 

765 """ 

766 if options.get("get"): 

767 # Redis will return a getCommand result. 

768 # See `setGenericCommand` in t_string.c 

769 return response 

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

771 

772 

773def parse_gcra(response, **options): 

774 """ 

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

776 

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

778 """ 

779 return GCRAResponse( 

780 limited=bool(response[0]), 

781 max_req_num=int(response[1]), 

782 num_avail_req=int(response[2]), 

783 retry_after=int(response[3]), 

784 full_burst_after=int(response[4]), 

785 ) 

786 

787 

788def string_keys_to_dict(key_string, callback): 

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

790 

791 

792_RedisCallbacks = { 

793 **string_keys_to_dict( 

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

795 "PEXPIRE PEXPIREAT RENAMENX SETEX SETNX SMOVE", 

796 bool, 

797 ), 

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

799 **string_keys_to_dict( 

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

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

802 bool_ok, 

803 ), 

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

805 **string_keys_to_dict( 

806 "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH", 

807 parse_geosearch_generic, 

808 ), 

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

810 "ACL GETUSER": parse_acl_getuser, 

811 "ACL LOAD": bool_ok, 

812 "ACL LOG": parse_acl_log, 

813 "ACL SETUSER": bool_ok, 

814 "ACL SAVE": bool_ok, 

815 "CLIENT INFO": parse_client_info, 

816 "CLIENT KILL": parse_client_kill, 

817 "CLIENT LIST": parse_client_list, 

818 "CLIENT PAUSE": bool_ok, 

819 "CLIENT SETINFO": bool_ok, 

820 "CLIENT SETNAME": bool_ok, 

821 "CLIENT UNBLOCK": bool, 

822 "CLUSTER ADDSLOTS": bool_ok, 

823 "CLUSTER ADDSLOTSRANGE": bool_ok, 

824 "CLUSTER DELSLOTS": bool_ok, 

825 "CLUSTER DELSLOTSRANGE": bool_ok, 

826 "CLUSTER FAILOVER": bool_ok, 

827 "CLUSTER FORGET": bool_ok, 

828 "CLUSTER INFO": parse_cluster_info, 

829 "CLUSTER MEET": bool_ok, 

830 "CLUSTER NODES": parse_cluster_nodes, 

831 "CLUSTER REPLICAS": parse_cluster_nodes, 

832 "CLUSTER REPLICATE": bool_ok, 

833 "CLUSTER RESET": bool_ok, 

834 "CLUSTER SAVECONFIG": bool_ok, 

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

836 "CLUSTER SETSLOT": bool_ok, 

837 "CLUSTER SLAVES": parse_cluster_nodes, 

838 "COMMAND": parse_command, 

839 "CONFIG RESETSTAT": bool_ok, 

840 "CONFIG SET": bool_ok, 

841 "FUNCTION DELETE": bool_ok, 

842 "FUNCTION FLUSH": bool_ok, 

843 "FUNCTION RESTORE": bool_ok, 

844 "GCRA": parse_gcra, 

845 "GEODIST": float_or_none, 

846 "HSCAN": parse_hscan, 

847 "INFO": parse_info, 

848 "LASTSAVE": timestamp_to_datetime, 

849 "MEMORY PURGE": bool_ok, 

850 "MODULE LOAD": bool, 

851 "MODULE UNLOAD": bool, 

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

853 "PUBSUB NUMSUB": parse_pubsub_numsub, 

854 "PUBSUB SHARDNUMSUB": parse_pubsub_numsub, 

855 "QUIT": bool_ok, 

856 "SET": parse_set_result, 

857 "SCAN": parse_scan, 

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

859 "SCRIPT FLUSH": bool_ok, 

860 "SCRIPT KILL": bool_ok, 

861 "SCRIPT LOAD": str_if_bytes, 

862 "SENTINEL CKQUORUM": bool_ok, 

863 "SENTINEL FAILOVER": bool_ok, 

864 "SENTINEL FLUSHCONFIG": bool_ok, 

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

866 "SENTINEL MONITOR": bool_ok, 

867 "SENTINEL RESET": bool_ok, 

868 "SENTINEL REMOVE": bool_ok, 

869 "SENTINEL SET": bool_ok, 

870 "SLOWLOG GET": parse_slowlog_get, 

871 "SLOWLOG RESET": bool_ok, 

872 "SORT": sort_return_tuples, 

873 "SSCAN": parse_scan, 

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

875 "XAUTOCLAIM": parse_xautoclaim, 

876 "XCLAIM": parse_xclaim, 

877 "XGROUP CREATE": bool_ok, 

878 "XGROUP DESTROY": bool, 

879 "XGROUP SETID": bool_ok, 

880 "XINFO STREAM": parse_xinfo_stream, 

881 "XPENDING": parse_xpending, 

882 "ZSCAN": parse_zscan, 

883} 

884 

885 

886_RedisCallbacksRESP2 = { 

887 **string_keys_to_dict( 

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

889 ), 

890 **string_keys_to_dict( 

891 "ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZREVRANGE " 

892 "ZREVRANGEBYSCORE ZUNION", 

893 zset_score_pairs, 

894 ), 

895 **string_keys_to_dict( 

896 "ZREVRANK ZRANK", 

897 zset_score_for_rank, 

898 ), 

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

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

901 **string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None), 

902 **string_keys_to_dict( 

903 "BZPOPMAX BZPOPMIN", lambda r: r and (r[0], r[1], float(r[2])) or None 

904 ), 

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

906 "ACL GENPASS": str_if_bytes, 

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

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

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

910 "ACL WHOAMI": str_if_bytes, 

911 "CLIENT GETNAME": str_if_bytes, 

912 "CLIENT TRACKINGINFO": lambda r: list(map(str_if_bytes, r)), 

913 "CLUSTER GETKEYSINSLOT": lambda r: list(map(str_if_bytes, r)), 

914 "COMMAND GETKEYS": lambda r: list(map(str_if_bytes, r)), 

915 "CONFIG GET": parse_config_get, 

916 "DEBUG OBJECT": parse_debug_object, 

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

918 "GEOPOS": lambda r: list( 

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

920 ), 

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

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

923 "MEMORY STATS": parse_memory_stats, 

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

925 "RESET": str_if_bytes, 

926 "SENTINEL MASTER": parse_sentinel_master, 

927 "SENTINEL MASTERS": parse_sentinel_masters, 

928 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels, 

929 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels, 

930 "STRALGO": parse_stralgo, 

931 "XINFO CONSUMERS": parse_list_of_dicts, 

932 "XINFO GROUPS": parse_list_of_dicts, 

933 "ZADD": parse_zadd, 

934 "ZMSCORE": parse_zmscore, 

935} 

936 

937 

938_RedisCallbacksRESP3 = { 

939 **string_keys_to_dict( 

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

941 ), 

942 **string_keys_to_dict( 

943 "ZRANGE ZINTER ZPOPMAX ZPOPMIN HGETALL XREADGROUP", 

944 lambda r, **kwargs: r, 

945 ), 

946 **string_keys_to_dict( 

947 "ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION", 

948 zset_score_pairs_resp3, 

949 ), 

950 **string_keys_to_dict( 

951 "ZREVRANK ZRANK", 

952 zset_score_for_rank_resp3, 

953 ), 

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

955 "ACL LOG": lambda r: ( 

956 [ 

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

958 for x in r 

959 ] 

960 if isinstance(r, list) 

961 else bool_ok(r) 

962 ), 

963 "COMMAND": parse_command_resp3, 

964 "CONFIG GET": lambda r: { 

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

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

967 ) 

968 for key, value in r.items() 

969 }, 

970 "MEMORY STATS": lambda r: {str_if_bytes(key): value for key, value in r.items()}, 

971 "SENTINEL MASTER": parse_sentinel_state_resp3, 

972 "SENTINEL MASTERS": parse_sentinel_masters_resp3, 

973 "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3, 

974 "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3, 

975 "STRALGO": lambda r, **options: ( 

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

977 if isinstance(r, dict) 

978 else str_if_bytes(r) 

979 ), 

980 "XINFO CONSUMERS": lambda r: [ 

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

982 ], 

983 "XINFO GROUPS": lambda r: [ 

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

985 ], 

986}