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

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

258 statements  

1from typing import ( 

2 TYPE_CHECKING, 

3 Any, 

4 Callable, 

5 Dict, 

6 Iterator, 

7 List, 

8 Optional, 

9 Set, 

10 Tuple, 

11 TypeVar, 

12 Union, 

13) 

14 

15from redis.commands.metadata import ( 

16 CommandMetadata, 

17 CommandMetadataRecordsCache, 

18 CommandPolicies, 

19 PolicyRecords, 

20 RequestPolicy, 

21 ResponsePolicy, 

22) 

23from redis.exceptions import IncorrectPolicyType, RedisError, ResponseError 

24from redis.utils import deprecated_function, str_if_bytes 

25 

26if TYPE_CHECKING: 

27 from redis.asyncio.cluster import ClusterNode 

28 

29# The record type ``_build_command_records`` builds per command: policies or full metadata. 

30# Constrained to the two, rather than left open, so a ``to_record`` that builds anything else 

31# is a type error at the call site. 

32_RecordT = TypeVar("_RecordT", CommandPolicies, CommandMetadata) 

33 

34# Re-exported for backwards compatibility: these types used to be defined here and are 

35# now owned by ``redis.commands.metadata``. Import them from their new home instead. 

36__all__ = [ 

37 "AbstractCommandsParser", 

38 "AsyncCommandsParser", 

39 "CommandPolicies", 

40 "CommandsParser", 

41 "PolicyRecords", 

42 "RequestPolicy", 

43 "ResponsePolicy", 

44] 

45 

46 

47class AbstractCommandsParser: 

48 def _get_pubsub_keys(self, *args): 

49 """ 

50 Get the keys from pubsub command. 

51 Although PubSub commands have predetermined key locations, they are not 

52 supported in the 'COMMAND's output, so the key positions are hardcoded 

53 in this method 

54 """ 

55 if len(args) < 2: 

56 # The command has no keys in it 

57 return None 

58 args = [str_if_bytes(arg) for arg in args] 

59 command = args[0].upper() 

60 keys = None 

61 if command == "PUBSUB": 

62 # the second argument is a part of the command name, e.g. 

63 # ['PUBSUB', 'NUMSUB', 'foo']. 

64 pubsub_type = args[1].upper() 

65 if pubsub_type in ["CHANNELS", "NUMSUB", "SHARDCHANNELS", "SHARDNUMSUB"]: 

66 keys = args[2:] 

67 elif command in ["SUBSCRIBE", "PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE"]: 

68 # format example: 

69 # SUBSCRIBE channel [channel ...] 

70 keys = list(args[1:]) 

71 elif command in ["PUBLISH", "SPUBLISH"]: 

72 # format example: 

73 # PUBLISH channel message 

74 keys = [args[1]] 

75 return keys 

76 

77 def parse_subcommand(self, command, **options): 

78 return _parse_subcommand(command) 

79 

80 

81class CommandsParser(AbstractCommandsParser): 

82 """ 

83 Parses Redis commands to get command keys. 

84 COMMAND output is used to determine key locations. 

85 Commands that do not have a predefined key location are flagged with 

86 'movablekeys', and these commands' keys are determined by the command 

87 'COMMAND GETKEYS'. 

88 """ 

89 

90 def __init__(self, redis_connection): 

91 self.commands = {} 

92 self.redis_connection = redis_connection 

93 self.initialize(self.redis_connection) 

94 

95 def initialize(self, r): 

96 commands = r.command() 

97 uppercase_commands = [] 

98 for cmd in commands: 

99 if any(x.isupper() for x in cmd): 

100 uppercase_commands.append(cmd) 

101 for cmd in uppercase_commands: 

102 commands[cmd.lower()] = commands.pop(cmd) 

103 self.commands = commands 

104 

105 # As soon as this PR is merged into Redis, we should reimplement 

106 # our logic to use COMMAND INFO changes to determine the key positions 

107 # https://github.com/redis/redis/pull/8324 

108 def get_keys(self, redis_conn, *args): 

109 """ 

110 Get the keys from the passed command. 

111 

112 NOTE: Due to a bug in redis<7.0, this function does not work properly 

113 for EVAL or EVALSHA when the `numkeys` arg is 0. 

114 - issue: https://github.com/redis/redis/issues/9493 

115 - fix: https://github.com/redis/redis/pull/9733 

116 

117 So, don't use this function with EVAL or EVALSHA. 

118 """ 

119 if len(args) < 2: 

120 # The command has no keys in it 

121 return None 

122 

123 cmd_name = args[0].lower() 

124 if cmd_name not in self.commands: 

125 # try to split the command name and to take only the main command, 

126 # e.g. 'memory' for 'memory usage' 

127 cmd_name_split = cmd_name.split() 

128 cmd_name = cmd_name_split[0] 

129 if cmd_name in self.commands: 

130 # save the split command to args 

131 args = cmd_name_split + list(args[1:]) 

132 else: 

133 # We'll try to reinitialize the commands cache, if the engine 

134 # version has changed, the commands may not be current 

135 self.initialize(redis_conn) 

136 if cmd_name not in self.commands: 

137 raise RedisError( 

138 f"{cmd_name.upper()} command doesn't exist in Redis commands" 

139 ) 

140 

141 command = self.commands.get(cmd_name) 

142 if "movablekeys" in command["flags"]: 

143 keys = self._get_moveable_keys(redis_conn, *args) 

144 elif "pubsub" in command["flags"] or command["name"] == "pubsub": 

145 keys = self._get_pubsub_keys(*args) 

146 else: 

147 if ( 

148 command["step_count"] == 0 

149 and command["first_key_pos"] == 0 

150 and command["last_key_pos"] == 0 

151 ): 

152 is_subcmd = False 

153 if "subcommands" in command: 

154 subcmd_name = f"{cmd_name}|{args[1].lower()}" 

155 for subcmd in command["subcommands"]: 

156 if str_if_bytes(subcmd[0]) == subcmd_name: 

157 command = self.parse_subcommand(subcmd) 

158 

159 if command["first_key_pos"] > 0: 

160 is_subcmd = True 

161 

162 # The command doesn't have keys in it 

163 if not is_subcmd: 

164 return None 

165 last_key_pos = command["last_key_pos"] 

166 if last_key_pos < 0: 

167 last_key_pos = len(args) - abs(last_key_pos) 

168 keys_pos = list( 

169 range(command["first_key_pos"], last_key_pos + 1, command["step_count"]) 

170 ) 

171 keys = [args[pos] for pos in keys_pos] 

172 

173 return keys 

174 

175 def _get_moveable_keys(self, redis_conn, *args): 

176 """ 

177 NOTE: Due to a bug in redis<7.0, this function does not work properly 

178 for EVAL or EVALSHA when the `numkeys` arg is 0. 

179 - issue: https://github.com/redis/redis/issues/9493 

180 - fix: https://github.com/redis/redis/pull/9733 

181 

182 So, don't use this function with EVAL or EVALSHA. 

183 """ 

184 # The command name should be split into separate arguments, 

185 # e.g. 'MEMORY USAGE' will be split into ['MEMORY', 'USAGE'] 

186 pieces = args[0].split() + list(args[1:]) 

187 try: 

188 keys = redis_conn.execute_command("COMMAND GETKEYS", *pieces) 

189 except ResponseError as e: 

190 message = e.__str__() 

191 if ( 

192 "Invalid arguments" in message 

193 or "The command has no key arguments" in message 

194 ): 

195 return None 

196 else: 

197 raise e 

198 return keys 

199 

200 @deprecated_function( 

201 version="8.2.0", 

202 reason="Use get_commands_metadata_cache() instead.", 

203 ) 

204 def get_command_policies(self) -> PolicyRecords: 

205 """ 

206 Retrieve and process the command policies for all commands and subcommands. 

207 

208 DEPRECATED: use :meth:`get_commands_metadata_cache` instead. Nothing in this library calls this 

209 method any more, and it will be removed in a future release. A metadata resolver keeps a 

210 compatibility shim for an object that serves only this method, because it was the whole 

211 contract in 7.1.0; that shim is not an invitation to write one. To decide routing 

212 yourself, implement the public ``PolicyResolver`` ABC instead. 

213 

214 This method traverses through commands and subcommands, extracting policy details 

215 from associated data structures and constructing a dictionary of commands with their 

216 associated policies. It supports nested data structures and handles both main commands 

217 and their subcommands. 

218 

219 This is the routing view of :meth:`get_commands_metadata_cache`: the same traversal of the 

220 same reply, keyed the same way, projected down to the two policies the cluster client 

221 routes by. A caller that also needs the client-side-caching metadata asks for the 

222 metadata records instead. 

223 

224 Returns: 

225 PolicyRecords: A collection of commands and subcommands associated with their 

226 respective policies. 

227 

228 Raises: 

229 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

230 """ 

231 return _build_policy_records(self.commands) 

232 

233 def get_commands_metadata_cache(self) -> CommandMetadataRecordsCache: 

234 """ 

235 Retrieve and process the metadata records cache for all commands and subcommands. 

236 

237 This method normalizes the command flags, command tips and key metadata of the 

238 ``COMMAND`` output into metadata records, keyed the same way as the policy records 

239 that the deprecated ``get_command_policies`` returns. The routing policies each 

240 record carries are the ones that method resolves, so both views of the same reply 

241 stay in step. 

242 

243 Returns: 

244 CommandMetadataRecordsCache: A collection of commands and subcommands 

245 associated with their respective metadata. 

246 

247 Raises: 

248 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

249 """ 

250 return _build_commands_metadata_cache(self.commands) 

251 

252 

253class AsyncCommandsParser(AbstractCommandsParser): 

254 """ 

255 Parses Redis commands to get command keys. 

256 

257 COMMAND output is used to determine key locations. 

258 Commands that do not have a predefined key location are flagged with 'movablekeys', 

259 and these commands' keys are determined by the command 'COMMAND GETKEYS'. 

260 

261 NOTE: Due to a bug in redis<7.0, this does not work properly 

262 for EVAL or EVALSHA when the `numkeys` arg is 0. 

263 - issue: https://github.com/redis/redis/issues/9493 

264 - fix: https://github.com/redis/redis/pull/9733 

265 

266 So, don't use this with EVAL or EVALSHA. 

267 """ 

268 

269 __slots__ = ("commands", "node") 

270 

271 def __init__(self) -> None: 

272 self.commands: Dict[str, Union[int, Dict[str, Any]]] = {} 

273 

274 async def initialize(self, node: Optional["ClusterNode"] = None) -> None: 

275 if node: 

276 self.node = node 

277 

278 commands = await self.node.execute_command("COMMAND") 

279 self.commands = {cmd.lower(): command for cmd, command in commands.items()} 

280 

281 # As soon as this PR is merged into Redis, we should reimplement 

282 # our logic to use COMMAND INFO changes to determine the key positions 

283 # https://github.com/redis/redis/pull/8324 

284 async def get_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: 

285 """ 

286 Get the keys from the passed command. 

287 

288 NOTE: Due to a bug in redis<7.0, this function does not work properly 

289 for EVAL or EVALSHA when the `numkeys` arg is 0. 

290 - issue: https://github.com/redis/redis/issues/9493 

291 - fix: https://github.com/redis/redis/pull/9733 

292 

293 So, don't use this function with EVAL or EVALSHA. 

294 """ 

295 if len(args) < 2: 

296 # The command has no keys in it 

297 return None 

298 

299 cmd_name = args[0].lower() 

300 if cmd_name not in self.commands: 

301 # try to split the command name and to take only the main command, 

302 # e.g. 'memory' for 'memory usage' 

303 cmd_name_split = cmd_name.split() 

304 cmd_name = cmd_name_split[0] 

305 if cmd_name in self.commands: 

306 # save the split command to args 

307 args = cmd_name_split + list(args[1:]) 

308 else: 

309 # We'll try to reinitialize the commands cache, if the engine 

310 # version has changed, the commands may not be current 

311 await self.initialize() 

312 if cmd_name not in self.commands: 

313 raise RedisError( 

314 f"{cmd_name.upper()} command doesn't exist in Redis commands" 

315 ) 

316 

317 command = self.commands.get(cmd_name) 

318 if "movablekeys" in command["flags"]: 

319 keys = await self._get_moveable_keys(*args) 

320 elif "pubsub" in command["flags"] or command["name"] == "pubsub": 

321 keys = self._get_pubsub_keys(*args) 

322 else: 

323 if ( 

324 command["step_count"] == 0 

325 and command["first_key_pos"] == 0 

326 and command["last_key_pos"] == 0 

327 ): 

328 is_subcmd = False 

329 if "subcommands" in command: 

330 subcmd_name = f"{cmd_name}|{args[1].lower()}" 

331 for subcmd in command["subcommands"]: 

332 if str_if_bytes(subcmd[0]) == subcmd_name: 

333 command = self.parse_subcommand(subcmd) 

334 

335 if command["first_key_pos"] > 0: 

336 is_subcmd = True 

337 

338 # The command doesn't have keys in it 

339 if not is_subcmd: 

340 return None 

341 last_key_pos = command["last_key_pos"] 

342 if last_key_pos < 0: 

343 last_key_pos = len(args) - abs(last_key_pos) 

344 keys_pos = list( 

345 range(command["first_key_pos"], last_key_pos + 1, command["step_count"]) 

346 ) 

347 keys = [args[pos] for pos in keys_pos] 

348 

349 return keys 

350 

351 async def _get_moveable_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: 

352 try: 

353 keys = await self.node.execute_command("COMMAND GETKEYS", *args) 

354 except ResponseError as e: 

355 message = e.__str__() 

356 if ( 

357 "Invalid arguments" in message 

358 or "The command has no key arguments" in message 

359 ): 

360 return None 

361 else: 

362 raise e 

363 return keys 

364 

365 @deprecated_function( 

366 version="8.2.0", 

367 reason="Use get_commands_metadata_cache() instead.", 

368 ) 

369 async def get_command_policies(self) -> PolicyRecords: 

370 """ 

371 Retrieve and process the command policies for all commands and subcommands. 

372 

373 DEPRECATED: use :meth:`get_commands_metadata_cache` instead. Nothing in this library calls this 

374 method any more, and it will be removed in a future release. A metadata resolver keeps a 

375 compatibility shim for an object that serves only this method, because it was the whole 

376 contract in 7.1.0; that shim is not an invitation to write one. To decide routing 

377 yourself, implement the public ``PolicyResolver`` ABC instead. 

378 

379 This method traverses through commands and subcommands, extracting policy details 

380 from associated data structures and constructing a dictionary of commands with their 

381 associated policies. It supports nested data structures and handles both main commands 

382 and their subcommands. 

383 

384 This is the routing view of :meth:`get_commands_metadata_cache`: the same traversal of the 

385 same reply, keyed the same way, projected down to the two policies the cluster client 

386 routes by. A caller that also needs the client-side-caching metadata asks for the 

387 metadata records instead. 

388 

389 Returns: 

390 PolicyRecords: A collection of commands and subcommands associated with their 

391 respective policies. 

392 

393 Raises: 

394 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

395 """ 

396 return _build_policy_records(self.commands) 

397 

398 async def get_commands_metadata_cache(self) -> CommandMetadataRecordsCache: 

399 """ 

400 Retrieve and process the metadata records cache for all commands and subcommands. 

401 

402 This method normalizes the command flags, command tips and key metadata of the 

403 ``COMMAND`` output into metadata records, keyed the same way as the policy records 

404 that the deprecated ``get_command_policies`` returns. The routing policies each 

405 record carries are the ones that method resolves, so both views of the same reply 

406 stay in step. 

407 

408 Returns: 

409 CommandMetadataRecordsCache: A collection of commands and subcommands 

410 associated with their respective metadata. 

411 

412 Raises: 

413 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

414 """ 

415 return _build_commands_metadata_cache(self.commands) 

416 

417 

418# ============================================================================= 

419# Private helpers 

420# ============================================================================= 

421def _parse_subcommand(command: Any) -> Dict[str, Any]: 

422 """ 

423 Parse a single entry of a command's ``subcommands`` field into a details dict 

424 with the same shape as a top-level ``COMMAND`` entry. 

425 """ 

426 cmd_dict = {} 

427 cmd_name = str_if_bytes(command[0]) 

428 cmd_dict["name"] = cmd_name 

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

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

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

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

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

434 if len(command) > 7: 

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

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

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

438 return cmd_dict 

439 

440 

441def _is_keyless_command( 

442 commands: Dict[str, Any], 

443 command_name: str, 

444 subcommand_name: Optional[str] = None, 

445) -> bool: 

446 """ 

447 Determines whether a given command or subcommand is considered "keyless". 

448 

449 A keyless command does not operate on specific keys, which is determined based 

450 on the first key position in the command or subcommand details. If the command 

451 or subcommand's first key position is zero or negative, it is treated as keyless. 

452 

453 Parameters: 

454 commands: Dict[str, Any] 

455 The parsed ``COMMAND`` output to look the command up in. 

456 command_name: str 

457 The name of the command to check. 

458 subcommand_name: Optional[str], default=None 

459 The name of the subcommand to check, if applicable. If not provided, 

460 the check is performed only on the command. 

461 

462 Returns: 

463 bool 

464 True if the specified command or subcommand is considered keyless, 

465 False otherwise. 

466 

467 Raises: 

468 ValueError 

469 If the specified subcommand is not found within the command or the 

470 specified command does not exist in the available commands. 

471 """ 

472 if subcommand_name: 

473 for subcommand in commands.get(command_name)["subcommands"]: 

474 if str_if_bytes(subcommand[0]) == subcommand_name: 

475 parsed_subcmd = _parse_subcommand(subcommand) 

476 return parsed_subcmd["first_key_pos"] <= 0 

477 raise ValueError( 

478 f"Subcommand {subcommand_name} not found in command {command_name}" 

479 ) 

480 else: 

481 command_details = commands.get(command_name, None) 

482 if command_details is not None: 

483 return command_details["first_key_pos"] <= 0 

484 

485 raise ValueError(f"Command {command_name} not found in commands") 

486 

487 

488# Slots of the two-element buffer ``_apply_policy_tips`` writes into. A list rather than a 

489# record, so one buffer can be reused for every command of a ``COMMAND`` reply instead of a 

490# policy object being allocated per command and thrown away. 

491_REQUEST_POLICY = 0 

492_RESPONSE_POLICY = 1 

493 

494 

495def _walk_tips(data: Any) -> Iterator[str]: 

496 """ 

497 Recursively yield every tip string of a ``COMMAND`` reply fragment, decoding bytes. 

498 

499 The one traversal both tip readers share, so the routing view and the metadata view 

500 cannot disagree about which structures a tip may be nested inside. That matters because a 

501 metadata record is the superset of a policy record: it carries the two routing policies 

502 plus the cacheability markers, so it has to read at least everything the policy view 

503 reads off the same field. 

504 

505 A ``COMMAND`` reply reports a command's ``tips`` as a flat array, but the fragments 

506 :func:`_apply_policy_tips` is handed for a container subcommand are whole reply entries, 

507 whose nested arrays and maps have to be walked. Anything that is neither a string, an 

508 array nor a map carries no tip and is skipped. 

509 

510 Args: 

511 data: The fragment to walk (can be list, dict, str, bytes, etc.) 

512 

513 Yields: 

514 Each tip string found, in reply order, so that a caller keeping only one value per 

515 slot ends up with the last one the reply carried. 

516 """ 

517 if isinstance(data, (str, bytes)): 

518 # Decode bytes to string if needed 

519 yield str_if_bytes(data) 

520 

521 elif isinstance(data, list): 

522 # For lists, recursively process each element 

523 for item in data: 

524 yield from _walk_tips(item) 

525 

526 elif isinstance(data, dict): 

527 # For dictionaries, recursively process each value 

528 for value in data.values(): 

529 yield from _walk_tips(value) 

530 

531 

532def _apply_policy_tips(policy_pair: List[Any], data: Any) -> None: 

533 """ 

534 Extract policies from nested data structures. 

535 

536 Args: 

537 policy_pair: The ``[request_policy, response_policy]`` buffer to update in place. 

538 The last policy tip found for a slot wins. 

539 data: The data structure to search, walked by :func:`_walk_tips`. 

540 

541 Raises: 

542 IncorrectPolicyType: If an invalid policy type is encountered. 

543 """ 

544 for policy in _walk_tips(data): 

545 # Check if this is a policy string 

546 if policy.startswith("request_policy"): 

547 policy_type = policy.split(":")[1] 

548 

549 try: 

550 policy_pair[_REQUEST_POLICY] = RequestPolicy(policy_type) 

551 except ValueError: 

552 raise IncorrectPolicyType( 

553 f"Incorrect request policy type: {policy_type}" 

554 ) 

555 

556 elif policy.startswith("response_policy"): 

557 policy_type = policy.split(":")[1] 

558 

559 try: 

560 policy_pair[_RESPONSE_POLICY] = ResponsePolicy(policy_type) 

561 except ValueError: 

562 raise IncorrectPolicyType( 

563 f"Incorrect response policy type: {policy_type}" 

564 ) 

565 

566 

567def _build_command_records( 

568 commands: Dict[str, Any], 

569 to_record: Callable[[Dict[str, Any], RequestPolicy, ResponsePolicy], _RecordT], 

570) -> Dict[str, Dict[str, _RecordT]]: 

571 """ 

572 Traverse a parsed ``COMMAND`` reply, resolving the policies of every command, and build 

573 one record per command with ``to_record``. 

574 

575 ``to_record`` is called once per command and once per container subcommand, with the 

576 command's details and its resolved policies. Policies start at the keyed or keyless 

577 defaults, chosen by whether the command takes keys, and are then overwritten by the 

578 ``request_policy`` and ``response_policy`` command tips. 

579 

580 This is the single traversal behind both the policy records and the metadata records, so 

581 the two are keyed identically by construction and the tips of a reply are walked once no 

582 matter which view a caller asks for. The policies are handed over as plain enum members, 

583 so the caller that wants a ``CommandPolicies`` record builds one and the caller that 

584 wants a ``CommandMetadata`` record is not charged for one it would discard. 

585 

586 Args: 

587 commands: The parsed ``COMMAND`` output to traverse. 

588 to_record: Builds the record for one command from its details and policies. 

589 

590 Returns: 

591 The records of every command and subcommand, keyed by module name, then by command 

592 name. 

593 

594 Raises: 

595 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

596 """ 

597 command_records: Dict[str, Dict[str, _RecordT]] = {} 

598 # Reused by every command below: the policies are read out of it before the next command 

599 # resets it, so no per-command buffer is allocated. 

600 policy_pair: List[Any] = [ 

601 RequestPolicy.DEFAULT_KEYLESS, 

602 ResponsePolicy.DEFAULT_KEYLESS, 

603 ] 

604 

605 for command, details in commands.items(): 

606 # Check whether the command has keys 

607 is_keyless = _is_keyless_command(commands, command) 

608 

609 if is_keyless: 

610 policy_pair[_REQUEST_POLICY] = RequestPolicy.DEFAULT_KEYLESS 

611 policy_pair[_RESPONSE_POLICY] = ResponsePolicy.DEFAULT_KEYLESS 

612 else: 

613 policy_pair[_REQUEST_POLICY] = RequestPolicy.DEFAULT_KEYED 

614 policy_pair[_RESPONSE_POLICY] = ResponsePolicy.DEFAULT_KEYED 

615 

616 module_name, command_name = _split_module_and_command(command) 

617 module_records = command_records.setdefault(module_name, {}) 

618 

619 tips = details.get("tips") 

620 subcommands = details.get("subcommands") 

621 

622 # Process tips for the main command 

623 if tips: 

624 _apply_policy_tips(policy_pair, tips) 

625 

626 module_records[command_name] = to_record( 

627 details, policy_pair[_REQUEST_POLICY], policy_pair[_RESPONSE_POLICY] 

628 ) 

629 

630 # Process subcommands 

631 if subcommands: 

632 for subcommand_details in subcommands: 

633 # Get the subcommand name (first element) 

634 subcmd_name = subcommand_details[0] 

635 if isinstance(subcmd_name, bytes): 

636 subcmd_name = subcmd_name.decode() 

637 

638 # Check whether the subcommand has keys 

639 is_keyless = _is_keyless_command(commands, command, subcmd_name) 

640 

641 if is_keyless: 

642 policy_pair[_REQUEST_POLICY] = RequestPolicy.DEFAULT_KEYLESS 

643 policy_pair[_RESPONSE_POLICY] = ResponsePolicy.DEFAULT_KEYLESS 

644 else: 

645 policy_pair[_REQUEST_POLICY] = RequestPolicy.DEFAULT_KEYED 

646 policy_pair[_RESPONSE_POLICY] = ResponsePolicy.DEFAULT_KEYED 

647 

648 # Container subcommands are keyed by their space-joined name, e.g. 

649 # ``memory usage``, which is the form ``execute_command`` receives. 

650 subcmd_name = subcmd_name.replace("|", " ") 

651 

652 # Recursively extract policies from the rest of the subcommand details 

653 for subcommand_detail in subcommand_details[1:]: 

654 _apply_policy_tips(policy_pair, subcommand_detail) 

655 

656 module_records[subcmd_name] = to_record( 

657 _parse_subcommand(subcommand_details), 

658 policy_pair[_REQUEST_POLICY], 

659 policy_pair[_RESPONSE_POLICY], 

660 ) 

661 

662 return command_records 

663 

664 

665def _to_command_policies( 

666 details: Dict[str, Any], 

667 request_policy: RequestPolicy, 

668 response_policy: ResponsePolicy, 

669) -> CommandPolicies: 

670 """Build the policy record for a single entry of a ``COMMAND`` reply.""" 

671 return CommandPolicies( 

672 request_policy=request_policy, response_policy=response_policy 

673 ) 

674 

675 

676def _build_policy_records(commands: Dict[str, Any]) -> PolicyRecords: 

677 """ 

678 Retrieve and process the command policies for all commands and subcommands. 

679 

680 This function traverses through commands and subcommands, extracting policy details 

681 from associated data structures and constructing a dictionary of commands with their 

682 associated policies. It supports nested data structures and handles both main commands 

683 and their subcommands. 

684 

685 Args: 

686 commands: The parsed ``COMMAND`` output to build the policy records from. 

687 

688 Returns: 

689 PolicyRecords: A collection of commands and subcommands associated with their 

690 respective policies. 

691 

692 Raises: 

693 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

694 """ 

695 return _build_command_records(commands, _to_command_policies) 

696 

697 

698def _split_module_and_command(command: str) -> Tuple[str, str]: 

699 """ 

700 Split a name from a ``COMMAND`` reply into the ``(module, command)`` pair the record 

701 tables are keyed by. Non-module commands live under ``"core"``. 

702 """ 

703 split_name = command.split(".") 

704 

705 if len(split_name) > 1: 

706 return split_name[0], split_name[1] 

707 

708 return "core", split_name[0] 

709 

710 

711def _key_spec_flags(key_spec: Any) -> Set[str]: 

712 """ 

713 The flags of a single key specification. 

714 

715 RESP3 reports a key spec as a map and RESP2 as a flat ``[name, value, ...]`` sequence, 

716 so both shapes are read here. 

717 """ 

718 if isinstance(key_spec, dict): 

719 flags = key_spec.get(b"flags") or key_spec.get("flags") or () 

720 else: 

721 flags = () 

722 for index in range(0, len(key_spec) - 1, 2): 

723 if str_if_bytes(key_spec[index]) == "flags": 

724 flags = key_spec[index + 1] 

725 break 

726 

727 return {str_if_bytes(flag) for flag in flags} 

728 

729 

730def _has_key_argument(details: Dict[str, Any]) -> bool: 

731 """ 

732 Whether the command accepts at least one Redis key name argument. 

733 

734 Key specs decide it whenever the server reports them, including for ``movablekeys`` 

735 commands, whose keys are not discoverable from the legacy positions at all. A spec 

736 flagged ``not_key`` describes an argument that is not a key name, such as a shard 

737 pubsub channel, so a command whose every spec is ``not_key`` takes no keys. 

738 

739 Only a server that reports no key specs falls back to the legacy positions. 

740 ``last_key_pos`` is never consulted: it is ``-1`` for variadic commands whose key 

741 arguments run to the end of the argument list, and must not disqualify one. 

742 """ 

743 key_specs = details.get("key_specifications") 

744 

745 if key_specs: 

746 return any("not_key" not in _key_spec_flags(spec) for spec in key_specs) 

747 

748 return details.get("first_key_pos", 0) > 0 and details.get("step_count", 0) > 0 

749 

750 

751def _to_command_metadata( 

752 details: Dict[str, Any], 

753 request_policy: RequestPolicy, 

754 response_policy: ResponsePolicy, 

755) -> CommandMetadata: 

756 """ 

757 Build the metadata record for a single entry of a ``COMMAND`` reply. 

758 

759 ``details`` is either a top-level entry or a ``_parse_subcommand`` result. The routing 

760 policies are passed in rather than derived again, so a record always carries the same 

761 policies ``_build_policy_records`` resolves for the command. 

762 """ 

763 flags = {str_if_bytes(flag) for flag in details.get("flags") or ()} 

764 # Walked rather than read as a flat sequence, through the same traversal the routing view 

765 # resolves its tips with: a metadata record is the superset of a policy record, so it must 

766 # not read less of the field than the policy view does. 

767 tips = set(_walk_tips(details.get("tips"))) 

768 

769 return CommandMetadata( 

770 request_policy=request_policy, 

771 response_policy=response_policy, 

772 is_readonly="readonly" in flags, 

773 is_blocking="blocking" in flags, 

774 has_key_argument=_has_key_argument(details), 

775 # Matched exactly: ``nondeterministic_output_order`` is a different tip, denoting 

776 # only that element order varies. 

777 has_nondeterministic_output="nondeterministic_output" in tips, 

778 is_script_runner="script_runner" in flags, 

779 is_dont_cache="dont_cache" in tips, 

780 # An empty tips list is a real answer; a missing tips key means the server is too 

781 # old to report tips at all, which makes the negative markers undetectable. 

782 has_complete_metadata="flags" in details and "tips" in details, 

783 ) 

784 

785 

786def _build_commands_metadata_cache( 

787 commands: Dict[str, Any], 

788) -> CommandMetadataRecordsCache: 

789 """ 

790 Retrieve and process the metadata records cache for all commands and subcommands. 

791 

792 This function traverses through commands and subcommands, normalizing the command 

793 flags, command tips and key metadata of each into a metadata record. It shares its 

794 traversal with ``_build_policy_records``, so records are keyed the same way and carry 

795 the same policies, and a metadata resolver and a policy resolver agree on the same reply. 

796 

797 Args: 

798 commands: The parsed ``COMMAND`` output to build the metadata records from. 

799 

800 Returns: 

801 CommandMetadataRecordsCache: A collection of commands and subcommands associated 

802 with their respective metadata. 

803 

804 Raises: 

805 IncorrectPolicyType: If an invalid policy type is encountered during policy extraction. 

806 """ 

807 return _build_command_records(commands, _to_command_metadata)