commands.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import os
  2. from json import JSONDecodeError, loads
  3. from typing import Dict, List, Optional, Tuple, Union
  4. from redis.exceptions import DataError
  5. from redis.utils import deprecated_function
  6. from ._util import JsonType
  7. from .decoders import decode_dict_keys
  8. from .path import Path
  9. class JSONCommands:
  10. """json commands."""
  11. def arrappend(
  12. self, name: str, path: Optional[str] = Path.root_path(), *args: List[JsonType]
  13. ) -> List[Union[int, None]]:
  14. """Append the objects ``args`` to the array under the
  15. ``path` in key ``name``.
  16. For more information see `JSON.ARRAPPEND <https://redis.io/commands/json.arrappend>`_..
  17. """ # noqa
  18. pieces = [name, str(path)]
  19. for o in args:
  20. pieces.append(self._encode(o))
  21. return self.execute_command("JSON.ARRAPPEND", *pieces)
  22. def arrindex(
  23. self,
  24. name: str,
  25. path: str,
  26. scalar: int,
  27. start: Optional[int] = None,
  28. stop: Optional[int] = None,
  29. ) -> List[Union[int, None]]:
  30. """
  31. Return the index of ``scalar`` in the JSON array under ``path`` at key
  32. ``name``.
  33. The search can be limited using the optional inclusive ``start``
  34. and exclusive ``stop`` indices.
  35. For more information see `JSON.ARRINDEX <https://redis.io/commands/json.arrindex>`_.
  36. """ # noqa
  37. pieces = [name, str(path), self._encode(scalar)]
  38. if start is not None:
  39. pieces.append(start)
  40. if stop is not None:
  41. pieces.append(stop)
  42. return self.execute_command("JSON.ARRINDEX", *pieces)
  43. def arrinsert(
  44. self, name: str, path: str, index: int, *args: List[JsonType]
  45. ) -> List[Union[int, None]]:
  46. """Insert the objects ``args`` to the array at index ``index``
  47. under the ``path` in key ``name``.
  48. For more information see `JSON.ARRINSERT <https://redis.io/commands/json.arrinsert>`_.
  49. """ # noqa
  50. pieces = [name, str(path), index]
  51. for o in args:
  52. pieces.append(self._encode(o))
  53. return self.execute_command("JSON.ARRINSERT", *pieces)
  54. def arrlen(
  55. self, name: str, path: Optional[str] = Path.root_path()
  56. ) -> List[Union[int, None]]:
  57. """Return the length of the array JSON value under ``path``
  58. at key``name``.
  59. For more information see `JSON.ARRLEN <https://redis.io/commands/json.arrlen>`_.
  60. """ # noqa
  61. return self.execute_command("JSON.ARRLEN", name, str(path))
  62. def arrpop(
  63. self,
  64. name: str,
  65. path: Optional[str] = Path.root_path(),
  66. index: Optional[int] = -1,
  67. ) -> List[Union[str, None]]:
  68. """Pop the element at ``index`` in the array JSON value under
  69. ``path`` at key ``name``.
  70. For more information see `JSON.ARRPOP <https://redis.io/commands/json.arrpop>`_.
  71. """ # noqa
  72. return self.execute_command("JSON.ARRPOP", name, str(path), index)
  73. def arrtrim(
  74. self, name: str, path: str, start: int, stop: int
  75. ) -> List[Union[int, None]]:
  76. """Trim the array JSON value under ``path`` at key ``name`` to the
  77. inclusive range given by ``start`` and ``stop``.
  78. For more information see `JSON.ARRTRIM <https://redis.io/commands/json.arrtrim>`_.
  79. """ # noqa
  80. return self.execute_command("JSON.ARRTRIM", name, str(path), start, stop)
  81. def type(self, name: str, path: Optional[str] = Path.root_path()) -> List[str]:
  82. """Get the type of the JSON value under ``path`` from key ``name``.
  83. For more information see `JSON.TYPE <https://redis.io/commands/json.type>`_.
  84. """ # noqa
  85. return self.execute_command("JSON.TYPE", name, str(path))
  86. def resp(self, name: str, path: Optional[str] = Path.root_path()) -> List:
  87. """Return the JSON value under ``path`` at key ``name``.
  88. For more information see `JSON.RESP <https://redis.io/commands/json.resp>`_.
  89. """ # noqa
  90. return self.execute_command("JSON.RESP", name, str(path))
  91. def objkeys(
  92. self, name: str, path: Optional[str] = Path.root_path()
  93. ) -> List[Union[List[str], None]]:
  94. """Return the key names in the dictionary JSON value under ``path`` at
  95. key ``name``.
  96. For more information see `JSON.OBJKEYS <https://redis.io/commands/json.objkeys>`_.
  97. """ # noqa
  98. return self.execute_command("JSON.OBJKEYS", name, str(path))
  99. def objlen(
  100. self, name: str, path: Optional[str] = Path.root_path()
  101. ) -> List[Optional[int]]:
  102. """Return the length of the dictionary JSON value under ``path`` at key
  103. ``name``.
  104. For more information see `JSON.OBJLEN <https://redis.io/commands/json.objlen>`_.
  105. """ # noqa
  106. return self.execute_command("JSON.OBJLEN", name, str(path))
  107. def numincrby(self, name: str, path: str, number: int) -> str:
  108. """Increment the numeric (integer or floating point) JSON value under
  109. ``path`` at key ``name`` by the provided ``number``.
  110. For more information see `JSON.NUMINCRBY <https://redis.io/commands/json.numincrby>`_.
  111. """ # noqa
  112. return self.execute_command(
  113. "JSON.NUMINCRBY", name, str(path), self._encode(number)
  114. )
  115. @deprecated_function(version="4.0.0", reason="deprecated since redisjson 1.0.0")
  116. def nummultby(self, name: str, path: str, number: int) -> str:
  117. """Multiply the numeric (integer or floating point) JSON value under
  118. ``path`` at key ``name`` with the provided ``number``.
  119. For more information see `JSON.NUMMULTBY <https://redis.io/commands/json.nummultby>`_.
  120. """ # noqa
  121. return self.execute_command(
  122. "JSON.NUMMULTBY", name, str(path), self._encode(number)
  123. )
  124. def clear(self, name: str, path: Optional[str] = Path.root_path()) -> int:
  125. """Empty arrays and objects (to have zero slots/keys without deleting the
  126. array/object).
  127. Return the count of cleared paths (ignoring non-array and non-objects
  128. paths).
  129. For more information see `JSON.CLEAR <https://redis.io/commands/json.clear>`_.
  130. """ # noqa
  131. return self.execute_command("JSON.CLEAR", name, str(path))
  132. def delete(self, key: str, path: Optional[str] = Path.root_path()) -> int:
  133. """Delete the JSON value stored at key ``key`` under ``path``.
  134. For more information see `JSON.DEL <https://redis.io/commands/json.del>`_.
  135. """
  136. return self.execute_command("JSON.DEL", key, str(path))
  137. # forget is an alias for delete
  138. forget = delete
  139. def get(
  140. self, name: str, *args, no_escape: Optional[bool] = False
  141. ) -> Optional[List[JsonType]]:
  142. """
  143. Get the object stored as a JSON value at key ``name``.
  144. ``args`` is zero or more paths, and defaults to root path
  145. ```no_escape`` is a boolean flag to add no_escape option to get
  146. non-ascii characters
  147. For more information see `JSON.GET <https://redis.io/commands/json.get>`_.
  148. """ # noqa
  149. pieces = [name]
  150. if no_escape:
  151. pieces.append("noescape")
  152. if len(args) == 0:
  153. pieces.append(Path.root_path())
  154. else:
  155. for p in args:
  156. pieces.append(str(p))
  157. # Handle case where key doesn't exist. The JSONDecoder would raise a
  158. # TypeError exception since it can't decode None
  159. try:
  160. return self.execute_command("JSON.GET", *pieces)
  161. except TypeError:
  162. return None
  163. def mget(self, keys: List[str], path: str) -> List[JsonType]:
  164. """
  165. Get the objects stored as a JSON values under ``path``. ``keys``
  166. is a list of one or more keys.
  167. For more information see `JSON.MGET <https://redis.io/commands/json.mget>`_.
  168. """ # noqa
  169. pieces = []
  170. pieces += keys
  171. pieces.append(str(path))
  172. return self.execute_command("JSON.MGET", *pieces)
  173. def set(
  174. self,
  175. name: str,
  176. path: str,
  177. obj: JsonType,
  178. nx: Optional[bool] = False,
  179. xx: Optional[bool] = False,
  180. decode_keys: Optional[bool] = False,
  181. ) -> Optional[str]:
  182. """
  183. Set the JSON value at key ``name`` under the ``path`` to ``obj``.
  184. ``nx`` if set to True, set ``value`` only if it does not exist.
  185. ``xx`` if set to True, set ``value`` only if it exists.
  186. ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
  187. with utf-8.
  188. For the purpose of using this within a pipeline, this command is also
  189. aliased to JSON.SET.
  190. For more information see `JSON.SET <https://redis.io/commands/json.set>`_.
  191. """
  192. if decode_keys:
  193. obj = decode_dict_keys(obj)
  194. pieces = [name, str(path), self._encode(obj)]
  195. # Handle existential modifiers
  196. if nx and xx:
  197. raise Exception(
  198. "nx and xx are mutually exclusive: use one, the "
  199. "other or neither - but not both"
  200. )
  201. elif nx:
  202. pieces.append("NX")
  203. elif xx:
  204. pieces.append("XX")
  205. return self.execute_command("JSON.SET", *pieces)
  206. def mset(self, triplets: List[Tuple[str, str, JsonType]]) -> Optional[str]:
  207. """
  208. Set the JSON value at key ``name`` under the ``path`` to ``obj``
  209. for one or more keys.
  210. ``triplets`` is a list of one or more triplets of key, path, value.
  211. For the purpose of using this within a pipeline, this command is also
  212. aliased to JSON.MSET.
  213. For more information see `JSON.MSET <https://redis.io/commands/json.mset>`_.
  214. """
  215. pieces = []
  216. for triplet in triplets:
  217. pieces.extend([triplet[0], str(triplet[1]), self._encode(triplet[2])])
  218. return self.execute_command("JSON.MSET", *pieces)
  219. def merge(
  220. self,
  221. name: str,
  222. path: str,
  223. obj: JsonType,
  224. decode_keys: Optional[bool] = False,
  225. ) -> Optional[str]:
  226. """
  227. Merges a given JSON value into matching paths. Consequently, JSON values
  228. at matching paths are updated, deleted, or expanded with new children
  229. ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
  230. with utf-8.
  231. For more information see `JSON.MERGE <https://redis.io/commands/json.merge>`_.
  232. """
  233. if decode_keys:
  234. obj = decode_dict_keys(obj)
  235. pieces = [name, str(path), self._encode(obj)]
  236. return self.execute_command("JSON.MERGE", *pieces)
  237. def set_file(
  238. self,
  239. name: str,
  240. path: str,
  241. file_name: str,
  242. nx: Optional[bool] = False,
  243. xx: Optional[bool] = False,
  244. decode_keys: Optional[bool] = False,
  245. ) -> Optional[str]:
  246. """
  247. Set the JSON value at key ``name`` under the ``path`` to the content
  248. of the json file ``file_name``.
  249. ``nx`` if set to True, set ``value`` only if it does not exist.
  250. ``xx`` if set to True, set ``value`` only if it exists.
  251. ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
  252. with utf-8.
  253. """
  254. with open(file_name, "r") as fp:
  255. file_content = loads(fp.read())
  256. return self.set(name, path, file_content, nx=nx, xx=xx, decode_keys=decode_keys)
  257. def set_path(
  258. self,
  259. json_path: str,
  260. root_folder: str,
  261. nx: Optional[bool] = False,
  262. xx: Optional[bool] = False,
  263. decode_keys: Optional[bool] = False,
  264. ) -> Dict[str, bool]:
  265. """
  266. Iterate over ``root_folder`` and set each JSON file to a value
  267. under ``json_path`` with the file name as the key.
  268. ``nx`` if set to True, set ``value`` only if it does not exist.
  269. ``xx`` if set to True, set ``value`` only if it exists.
  270. ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
  271. with utf-8.
  272. """
  273. set_files_result = {}
  274. for root, dirs, files in os.walk(root_folder):
  275. for file in files:
  276. file_path = os.path.join(root, file)
  277. try:
  278. file_name = file_path.rsplit(".")[0]
  279. self.set_file(
  280. file_name,
  281. json_path,
  282. file_path,
  283. nx=nx,
  284. xx=xx,
  285. decode_keys=decode_keys,
  286. )
  287. set_files_result[file_path] = True
  288. except JSONDecodeError:
  289. set_files_result[file_path] = False
  290. return set_files_result
  291. def strlen(self, name: str, path: Optional[str] = None) -> List[Union[int, None]]:
  292. """Return the length of the string JSON value under ``path`` at key
  293. ``name``.
  294. For more information see `JSON.STRLEN <https://redis.io/commands/json.strlen>`_.
  295. """ # noqa
  296. pieces = [name]
  297. if path is not None:
  298. pieces.append(str(path))
  299. return self.execute_command("JSON.STRLEN", *pieces)
  300. def toggle(
  301. self, name: str, path: Optional[str] = Path.root_path()
  302. ) -> Union[bool, List[Optional[int]]]:
  303. """Toggle boolean value under ``path`` at key ``name``.
  304. returning the new value.
  305. For more information see `JSON.TOGGLE <https://redis.io/commands/json.toggle>`_.
  306. """ # noqa
  307. return self.execute_command("JSON.TOGGLE", name, str(path))
  308. def strappend(
  309. self, name: str, value: str, path: Optional[str] = Path.root_path()
  310. ) -> Union[int, List[Optional[int]]]:
  311. """Append to the string JSON value. If two options are specified after
  312. the key name, the path is determined to be the first. If a single
  313. option is passed, then the root_path (i.e Path.root_path()) is used.
  314. For more information see `JSON.STRAPPEND <https://redis.io/commands/json.strappend>`_.
  315. """ # noqa
  316. pieces = [name, str(path), self._encode(value)]
  317. return self.execute_command("JSON.STRAPPEND", *pieces)
  318. def debug(
  319. self,
  320. subcommand: str,
  321. key: Optional[str] = None,
  322. path: Optional[str] = Path.root_path(),
  323. ) -> Union[int, List[str]]:
  324. """Return the memory usage in bytes of a value under ``path`` from
  325. key ``name``.
  326. For more information see `JSON.DEBUG <https://redis.io/commands/json.debug>`_.
  327. """ # noqa
  328. valid_subcommands = ["MEMORY", "HELP"]
  329. if subcommand not in valid_subcommands:
  330. raise DataError("The only valid subcommands are ", str(valid_subcommands))
  331. pieces = [subcommand]
  332. if subcommand == "MEMORY":
  333. if key is None:
  334. raise DataError("No key specified")
  335. pieces.append(key)
  336. pieces.append(str(path))
  337. return self.execute_command("JSON.DEBUG", *pieces)
  338. @deprecated_function(
  339. version="4.0.0", reason="redisjson-py supported this, call get directly."
  340. )
  341. def jsonget(self, *args, **kwargs):
  342. return self.get(*args, **kwargs)
  343. @deprecated_function(
  344. version="4.0.0", reason="redisjson-py supported this, call get directly."
  345. )
  346. def jsonmget(self, *args, **kwargs):
  347. return self.mget(*args, **kwargs)
  348. @deprecated_function(
  349. version="4.0.0", reason="redisjson-py supported this, call get directly."
  350. )
  351. def jsonset(self, *args, **kwargs):
  352. return self.set(*args, **kwargs)