图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
11 KiB

  1. from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
  2. from redis.exceptions import RedisError, ResponseError
  3. from redis.utils import str_if_bytes
  4. if TYPE_CHECKING:
  5. from redis.asyncio.cluster import ClusterNode
  6. class AbstractCommandsParser:
  7. def _get_pubsub_keys(self, *args):
  8. """
  9. Get the keys from pubsub command.
  10. Although PubSub commands have predetermined key locations, they are not
  11. supported in the 'COMMAND's output, so the key positions are hardcoded
  12. in this method
  13. """
  14. if len(args) < 2:
  15. # The command has no keys in it
  16. return None
  17. args = [str_if_bytes(arg) for arg in args]
  18. command = args[0].upper()
  19. keys = None
  20. if command == "PUBSUB":
  21. # the second argument is a part of the command name, e.g.
  22. # ['PUBSUB', 'NUMSUB', 'foo'].
  23. pubsub_type = args[1].upper()
  24. if pubsub_type in ["CHANNELS", "NUMSUB", "SHARDCHANNELS", "SHARDNUMSUB"]:
  25. keys = args[2:]
  26. elif command in ["SUBSCRIBE", "PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE"]:
  27. # format example:
  28. # SUBSCRIBE channel [channel ...]
  29. keys = list(args[1:])
  30. elif command in ["PUBLISH", "SPUBLISH"]:
  31. # format example:
  32. # PUBLISH channel message
  33. keys = [args[1]]
  34. return keys
  35. def parse_subcommand(self, command, **options):
  36. cmd_dict = {}
  37. cmd_name = str_if_bytes(command[0])
  38. cmd_dict["name"] = cmd_name
  39. cmd_dict["arity"] = int(command[1])
  40. cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]]
  41. cmd_dict["first_key_pos"] = command[3]
  42. cmd_dict["last_key_pos"] = command[4]
  43. cmd_dict["step_count"] = command[5]
  44. if len(command) > 7:
  45. cmd_dict["tips"] = command[7]
  46. cmd_dict["key_specifications"] = command[8]
  47. cmd_dict["subcommands"] = command[9]
  48. return cmd_dict
  49. class CommandsParser(AbstractCommandsParser):
  50. """
  51. Parses Redis commands to get command keys.
  52. COMMAND output is used to determine key locations.
  53. Commands that do not have a predefined key location are flagged with
  54. 'movablekeys', and these commands' keys are determined by the command
  55. 'COMMAND GETKEYS'.
  56. """
  57. def __init__(self, redis_connection):
  58. self.commands = {}
  59. self.initialize(redis_connection)
  60. def initialize(self, r):
  61. commands = r.command()
  62. uppercase_commands = []
  63. for cmd in commands:
  64. if any(x.isupper() for x in cmd):
  65. uppercase_commands.append(cmd)
  66. for cmd in uppercase_commands:
  67. commands[cmd.lower()] = commands.pop(cmd)
  68. self.commands = commands
  69. # As soon as this PR is merged into Redis, we should reimplement
  70. # our logic to use COMMAND INFO changes to determine the key positions
  71. # https://github.com/redis/redis/pull/8324
  72. def get_keys(self, redis_conn, *args):
  73. """
  74. Get the keys from the passed command.
  75. NOTE: Due to a bug in redis<7.0, this function does not work properly
  76. for EVAL or EVALSHA when the `numkeys` arg is 0.
  77. - issue: https://github.com/redis/redis/issues/9493
  78. - fix: https://github.com/redis/redis/pull/9733
  79. So, don't use this function with EVAL or EVALSHA.
  80. """
  81. if len(args) < 2:
  82. # The command has no keys in it
  83. return None
  84. cmd_name = args[0].lower()
  85. if cmd_name not in self.commands:
  86. # try to split the command name and to take only the main command,
  87. # e.g. 'memory' for 'memory usage'
  88. cmd_name_split = cmd_name.split()
  89. cmd_name = cmd_name_split[0]
  90. if cmd_name in self.commands:
  91. # save the splitted command to args
  92. args = cmd_name_split + list(args[1:])
  93. else:
  94. # We'll try to reinitialize the commands cache, if the engine
  95. # version has changed, the commands may not be current
  96. self.initialize(redis_conn)
  97. if cmd_name not in self.commands:
  98. raise RedisError(
  99. f"{cmd_name.upper()} command doesn't exist in Redis commands"
  100. )
  101. command = self.commands.get(cmd_name)
  102. if "movablekeys" in command["flags"]:
  103. keys = self._get_moveable_keys(redis_conn, *args)
  104. elif "pubsub" in command["flags"] or command["name"] == "pubsub":
  105. keys = self._get_pubsub_keys(*args)
  106. else:
  107. if (
  108. command["step_count"] == 0
  109. and command["first_key_pos"] == 0
  110. and command["last_key_pos"] == 0
  111. ):
  112. is_subcmd = False
  113. if "subcommands" in command:
  114. subcmd_name = f"{cmd_name}|{args[1].lower()}"
  115. for subcmd in command["subcommands"]:
  116. if str_if_bytes(subcmd[0]) == subcmd_name:
  117. command = self.parse_subcommand(subcmd)
  118. is_subcmd = True
  119. # The command doesn't have keys in it
  120. if not is_subcmd:
  121. return None
  122. last_key_pos = command["last_key_pos"]
  123. if last_key_pos < 0:
  124. last_key_pos = len(args) - abs(last_key_pos)
  125. keys_pos = list(
  126. range(command["first_key_pos"], last_key_pos + 1, command["step_count"])
  127. )
  128. keys = [args[pos] for pos in keys_pos]
  129. return keys
  130. def _get_moveable_keys(self, redis_conn, *args):
  131. """
  132. NOTE: Due to a bug in redis<7.0, this function does not work properly
  133. for EVAL or EVALSHA when the `numkeys` arg is 0.
  134. - issue: https://github.com/redis/redis/issues/9493
  135. - fix: https://github.com/redis/redis/pull/9733
  136. So, don't use this function with EVAL or EVALSHA.
  137. """
  138. # The command name should be splitted into separate arguments,
  139. # e.g. 'MEMORY USAGE' will be splitted into ['MEMORY', 'USAGE']
  140. pieces = args[0].split() + list(args[1:])
  141. try:
  142. keys = redis_conn.execute_command("COMMAND GETKEYS", *pieces)
  143. except ResponseError as e:
  144. message = e.__str__()
  145. if (
  146. "Invalid arguments" in message
  147. or "The command has no key arguments" in message
  148. ):
  149. return None
  150. else:
  151. raise e
  152. return keys
  153. class AsyncCommandsParser(AbstractCommandsParser):
  154. """
  155. Parses Redis commands to get command keys.
  156. COMMAND output is used to determine key locations.
  157. Commands that do not have a predefined key location are flagged with 'movablekeys',
  158. and these commands' keys are determined by the command 'COMMAND GETKEYS'.
  159. NOTE: Due to a bug in redis<7.0, this does not work properly
  160. for EVAL or EVALSHA when the `numkeys` arg is 0.
  161. - issue: https://github.com/redis/redis/issues/9493
  162. - fix: https://github.com/redis/redis/pull/9733
  163. So, don't use this with EVAL or EVALSHA.
  164. """
  165. __slots__ = ("commands", "node")
  166. def __init__(self) -> None:
  167. self.commands: Dict[str, Union[int, Dict[str, Any]]] = {}
  168. async def initialize(self, node: Optional["ClusterNode"] = None) -> None:
  169. if node:
  170. self.node = node
  171. commands = await self.node.execute_command("COMMAND")
  172. self.commands = {cmd.lower(): command for cmd, command in commands.items()}
  173. # As soon as this PR is merged into Redis, we should reimplement
  174. # our logic to use COMMAND INFO changes to determine the key positions
  175. # https://github.com/redis/redis/pull/8324
  176. async def get_keys(self, *args: Any) -> Optional[Tuple[str, ...]]:
  177. """
  178. Get the keys from the passed command.
  179. NOTE: Due to a bug in redis<7.0, this function does not work properly
  180. for EVAL or EVALSHA when the `numkeys` arg is 0.
  181. - issue: https://github.com/redis/redis/issues/9493
  182. - fix: https://github.com/redis/redis/pull/9733
  183. So, don't use this function with EVAL or EVALSHA.
  184. """
  185. if len(args) < 2:
  186. # The command has no keys in it
  187. return None
  188. cmd_name = args[0].lower()
  189. if cmd_name not in self.commands:
  190. # try to split the command name and to take only the main command,
  191. # e.g. 'memory' for 'memory usage'
  192. cmd_name_split = cmd_name.split()
  193. cmd_name = cmd_name_split[0]
  194. if cmd_name in self.commands:
  195. # save the splitted command to args
  196. args = cmd_name_split + list(args[1:])
  197. else:
  198. # We'll try to reinitialize the commands cache, if the engine
  199. # version has changed, the commands may not be current
  200. await self.initialize()
  201. if cmd_name not in self.commands:
  202. raise RedisError(
  203. f"{cmd_name.upper()} command doesn't exist in Redis commands"
  204. )
  205. command = self.commands.get(cmd_name)
  206. if "movablekeys" in command["flags"]:
  207. keys = await self._get_moveable_keys(*args)
  208. elif "pubsub" in command["flags"] or command["name"] == "pubsub":
  209. keys = self._get_pubsub_keys(*args)
  210. else:
  211. if (
  212. command["step_count"] == 0
  213. and command["first_key_pos"] == 0
  214. and command["last_key_pos"] == 0
  215. ):
  216. is_subcmd = False
  217. if "subcommands" in command:
  218. subcmd_name = f"{cmd_name}|{args[1].lower()}"
  219. for subcmd in command["subcommands"]:
  220. if str_if_bytes(subcmd[0]) == subcmd_name:
  221. command = self.parse_subcommand(subcmd)
  222. is_subcmd = True
  223. # The command doesn't have keys in it
  224. if not is_subcmd:
  225. return None
  226. last_key_pos = command["last_key_pos"]
  227. if last_key_pos < 0:
  228. last_key_pos = len(args) - abs(last_key_pos)
  229. keys_pos = list(
  230. range(command["first_key_pos"], last_key_pos + 1, command["step_count"])
  231. )
  232. keys = [args[pos] for pos in keys_pos]
  233. return keys
  234. async def _get_moveable_keys(self, *args: Any) -> Optional[Tuple[str, ...]]:
  235. try:
  236. keys = await self.node.execute_command("COMMAND GETKEYS", *args)
  237. except ResponseError as e:
  238. message = e.__str__()
  239. if (
  240. "Invalid arguments" in message
  241. or "The command has no key arguments" in message
  242. ):
  243. return None
  244. else:
  245. raise e
  246. return keys