summaryrefslogtreecommitdiff
path: root/redis/commands/json
diff options
context:
space:
mode:
Diffstat (limited to 'redis/commands/json')
-rw-r--r--redis/commands/json/__init__.py84
-rw-r--r--redis/commands/json/commands.py197
-rw-r--r--redis/commands/json/helpers.py25
-rw-r--r--redis/commands/json/path.py21
4 files changed, 327 insertions, 0 deletions
diff --git a/redis/commands/json/__init__.py b/redis/commands/json/__init__.py
new file mode 100644
index 0000000..92f1199
--- /dev/null
+++ b/redis/commands/json/__init__.py
@@ -0,0 +1,84 @@
+# from typing import Optional
+from json import JSONDecoder, JSONEncoder
+
+# # from redis.client import Redis
+
+from .helpers import bulk_of_jsons
+from ..helpers import nativestr, delist
+from .commands import JSONCommands
+# from ..feature import AbstractFeature
+
+
+class JSON(JSONCommands):
+ """
+ Create a client for talking to json.
+
+ :param decoder:
+ :type json.JSONDecoder: An instance of json.JSONDecoder
+
+ :param encoder:
+ :type json.JSONEncoder: An instance of json.JSONEncoder
+ """
+
+ def __init__(
+ self,
+ client,
+ decoder=JSONDecoder(),
+ encoder=JSONEncoder(),
+ ):
+ """
+ Create a client for talking to json.
+
+ :param decoder:
+ :type json.JSONDecoder: An instance of json.JSONDecoder
+
+ :param encoder:
+ :type json.JSONEncoder: An instance of json.JSONEncoder
+ """
+ # Set the module commands' callbacks
+ self.MODULE_CALLBACKS = {
+ "JSON.CLEAR": int,
+ "JSON.DEL": int,
+ "JSON.FORGET": int,
+ "JSON.GET": self._decode,
+ "JSON.MGET": bulk_of_jsons(self._decode),
+ "JSON.SET": lambda r: r and nativestr(r) == "OK",
+ "JSON.NUMINCRBY": self._decode,
+ "JSON.NUMMULTBY": self._decode,
+ "JSON.TOGGLE": lambda b: b == b"true",
+ "JSON.STRAPPEND": int,
+ "JSON.STRLEN": int,
+ "JSON.ARRAPPEND": int,
+ "JSON.ARRINDEX": int,
+ "JSON.ARRINSERT": int,
+ "JSON.ARRLEN": int,
+ "JSON.ARRPOP": self._decode,
+ "JSON.ARRTRIM": int,
+ "JSON.OBJLEN": int,
+ "JSON.OBJKEYS": delist,
+ # "JSON.RESP": delist,
+ "JSON.DEBUG": int,
+ }
+
+ self.client = client
+ self.execute_command = client.execute_command
+
+ for key, value in self.MODULE_CALLBACKS.items():
+ self.client.set_response_callback(key, value)
+
+ self.__encoder__ = encoder
+ self.__decoder__ = decoder
+
+ def _decode(self, obj):
+ """Get the decoder."""
+ if obj is None:
+ return obj
+
+ try:
+ return self.__decoder__.decode(obj)
+ except TypeError:
+ return self.__decoder__.decode(obj.decode())
+
+ def _encode(self, obj):
+ """Get the encoder."""
+ return self.__encoder__.encode(obj)
diff --git a/redis/commands/json/commands.py b/redis/commands/json/commands.py
new file mode 100644
index 0000000..2f8039f
--- /dev/null
+++ b/redis/commands/json/commands.py
@@ -0,0 +1,197 @@
+from .path import Path, str_path
+from .helpers import decode_dict_keys
+
+
+class JSONCommands:
+ """json commands."""
+
+ def arrappend(self, name, path=Path.rootPath(), *args):
+ """Append the objects ``args`` to the array under the
+ ``path` in key ``name``.
+ """
+ pieces = [name, str_path(path)]
+ for o in args:
+ pieces.append(self._encode(o))
+ return self.execute_command("JSON.ARRAPPEND", *pieces)
+
+ def arrindex(self, name, path, scalar, start=0, stop=-1):
+ """
+ Return the index of ``scalar`` in the JSON array under ``path`` at key
+ ``name``.
+
+ The search can be limited using the optional inclusive ``start``
+ and exclusive ``stop`` indices.
+ """
+ return self.execute_command(
+ "JSON.ARRINDEX", name, str_path(path), self._encode(scalar),
+ start, stop
+ )
+
+ def arrinsert(self, name, path, index, *args):
+ """Insert the objects ``args`` to the array at index ``index``
+ under the ``path` in key ``name``.
+ """
+ pieces = [name, str_path(path), index]
+ for o in args:
+ pieces.append(self._encode(o))
+ return self.execute_command("JSON.ARRINSERT", *pieces)
+
+ def forget(self, name, path=Path.rootPath()):
+ """Alias for jsondel (delete the JSON value)."""
+ return self.execute_command("JSON.FORGET", name, str_path(path))
+
+ def arrlen(self, name, path=Path.rootPath()):
+ """Return the length of the array JSON value under ``path``
+ at key``name``.
+ """
+ return self.execute_command("JSON.ARRLEN", name, str_path(path))
+
+ def arrpop(self, name, path=Path.rootPath(), index=-1):
+ """Pop the element at ``index`` in the array JSON value under
+ ``path`` at key ``name``.
+ """
+ return self.execute_command("JSON.ARRPOP", name, str_path(path), index)
+
+ def arrtrim(self, name, path, start, stop):
+ """Trim the array JSON value under ``path`` at key ``name`` to the
+ inclusive range given by ``start`` and ``stop``.
+ """
+ return self.execute_command("JSON.ARRTRIM", name, str_path(path),
+ start, stop)
+
+ def type(self, name, path=Path.rootPath()):
+ """Get the type of the JSON value under ``path`` from key ``name``."""
+ return self.execute_command("JSON.TYPE", name, str_path(path))
+
+ def resp(self, name, path=Path.rootPath()):
+ """Return the JSON value under ``path`` at key ``name``."""
+ return self.execute_command("JSON.RESP", name, str_path(path))
+
+ def objkeys(self, name, path=Path.rootPath()):
+ """Return the key names in the dictionary JSON value under ``path`` at
+ key ``name``."""
+ return self.execute_command("JSON.OBJKEYS", name, str_path(path))
+
+ def objlen(self, name, path=Path.rootPath()):
+ """Return the length of the dictionary JSON value under ``path`` at key
+ ``name``.
+ """
+ return self.execute_command("JSON.OBJLEN", name, str_path(path))
+
+ def numincrby(self, name, path, number):
+ """Increment the numeric (integer or floating point) JSON value under
+ ``path`` at key ``name`` by the provided ``number``.
+ """
+ return self.execute_command(
+ "JSON.NUMINCRBY", name, str_path(path), self._encode(number)
+ )
+
+ def nummultby(self, name, path, number):
+ """Multiply the numeric (integer or floating point) JSON value under
+ ``path`` at key ``name`` with the provided ``number``.
+ """
+ return self.execute_command(
+ "JSON.NUMMULTBY", name, str_path(path), self._encode(number)
+ )
+
+ def clear(self, name, path=Path.rootPath()):
+ """
+ Empty arrays and objects (to have zero slots/keys without deleting the
+ array/object).
+
+ Return the count of cleared paths (ignoring non-array and non-objects
+ paths).
+ """
+ return self.execute_command("JSON.CLEAR", name, str_path(path))
+
+ def delete(self, name, path=Path.rootPath()):
+ """Delete the JSON value stored at key ``name`` under ``path``."""
+ return self.execute_command("JSON.DEL", name, str_path(path))
+
+ def get(self, name, *args, no_escape=False):
+ """
+ Get the object stored as a JSON value at key ``name``.
+
+ ``args`` is zero or more paths, and defaults to root path
+ ```no_escape`` is a boolean flag to add no_escape option to get
+ non-ascii characters
+ """
+ pieces = [name]
+ if no_escape:
+ pieces.append("noescape")
+
+ if len(args) == 0:
+ pieces.append(Path.rootPath())
+
+ else:
+ for p in args:
+ pieces.append(str_path(p))
+
+ # Handle case where key doesn't exist. The JSONDecoder would raise a
+ # TypeError exception since it can't decode None
+ try:
+ return self.execute_command("JSON.GET", *pieces)
+ except TypeError:
+ return None
+
+ def mget(self, path, *args):
+ """Get the objects stored as a JSON values under ``path`` from keys
+ ``args``.
+ """
+ pieces = []
+ pieces.extend(args)
+ pieces.append(str_path(path))
+ return self.execute_command("JSON.MGET", *pieces)
+
+ def set(self, name, path, obj, nx=False, xx=False, decode_keys=False):
+ """
+ Set the JSON value at key ``name`` under the ``path`` to ``obj``.
+
+ ``nx`` if set to True, set ``value`` only if it does not exist.
+ ``xx`` if set to True, set ``value`` only if it exists.
+ ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
+ with utf-8.
+ """
+ if decode_keys:
+ obj = decode_dict_keys(obj)
+
+ pieces = [name, str_path(path), self._encode(obj)]
+
+ # Handle existential modifiers
+ if nx and xx:
+ raise Exception(
+ "nx and xx are mutually exclusive: use one, the "
+ "other or neither - but not both"
+ )
+ elif nx:
+ pieces.append("NX")
+ elif xx:
+ pieces.append("XX")
+ return self.execute_command("JSON.SET", *pieces)
+
+ def strlen(self, name, path=Path.rootPath()):
+ """Return the length of the string JSON value under ``path`` at key
+ ``name``.
+ """
+ return self.execute_command("JSON.STRLEN", name, str_path(path))
+
+ def toggle(self, name, path=Path.rootPath()):
+ """Toggle boolean value under ``path`` at key ``name``.
+ returning the new value.
+ """
+ return self.execute_command("JSON.TOGGLE", name, str_path(path))
+
+ def strappend(self, name, string, path=Path.rootPath()):
+ """Append to the string JSON value under ``path`` at key ``name``
+ the provided ``string``.
+ """
+ return self.execute_command(
+ "JSON.STRAPPEND", name, str_path(path), self._encode(string)
+ )
+
+ def debug(self, name, path=Path.rootPath()):
+ """Return the memory usage in bytes of a value under ``path`` from
+ key ``name``.
+ """
+ return self.execute_command("JSON.DEBUG", "MEMORY",
+ name, str_path(path))
diff --git a/redis/commands/json/helpers.py b/redis/commands/json/helpers.py
new file mode 100644
index 0000000..8fb20d9
--- /dev/null
+++ b/redis/commands/json/helpers.py
@@ -0,0 +1,25 @@
+import copy
+
+
+def bulk_of_jsons(d):
+ """Replace serialized JSON values with objects in a
+ bulk array response (list).
+ """
+
+ def _f(b):
+ for index, item in enumerate(b):
+ if item is not None:
+ b[index] = d(item)
+ return b
+
+ return _f
+
+
+def decode_dict_keys(obj):
+ """Decode the keys of the given dictionary with utf-8."""
+ newobj = copy.copy(obj)
+ for k in obj.keys():
+ if isinstance(k, bytes):
+ newobj[k.decode("utf-8")] = newobj[k]
+ newobj.pop(k)
+ return newobj
diff --git a/redis/commands/json/path.py b/redis/commands/json/path.py
new file mode 100644
index 0000000..dff8648
--- /dev/null
+++ b/redis/commands/json/path.py
@@ -0,0 +1,21 @@
+def str_path(p):
+ """Return the string representation of a path if it is of class Path."""
+ if isinstance(p, Path):
+ return p.strPath
+ else:
+ return p
+
+
+class Path(object):
+ """This class represents a path in a JSON value."""
+
+ strPath = ""
+
+ @staticmethod
+ def rootPath():
+ """Return the root path's string representation."""
+ return "."
+
+ def __init__(self, path):
+ """Make a new path based on the string representation in `path`."""
+ self.strPath = path