summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChayim <chayim@users.noreply.github.com>2021-11-04 13:20:31 +0200
committerGitHub <noreply@github.com>2021-11-04 13:20:31 +0200
commit8d3c61598706eb049caa66a23501018f2f416673 (patch)
tree0aa3430a7ad3b1a9212194043a9e08c187ecd9fe
parent72b49263f86f32c9df945433f21d3f3a7444d1a0 (diff)
downloadredis-py-8d3c61598706eb049caa66a23501018f2f416673.tar.gz
Support for json multipath ($) (#1663)
-rw-r--r--redis/commands/helpers.py5
-rw-r--r--redis/commands/json/__init__.py45
-rw-r--r--redis/commands/json/commands.py4
-rw-r--r--redis/commands/json/decoders.py65
-rw-r--r--redis/commands/json/helpers.py25
-rw-r--r--tests/conftest.py3
-rw-r--r--tests/test_json.py1133
-rw-r--r--tests/testdata/jsontestdata.py617
8 files changed, 1823 insertions, 74 deletions
diff --git a/redis/commands/helpers.py b/redis/commands/helpers.py
index 48ee556..2a4298c 100644
--- a/redis/commands/helpers.py
+++ b/redis/commands/helpers.py
@@ -17,7 +17,10 @@ def list_or_args(keys, args):
def nativestr(x):
"""Return the decoded binary string, or a string, depending on type."""
- return x.decode("utf-8", "replace") if isinstance(x, bytes) else x
+ r = x.decode("utf-8", "replace") if isinstance(x, bytes) else x
+ if r == 'null':
+ return
+ return r
def delist(x):
diff --git a/redis/commands/json/__init__.py b/redis/commands/json/__init__.py
index 3149bb8..d00627e 100644
--- a/redis/commands/json/__init__.py
+++ b/redis/commands/json/__init__.py
@@ -1,11 +1,10 @@
-from json import JSONDecoder, JSONEncoder
+from json import JSONDecoder, JSONEncoder, JSONDecodeError
from .decoders import (
- int_or_list,
- int_or_none
+ decode_list,
+ bulk_of_jsons,
)
-from .helpers import bulk_of_jsons
-from ..helpers import nativestr, delist
+from ..helpers import nativestr
from .commands import JSONCommands
@@ -46,19 +45,19 @@ class JSON(JSONCommands):
"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_or_none,
+ "JSON.TOGGLE": self._decode,
+ "JSON.STRAPPEND": self._decode,
+ "JSON.STRLEN": self._decode,
+ "JSON.ARRAPPEND": self._decode,
+ "JSON.ARRINDEX": self._decode,
+ "JSON.ARRINSERT": self._decode,
+ "JSON.ARRLEN": self._decode,
"JSON.ARRPOP": self._decode,
- "JSON.ARRTRIM": int,
- "JSON.OBJLEN": int,
- "JSON.OBJKEYS": delist,
- # "JSON.RESP": delist,
- "JSON.DEBUG": int_or_list,
+ "JSON.ARRTRIM": self._decode,
+ "JSON.OBJLEN": self._decode,
+ "JSON.OBJKEYS": self._decode,
+ "JSON.RESP": self._decode,
+ "JSON.DEBUG": self._decode,
}
self.client = client
@@ -77,9 +76,17 @@ class JSON(JSONCommands):
return obj
try:
- return self.__decoder__.decode(obj)
+ x = self.__decoder__.decode(obj)
+ if x is None:
+ raise TypeError
+ return x
except TypeError:
- return self.__decoder__.decode(obj.decode())
+ try:
+ return self.__decoder__.decode(obj.decode())
+ except AttributeError:
+ return decode_list(obj)
+ except (AttributeError, JSONDecodeError):
+ return decode_list(obj)
def _encode(self, obj):
"""Get the encoder."""
diff --git a/redis/commands/json/commands.py b/redis/commands/json/commands.py
index fb00e22..716741c 100644
--- a/redis/commands/json/commands.py
+++ b/redis/commands/json/commands.py
@@ -1,5 +1,5 @@
from .path import Path
-from .helpers import decode_dict_keys
+from .decoders import decode_dict_keys
from deprecated import deprecated
from redis.exceptions import DataError
@@ -192,7 +192,7 @@ class JSONCommands:
the key name, the path is determined to be the first. If a single
option is passed, then the rootpath (i.e Path.rootPath()) is used.
"""
- pieces = [name, str(path), value]
+ pieces = [name, str(path), self._encode(value)]
return self.execute_command(
"JSON.STRAPPEND", *pieces
)
diff --git a/redis/commands/json/decoders.py b/redis/commands/json/decoders.py
index 0ee102a..b19395c 100644
--- a/redis/commands/json/decoders.py
+++ b/redis/commands/json/decoders.py
@@ -1,12 +1,59 @@
-def int_or_list(b):
- if isinstance(b, int):
- return b
- else:
- return b
+from ..helpers import nativestr
+import re
+import copy
+
+def bulk_of_jsons(d):
+ """Replace serialized JSON values with objects in a
+ bulk array response (list).
+ """
-def int_or_none(b):
- if b is None:
- return None
- if isinstance(b, int):
+ 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
+
+
+def unstring(obj):
+ """
+ Attempt to parse string to native integer formats.
+ One can't simply call int/float in a try/catch because there is a
+ semantic difference between (for example) 15.0 and 15.
+ """
+ floatreg = '^\\d+.\\d+$'
+ match = re.findall(floatreg, obj)
+ if match != []:
+ return float(match[0])
+
+ intreg = "^\\d+$"
+ match = re.findall(intreg, obj)
+ if match != []:
+ return int(match[0])
+ return obj
+
+
+def decode_list(b):
+ """
+ Given a non-deserializable object, make a best effort to
+ return a useful set of results.
+ """
+ if isinstance(b, list):
+ return [nativestr(obj) for obj in b]
+ elif isinstance(b, bytes):
+ return unstring(nativestr(b))
+ elif isinstance(b, str):
+ return unstring(b)
+ return b
diff --git a/redis/commands/json/helpers.py b/redis/commands/json/helpers.py
deleted file mode 100644
index 8fb20d9..0000000
--- a/redis/commands/json/helpers.py
+++ /dev/null
@@ -1,25 +0,0 @@
-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/tests/conftest.py b/tests/conftest.py
index 47188df..b1a0f8c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -126,7 +126,8 @@ def _get_client(cls, request, single_connection_client=True, flushdb=True,
@pytest.fixture()
def modclient(request, **kwargs):
rmurl = request.config.getoption('--redismod-url')
- with _get_client(redis.Redis, request, from_url=rmurl, **kwargs) as client:
+ with _get_client(redis.Redis, request, from_url=rmurl,
+ decode_responses=True, **kwargs) as client:
yield client
diff --git a/tests/test_json.py b/tests/test_json.py
index f62346f..19b0c32 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -1,6 +1,8 @@
import pytest
import redis
from redis.commands.json.path import Path
+from redis import exceptions
+from redis.commands.json.decoders import unstring, decode_list
from .conftest import skip_ifmodversion_lt
@@ -29,7 +31,7 @@ def test_json_setgetdeleteforget(client):
@pytest.mark.redismod
-def test_justaget(client):
+def test_jsonget(client):
client.json().set("foo", Path.rootPath(), "bar")
assert client.json().get("foo") == "bar"
@@ -45,9 +47,10 @@ def test_json_get_jset(client):
@pytest.mark.redismod
def test_nonascii_setgetdelete(client):
- assert client.json().set("notascii", Path.rootPath(),
- "hyvää-élève") is True
- assert "hyvää-élève" == client.json().get("notascii", no_escape=True)
+ assert client.json().set("notascii", Path.rootPath(), "hyvää-élève")
+ assert "hyvää-élève" == client.json().get(
+ "notascii",
+ no_escape=True)
assert 1 == client.json().delete("notascii")
assert client.exists("notascii") == 0
@@ -90,8 +93,8 @@ def test_clear(client):
@pytest.mark.redismod
def test_type(client):
client.json().set("1", Path.rootPath(), 1)
- assert b"integer" == client.json().type("1", Path.rootPath())
- assert b"integer" == client.json().type("1")
+ assert "integer" == client.json().type("1", Path.rootPath())
+ assert "integer" == client.json().type("1")
@pytest.mark.redismod
@@ -117,7 +120,7 @@ def test_nummultby(client):
def test_toggle(client):
client.json().set("bool", Path.rootPath(), False)
assert client.json().toggle("bool", Path.rootPath())
- assert not client.json().toggle("bool", Path.rootPath())
+ assert client.json().toggle("bool", Path.rootPath()) is False
# check non-boolean value
client.json().set("num", Path.rootPath(), 1)
with pytest.raises(redis.exceptions.ResponseError):
@@ -126,11 +129,8 @@ def test_toggle(client):
@pytest.mark.redismod
def test_strappend(client):
- client.json().set("jsonkey", Path.rootPath(), 'foo')
- import json
- assert 6 == client.json().strappend("jsonkey", json.dumps('bar'))
- with pytest.raises(redis.exceptions.ResponseError):
- assert 6 == client.json().strappend("jsonkey", 'bar')
+ client.json().set("jsonkey", Path.rootPath(), "foo")
+ assert 6 == client.json().strappend("jsonkey", "bar")
assert "foobar" == client.json().get("jsonkey", Path.rootPath())
@@ -148,8 +148,7 @@ def test_debug(client):
def test_strlen(client):
client.json().set("str", Path.rootPath(), "foo")
assert 3 == client.json().strlen("str", Path.rootPath())
- import json
- client.json().strappend("str", json.dumps("bar"), Path.rootPath())
+ client.json().strappend("str", "bar", Path.rootPath())
assert 6 == client.json().strlen("str", Path.rootPath())
assert 6 == client.json().strlen("str")
@@ -186,7 +185,7 @@ def test_arrinsert(client):
# test prepends
client.json().set("val2", Path.rootPath(), [5, 6, 7, 8, 9])
- client.json().arrinsert("val2", Path.rootPath(), 0, ['some', 'thing'])
+ client.json().arrinsert("val2", Path.rootPath(), 0, ["some", "thing"])
assert client.json().get("val2") == [["some", "thing"], 5, 6, 7, 8, 9]
@@ -195,7 +194,7 @@ def test_arrlen(client):
client.json().set("arr", Path.rootPath(), [0, 1, 2, 3, 4])
assert 5 == client.json().arrlen("arr", Path.rootPath())
assert 5 == client.json().arrlen("arr")
- assert client.json().arrlen('fakekey') is None
+ assert client.json().arrlen("fakekey") is None
@pytest.mark.redismod
@@ -243,7 +242,7 @@ def test_arrtrim(client):
def test_resp(client):
obj = {"foo": "bar", "baz": 1, "qaz": True}
client.json().set("obj", Path.rootPath(), obj)
- assert b"bar" == client.json().resp("obj", Path("foo"))
+ assert "bar" == client.json().resp("obj", Path("foo"))
assert 1 == client.json().resp("obj", Path("baz"))
assert client.json().resp("obj", Path("qaz"))
assert isinstance(client.json().resp("obj"), list)
@@ -286,3 +285,1103 @@ def test_objlen(client):
# assert [True, "bar", 1] == p.execute()
# assert client.keys() == []
# assert client.get("foo") is None
+
+
+@pytest.mark.redismod
+def test_json_delete_with_dollar(client):
+ doc1 = {"a": 1, "nested": {"a": 2, "b": 3}}
+ assert client.json().set("doc1", "$", doc1)
+ assert client.json().delete("doc1", "$..a") == 2
+ r = client.json().get("doc1", "$")
+ assert r == [{"nested": {"b": 3}}]
+
+ doc2 = {"a": {"a": 2, "b": 3}, "b": [
+ "a", "b"], "nested": {"b": [True, "a", "b"]}}
+ assert client.json().set("doc2", "$", doc2)
+ assert client.json().delete("doc2", "$..a") == 1
+ res = client.json().get("doc2", "$")
+ assert res == [{"nested": {"b": [True, "a", "b"]}, "b": ["a", "b"]}]
+
+ doc3 = [
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {"ciao": [1, "a"]},
+ {"ciao": [2, "a"]},
+ {"ciaoc": [3, "non", "ciao"]},
+ {"ciao": [4, "a"]},
+ {"e": [5, "non", "ciao"]},
+ ],
+ }
+ ]
+ assert client.json().set("doc3", "$", doc3)
+ assert client.json().delete("doc3", '$.[0]["nested"]..ciao') == 3
+
+ doc3val = [
+ [
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {},
+ {},
+ {"ciaoc": [3, "non", "ciao"]},
+ {},
+ {"e": [5, "non", "ciao"]},
+ ],
+ }
+ ]
+ ]
+ res = client.json().get("doc3", "$")
+ assert res == doc3val
+
+ # Test default path
+ assert client.json().delete("doc3") == 1
+ assert client.json().get("doc3", "$") is None
+
+ client.json().delete("not_a_document", "..a")
+
+
+@pytest.mark.redismod
+def test_json_forget_with_dollar(client):
+ doc1 = {"a": 1, "nested": {"a": 2, "b": 3}}
+ assert client.json().set("doc1", "$", doc1)
+ assert client.json().forget("doc1", "$..a") == 2
+ r = client.json().get("doc1", "$")
+ assert r == [{"nested": {"b": 3}}]
+
+ doc2 = {"a": {"a": 2, "b": 3}, "b": [
+ "a", "b"], "nested": {"b": [True, "a", "b"]}}
+ assert client.json().set("doc2", "$", doc2)
+ assert client.json().forget("doc2", "$..a") == 1
+ res = client.json().get("doc2", "$")
+ assert res == [{"nested": {"b": [True, "a", "b"]}, "b": ["a", "b"]}]
+
+ doc3 = [
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {"ciao": [1, "a"]},
+ {"ciao": [2, "a"]},
+ {"ciaoc": [3, "non", "ciao"]},
+ {"ciao": [4, "a"]},
+ {"e": [5, "non", "ciao"]},
+ ],
+ }
+ ]
+ assert client.json().set("doc3", "$", doc3)
+ assert client.json().forget("doc3", '$.[0]["nested"]..ciao') == 3
+
+ doc3val = [
+ [
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {},
+ {},
+ {"ciaoc": [3, "non", "ciao"]},
+ {},
+ {"e": [5, "non", "ciao"]},
+ ],
+ }
+ ]
+ ]
+ res = client.json().get("doc3", "$")
+ assert res == doc3val
+
+ # Test default path
+ assert client.json().forget("doc3") == 1
+ assert client.json().get("doc3", "$") is None
+
+ client.json().forget("not_a_document", "..a")
+
+
+@pytest.mark.redismod
+def test_json_mget_dollar(client):
+ # Test mget with multi paths
+ client.json().set(
+ "doc1",
+ "$",
+ {"a": 1,
+ "b": 2,
+ "nested": {"a": 3},
+ "c": None, "nested2": {"a": None}},
+ )
+ client.json().set(
+ "doc2",
+ "$",
+ {"a": 4, "b": 5, "nested": {"a": 6},
+ "c": None, "nested2": {"a": [None]}},
+ )
+ # Compare also to single JSON.GET
+ assert client.json().get("doc1", "$..a") == [1, 3, None]
+ assert client.json().get("doc2", "$..a") == [4, 6, [None]]
+
+ # Test mget with single path
+ client.json().mget("doc1", "$..a") == [1, 3, None]
+ # Test mget with multi path
+ client.json().mget(["doc1", "doc2"], "$..a") == [
+ [1, 3, None], [4, 6, [None]]]
+
+ # Test missing key
+ client.json().mget(["doc1", "missing_doc"], "$..a") == [[1, 3, None], None]
+ res = client.json().mget(["missing_doc1", "missing_doc2"], "$..a")
+ assert res == [None, None]
+
+
+@pytest.mark.redismod
+def test_numby_commands_dollar(client):
+
+ # Test NUMINCRBY
+ client.json().set(
+ "doc1",
+ "$", {"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]})
+ # Test multi
+ assert client.json().numincrby("doc1", "$..a", 2) == \
+ [None, 4, 7.0, None]
+
+ assert client.json().numincrby("doc1", "$..a", 2.5) == \
+ [None, 6.5, 9.5, None]
+ # Test single
+ assert client.json().numincrby("doc1", "$.b[1].a", 2) == [11.5]
+
+ assert client.json().numincrby("doc1", "$.b[2].a", 2) == [None]
+ assert client.json().numincrby("doc1", "$.b[1].a", 3.5) == [15.0]
+
+ # Test NUMMULTBY
+ client.json().set("doc1", "$", {"a": "b", "b": [
+ {"a": 2}, {"a": 5.0}, {"a": "c"}]})
+
+ assert client.json().nummultby("doc1", "$..a", 2) == \
+ [None, 4, 10, None]
+ assert client.json().nummultby("doc1", "$..a", 2.5) == \
+ [None, 10.0, 25.0, None]
+ # Test single
+ assert client.json().nummultby("doc1", "$.b[1].a", 2) == [50.0]
+ assert client.json().nummultby("doc1", "$.b[2].a", 2) == [None]
+ assert client.json().nummultby("doc1", "$.b[1].a", 3) == [150.0]
+
+ # test missing keys
+ with pytest.raises(exceptions.ResponseError):
+ client.json().numincrby("non_existing_doc", "$..a", 2)
+ client.json().nummultby("non_existing_doc", "$..a", 2)
+
+ # Test legacy NUMINCRBY
+ client.json().set("doc1", "$", {"a": "b", "b": [
+ {"a": 2}, {"a": 5.0}, {"a": "c"}]})
+ client.json().numincrby("doc1", ".b[0].a", 3) == 5
+
+ # Test legacy NUMMULTBY
+ client.json().set("doc1", "$", {"a": "b", "b": [
+ {"a": 2}, {"a": 5.0}, {"a": "c"}]})
+ client.json().nummultby("doc1", ".b[0].a", 3) == 6
+
+
+@pytest.mark.redismod
+def test_strappend_dollar(client):
+
+ client.json().set(
+ "doc1", "$", {"a": "foo", "nested1": {
+ "a": "hello"}, "nested2": {"a": 31}}
+ )
+ # Test multi
+ client.json().strappend("doc1", "bar", "$..a") == [6, 8, None]
+
+ client.json().get("doc1", "$") == [
+ {"a": "foobar", "nested1": {"a": "hellobar"}, "nested2": {"a": 31}}
+ ]
+ # Test single
+ client.json().strappend("doc1", "baz", "$.nested1.a") == [11]
+
+ client.json().get("doc1", "$") == [
+ {"a": "foobar", "nested1": {"a": "hellobarbaz"}, "nested2": {"a": 31}}
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().strappend("non_existing_doc", "$..a", "err")
+
+ # Test multi
+ client.json().strappend("doc1", "bar", ".*.a") == 8
+ client.json().get("doc1", "$") == [
+ {"a": "foo", "nested1": {"a": "hellobar"}, "nested2": {"a": 31}}
+ ]
+
+ # Test missing path
+ with pytest.raises(exceptions.ResponseError):
+ client.json().strappend("doc1", "piu")
+
+
+@pytest.mark.redismod
+def test_strlen_dollar(client):
+
+ # Test multi
+ client.json().set(
+ "doc1", "$", {"a": "foo", "nested1": {
+ "a": "hello"}, "nested2": {"a": 31}}
+ )
+ assert client.json().strlen("doc1", "$..a") == [3, 5, None]
+
+ res2 = client.json().strappend("doc1", "bar", "$..a")
+ res1 = client.json().strlen("doc1", "$..a")
+ assert res1 == res2
+
+ # Test single
+ client.json().strlen("doc1", "$.nested1.a") == [8]
+ client.json().strlen("doc1", "$.nested2.a") == [None]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().strlen("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+def test_arrappend_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi
+ client.json().arrappend("doc1", "$..a", "bar", "racuda") == [3, 5, None]
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", None, "world", "bar", "racuda"]},
+ "nested2": {"a": 31},
+ }
+ ]
+
+ # Test single
+ assert client.json().arrappend("doc1", "$.nested1.a", "baz") == [6]
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", None, "world", "bar", "racuda", "baz"]},
+ "nested2": {"a": 31},
+ }
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrappend("non_existing_doc", "$..a")
+
+ # Test legacy
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi (all paths are updated, but return result of last path)
+ assert client.json().arrappend("doc1", "..a", "bar", "racuda") == 5
+
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", None, "world", "bar", "racuda"]},
+ "nested2": {"a": 31},
+ }
+ ]
+ # Test single
+ assert client.json().arrappend("doc1", ".nested1.a", "baz") == 6
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", None, "world", "bar", "racuda", "baz"]},
+ "nested2": {"a": 31},
+ }
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrappend("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+def test_arrinsert_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi
+ assert client.json().arrinsert("doc1", "$..a", "1",
+ "bar", "racuda") == [3, 5, None]
+
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", "bar", "racuda", None, "world"]},
+ "nested2": {"a": 31},
+ }
+ ]
+ # Test single
+ assert client.json().arrinsert("doc1", "$.nested1.a", -2, "baz") == [6]
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo", "bar", "racuda"],
+ "nested1": {"a": ["hello", "bar", "racuda", "baz", None, "world"]},
+ "nested2": {"a": 31},
+ }
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrappend("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+def test_arrlen_dollar(client):
+
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+
+ # Test multi
+ assert client.json().arrlen("doc1", "$..a") == [1, 3, None]
+ assert client.json().arrappend("doc1", "$..a", "non", "abba", "stanza") \
+ == [4, 6, None]
+
+ client.json().clear("doc1", "$.a")
+ assert client.json().arrlen("doc1", "$..a") == [0, 6, None]
+ # Test single
+ assert client.json().arrlen("doc1", "$.nested1.a") == [6]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrappend("non_existing_doc", "$..a")
+
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi (return result of last path)
+ assert client.json().arrlen("doc1", "$..a") == [1, 3, None]
+ assert client.json().arrappend("doc1", "..a", "non", "abba", "stanza") == 6
+
+ # Test single
+ assert client.json().arrlen("doc1", ".nested1.a") == 6
+
+ # Test missing key
+ assert client.json().arrlen("non_existing_doc", "..a") is None
+
+
+@pytest.mark.redismod
+def test_arrpop_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+
+ # # # Test multi
+ assert client.json().arrpop("doc1", "$..a", 1) == ['"foo"', None, None]
+
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": ["hello", "world"]}, "nested2": {"a": 31}}
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrpop("non_existing_doc", "..a")
+
+ # # Test legacy
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi (all paths are updated, but return result of last path)
+ client.json().arrpop("doc1", "..a", "1") is None
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": ["hello", "world"]}, "nested2": {"a": 31}}
+ ]
+
+ # # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrpop("non_existing_doc", "..a")
+
+
+@pytest.mark.redismod
+def test_arrtrim_dollar(client):
+
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+ # Test multi
+ assert client.json().arrtrim("doc1", "$..a", "1", -1) == [0, 2, None]
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": [None, "world"]}, "nested2": {"a": 31}}
+ ]
+
+ assert client.json().arrtrim("doc1", "$..a", "1", "1") == [0, 1, None]
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": ["world"]}, "nested2": {"a": 31}}
+ ]
+ # Test single
+ assert client.json().arrtrim("doc1", "$.nested1.a", 1, 0) == [0]
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": []}, "nested2": {"a": 31}}
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrtrim("non_existing_doc", "..a", "0", 1)
+
+ # Test legacy
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": ["hello", None, "world"]},
+ "nested2": {"a": 31},
+ },
+ )
+
+ # Test multi (all paths are updated, but return result of last path)
+ assert client.json().arrtrim("doc1", "..a", "1", "-1") == 2
+
+ # Test single
+ assert client.json().arrtrim("doc1", ".nested1.a", "1", "1") == 1
+ assert client.json().get("doc1", "$") == [
+ {"a": [], "nested1": {"a": ["world"]}, "nested2": {"a": 31}}
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrtrim("non_existing_doc", "..a", 1, 1)
+
+
+@pytest.mark.redismod
+def test_objkeys_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": {"baz": 50}},
+ },
+ )
+
+ # Test single
+ assert client.json().objkeys("doc1", "$.nested1.a") == [["foo", "bar"]]
+
+ # Test legacy
+ assert client.json().objkeys("doc1", ".*.a") == ["foo", "bar"]
+ # Test single
+ assert client.json().objkeys("doc1", ".nested2.a") == ["baz"]
+
+ # Test missing key
+ assert client.json().objkeys("non_existing_doc", "..a") is None
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().objkeys("doc1", "$.nowhere")
+
+
+@pytest.mark.redismod
+def test_objlen_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": {"baz": 50}},
+ },
+ )
+ # Test multi
+ assert client.json().objlen("doc1", "$..a") == [2, None, 1]
+ # Test single
+ assert client.json().objlen("doc1", "$.nested1.a") == [2]
+
+ # Test missing key
+ assert client.json().objlen("non_existing_doc", "$..a") is None
+
+ # Test missing path
+ with pytest.raises(exceptions.ResponseError):
+ client.json().objlen("doc1", "$.nowhere")
+
+ # Test legacy
+ assert client.json().objlen("doc1", ".*.a") == 2
+
+ # Test single
+ assert client.json().objlen("doc1", ".nested2.a") == 1
+
+ # Test missing key
+ assert client.json().objlen("non_existing_doc", "..a") is None
+
+ # Test missing path
+ with pytest.raises(exceptions.ResponseError):
+ client.json().objlen("doc1", ".nowhere")
+
+
+@pytest.mark.redismod
+def load_types_data(nested_key_name):
+ td = {
+ "object": {},
+ "array": [],
+ "string": "str",
+ "integer": 42,
+ "number": 1.2,
+ "boolean": False,
+ "null": None,
+ }
+ jdata = {}
+ types = []
+ for i, (k, v) in zip(range(1, len(td) + 1), iter(td.items())):
+ jdata["nested" + str(i)] = {nested_key_name: v}
+ types.append(k)
+
+ return jdata, types
+
+
+@pytest.mark.redismod
+def test_type_dollar(client):
+ jdata, jtypes = load_types_data("a")
+ client.json().set("doc1", "$", jdata)
+ # Test multi
+ assert client.json().type("doc1", "$..a") == jtypes
+
+ # Test single
+ assert client.json().type("doc1", "$.nested2.a") == [jtypes[1]]
+
+ # Test missing key
+ assert client.json().type("non_existing_doc", "..a") is None
+
+
+@pytest.mark.redismod
+def test_clear_dollar(client):
+
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": "claro"},
+ "nested3": {"a": {"baz": 50}},
+ },
+ )
+ # Test multi
+ assert client.json().clear("doc1", "$..a") == 3
+
+ assert client.json().get("doc1", "$") == [
+ {"nested1": {"a": {}}, "a": [], "nested2": {
+ "a": "claro"}, "nested3": {"a": {}}}
+ ]
+
+ # Test single
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": "claro"},
+ "nested3": {"a": {"baz": 50}},
+ },
+ )
+ assert client.json().clear("doc1", "$.nested1.a") == 1
+ assert client.json().get("doc1", "$") == [
+ {
+ "nested1": {"a": {}},
+ "a": ["foo"],
+ "nested2": {"a": "claro"},
+ "nested3": {"a": {"baz": 50}},
+ }
+ ]
+
+ # Test missing path (defaults to root)
+ assert client.json().clear("doc1") == 1
+ assert client.json().get("doc1", "$") == [{}]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().clear("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+def test_toggle_dollar(client):
+ client.json().set(
+ "doc1",
+ "$",
+ {
+ "a": ["foo"],
+ "nested1": {"a": False},
+ "nested2": {"a": 31},
+ "nested3": {"a": True},
+ },
+ )
+ # Test multi
+ assert client.json().toggle("doc1", "$..a") == [None, 1, None, 0]
+ assert client.json().get("doc1", "$") == [
+ {
+ "a": ["foo"],
+ "nested1": {"a": True},
+ "nested2": {"a": 31},
+ "nested3": {"a": False},
+ }
+ ]
+
+ # Test missing key
+ with pytest.raises(exceptions.ResponseError):
+ client.json().toggle("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+def test_debug_dollar(client):
+
+ jdata, jtypes = load_types_data("a")
+
+ client.json().set("doc1", "$", jdata)
+
+ # Test multi
+ assert client.json().debug("MEMORY", "doc1", "$..a") == [
+ 72, 24, 24, 16, 16, 1, 0]
+
+ # Test single
+ assert client.json().debug("MEMORY", "doc1", "$.nested2.a") == [24]
+
+ # Test legacy
+ assert client.json().debug("MEMORY", "doc1", "..a") == 72
+
+ # Test missing path (defaults to root)
+ assert client.json().debug("MEMORY", "doc1") == 72
+
+ # Test missing key
+ assert client.json().debug("MEMORY", "non_existing_doc", "$..a") == []
+
+
+def test_resp_dollar(client):
+
+ data = {
+ "L1": {
+ "a": {
+ "A1_B1": 10,
+ "A1_B2": False,
+ "A1_B3": {
+ "A1_B3_C1": None,
+ "A1_B3_C2": [
+ "A1_B3_C2_D1_1",
+ "A1_B3_C2_D1_2",
+ -19.5,
+ "A1_B3_C2_D1_4",
+ "A1_B3_C2_D1_5",
+ {"A1_B3_C2_D1_6_E1": True},
+ ],
+ "A1_B3_C3": [1],
+ },
+ "A1_B4": {
+ "A1_B4_C1": "foo",
+ },
+ },
+ },
+ "L2": {
+ "a": {
+ "A2_B1": 20,
+ "A2_B2": False,
+ "A2_B3": {
+ "A2_B3_C1": None,
+ "A2_B3_C2": [
+ "A2_B3_C2_D1_1",
+ "A2_B3_C2_D1_2",
+ -37.5,
+ "A2_B3_C2_D1_4",
+ "A2_B3_C2_D1_5",
+ {"A2_B3_C2_D1_6_E1": False},
+ ],
+ "A2_B3_C3": [2],
+ },
+ "A2_B4": {
+ "A2_B4_C1": "bar",
+ },
+ },
+ },
+ }
+ client.json().set("doc1", "$", data)
+ # Test multi
+ res = client.json().resp("doc1", "$..a")
+ assert res == [
+ [
+ "{",
+ "A1_B1",
+ 10,
+ "A1_B2",
+ "false",
+ "A1_B3",
+ [
+ "{",
+ "A1_B3_C1",
+ None,
+ "A1_B3_C2",
+ [
+ "[",
+ "A1_B3_C2_D1_1",
+ "A1_B3_C2_D1_2",
+ "-19.5",
+ "A1_B3_C2_D1_4",
+ "A1_B3_C2_D1_5",
+ ["{", "A1_B3_C2_D1_6_E1", "true"],
+ ],
+ "A1_B3_C3",
+ ["[", 1],
+ ],
+ "A1_B4",
+ ["{", "A1_B4_C1", "foo"],
+ ],
+ [
+ "{",
+ "A2_B1",
+ 20,
+ "A2_B2",
+ "false",
+ "A2_B3",
+ [
+ "{",
+ "A2_B3_C1",
+ None,
+ "A2_B3_C2",
+ [
+ "[",
+ "A2_B3_C2_D1_1",
+ "A2_B3_C2_D1_2",
+ "-37.5",
+ "A2_B3_C2_D1_4",
+ "A2_B3_C2_D1_5",
+ ["{", "A2_B3_C2_D1_6_E1", "false"],
+ ],
+ "A2_B3_C3",
+ ["[", 2],
+ ],
+ "A2_B4",
+ ["{", "A2_B4_C1", "bar"],
+ ],
+ ]
+
+ # Test single
+ resSingle = client.json().resp("doc1", "$.L1.a")
+ assert resSingle == [
+ [
+ "{",
+ "A1_B1",
+ 10,
+ "A1_B2",
+ "false",
+ "A1_B3",
+ [
+ "{",
+ "A1_B3_C1",
+ None,
+ "A1_B3_C2",
+ [
+ "[",
+ "A1_B3_C2_D1_1",
+ "A1_B3_C2_D1_2",
+ "-19.5",
+ "A1_B3_C2_D1_4",
+ "A1_B3_C2_D1_5",
+ ["{", "A1_B3_C2_D1_6_E1", "true"],
+ ],
+ "A1_B3_C3",
+ ["[", 1],
+ ],
+ "A1_B4",
+ ["{", "A1_B4_C1", "foo"],
+ ]
+ ]
+
+ # Test missing path
+ with pytest.raises(exceptions.ResponseError):
+ client.json().resp("doc1", "$.nowhere")
+
+ # Test missing key
+ assert client.json().resp("non_existing_doc", "$..a") is None
+
+
+def test_arrindex_dollar(client):
+
+ client.json().set(
+ "store",
+ "$",
+ {
+ "store": {
+ "book": [
+ {
+ "category": "reference",
+ "author": "Nigel Rees",
+ "title": "Sayings of the Century",
+ "price": 8.95,
+ "size": [10, 20, 30, 40],
+ },
+ {
+ "category": "fiction",
+ "author": "Evelyn Waugh",
+ "title": "Sword of Honour",
+ "price": 12.99,
+ "size": [50, 60, 70, 80],
+ },
+ {
+ "category": "fiction",
+ "author": "Herman Melville",
+ "title": "Moby Dick",
+ "isbn": "0-553-21311-3",
+ "price": 8.99,
+ "size": [5, 10, 20, 30],
+ },
+ {
+ "category": "fiction",
+ "author": "J. R. R. Tolkien",
+ "title": "The Lord of the Rings",
+ "isbn": "0-395-19395-8",
+ "price": 22.99,
+ "size": [5, 6, 7, 8],
+ },
+ ],
+ "bicycle": {"color": "red", "price": 19.95},
+ }
+ },
+ )
+
+ assert client.json().get("store", "$.store.book[?(@.price<10)].size") == [
+ [10, 20, 30, 40],
+ [5, 10, 20, 30],
+ ]
+ assert client.json().arrindex(
+ "store", "$.store.book[?(@.price<10)].size", "20"
+ ) == [-1, -1]
+
+ # Test index of int scalar in multi values
+ client.json().set(
+ "test_num",
+ ".",
+ [
+ {"arr": [0, 1, 3.0, 3, 2, 1, 0, 3]},
+ {"nested1_found": {"arr": [5, 4, 3, 2, 1, 0, 1, 2, 3.0, 2, 4, 5]}},
+ {"nested2_not_found": {"arr": [2, 4, 6]}},
+ {"nested3_scalar": {"arr": "3"}},
+ [
+ {"nested41_not_arr": {"arr_renamed": [1, 2, 3]}},
+ {"nested42_empty_arr": {"arr": []}},
+ ],
+ ],
+ )
+
+ assert client.json().get("test_num", "$..arr") == [
+ [0, 1, 3.0, 3, 2, 1, 0, 3],
+ [5, 4, 3, 2, 1, 0, 1, 2, 3.0, 2, 4, 5],
+ [2, 4, 6],
+ "3",
+ [],
+ ]
+
+ assert client.json().arrindex("test_num", "$..arr", 3) == [
+ 3, 2, -1, None, -1]
+
+ # Test index of double scalar in multi values
+ assert client.json().arrindex("test_num", "$..arr", 3.0) == [
+ 2, 8, -1, None, -1]
+
+ # Test index of string scalar in multi values
+ client.json().set(
+ "test_string",
+ ".",
+ [
+ {"arr": ["bazzz", "bar", 2, "baz", 2, "ba", "baz", 3]},
+ {
+ "nested1_found": {
+ "arr": [
+ None,
+ "baz2",
+ "buzz", 2, 1, 0, 1, "2", "baz", 2, 4, 5]
+ }
+ },
+ {"nested2_not_found": {"arr": ["baz2", 4, 6]}},
+ {"nested3_scalar": {"arr": "3"}},
+ [
+ {"nested41_arr": {"arr_renamed": [1, "baz", 3]}},
+ {"nested42_empty_arr": {"arr": []}},
+ ],
+ ],
+ )
+ assert client.json().get("test_string", "$..arr") == [
+ ["bazzz", "bar", 2, "baz", 2, "ba", "baz", 3],
+ [None, "baz2", "buzz", 2, 1, 0, 1, "2", "baz", 2, 4, 5],
+ ["baz2", 4, 6],
+ "3",
+ [],
+ ]
+
+ assert client.json().arrindex("test_string", "$..arr", "baz") == [
+ 3,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+
+ assert client.json().arrindex("test_string", "$..arr", "baz", 2) == [
+ 3,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "baz", 4) == [
+ 6,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "baz", -5) == [
+ 3,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "baz", 4, 7) == [
+ 6,
+ -1,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "baz", 4, -1) == [
+ 6,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "baz", 4, 0) == [
+ 6,
+ 8,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "5", 7, -1) == [
+ -1,
+ -1,
+ -1,
+ None,
+ -1,
+ ]
+ assert client.json().arrindex("test_string", "$..arr", "5", 7, 0) == [
+ -1,
+ -1,
+ -1,
+ None,
+ -1,
+ ]
+
+ # Test index of None scalar in multi values
+ client.json().set(
+ "test_None",
+ ".",
+ [
+ {"arr": ["bazzz", "None", 2, None, 2, "ba", "baz", 3]},
+ {
+ "nested1_found": {
+ "arr": [
+ "zaz",
+ "baz2",
+ "buzz",
+ 2, 1, 0, 1, "2", None, 2, 4, 5]
+ }
+ },
+ {"nested2_not_found": {"arr": ["None", 4, 6]}},
+ {"nested3_scalar": {"arr": None}},
+ [
+ {"nested41_arr": {"arr_renamed": [1, None, 3]}},
+ {"nested42_empty_arr": {"arr": []}},
+ ],
+ ],
+ )
+ assert client.json().get("test_None", "$..arr") == [
+ ["bazzz", "None", 2, None, 2, "ba", "baz", 3],
+ ["zaz", "baz2", "buzz", 2, 1, 0, 1, "2", None, 2, 4, 5],
+ ["None", 4, 6],
+ None,
+ [],
+ ]
+
+ # Fail with none-scalar value
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrindex(
+ "test_None", "$..nested42_empty_arr.arr", {"arr": []})
+
+ # Do not fail with none-scalar value in legacy mode
+ assert (
+ client.json().arrindex(
+ "test_None", ".[4][1].nested42_empty_arr.arr", '{"arr":[]}'
+ )
+ == -1
+ )
+
+ # Test legacy (path begins with dot)
+ # Test index of int scalar in single value
+ assert client.json().arrindex("test_num", ".[0].arr", 3) == 3
+ assert client.json().arrindex("test_num", ".[0].arr", 9) == -1
+
+ with pytest.raises(exceptions.ResponseError):
+ client.json().arrindex("test_num", ".[0].arr_not", 3)
+ # Test index of string scalar in single value
+ assert client.json().arrindex("test_string", ".[0].arr", "baz") == 3
+ assert client.json().arrindex("test_string", ".[0].arr", "faz") == -1
+ # Test index of None scalar in single value
+ assert client.json().arrindex("test_None", ".[0].arr", "None") == 1
+ assert client.json().arrindex(
+ "test_None",
+ "..nested2_not_found.arr",
+ "None") == 0
+
+
+def test_decoders_and_unstring():
+ assert unstring("4") == 4
+ assert unstring("45.55") == 45.55
+ assert unstring("hello world") == "hello world"
+
+ assert decode_list(b"45.55") == 45.55
+ assert decode_list("45.55") == 45.55
+ assert decode_list(['hello', b'world']) == ['hello', 'world']
diff --git a/tests/testdata/jsontestdata.py b/tests/testdata/jsontestdata.py
new file mode 100644
index 0000000..0a920cc
--- /dev/null
+++ b/tests/testdata/jsontestdata.py
@@ -0,0 +1,617 @@
+nested_large_key = r"""
+{
+ "jkra": [
+ 154,
+ 4472,
+ [
+ 8567,
+ false,
+ 363.84,
+ 5276,
+ "ha",
+ "rizkzs",
+ 93
+ ],
+ false
+ ],
+ "hh": 20.77,
+ "mr": 973.217,
+ "ihbe": [
+ 68,
+ [
+ true,
+ {
+ "lqe": [
+ 486.363,
+ [
+ true,
+ {
+ "mp": {
+ "ory": "rj",
+ "qnl": "tyfrju",
+ "hf": None
+ },
+ "uooc": 7418,
+ "xela": 20,
+ "bt": 7014,
+ "ia": 547,
+ "szec": 68.73
+ },
+ None
+ ],
+ 3622,
+ "iwk",
+ None
+ ],
+ "fepi": 19.954,
+ "ivu": {
+ "rmnd": 65.539,
+ "bk": 98,
+ "nc": "bdg",
+ "dlb": {
+ "hw": {
+ "upzz": [
+ true,
+ {
+ "nwb": [
+ 4259.47
+ ],
+ "nbt": "yl"
+ },
+ false,
+ false,
+ 65,
+ [
+ [
+ [],
+ 629.149,
+ "lvynqh",
+ "hsk",
+ [],
+ 2011.932,
+ true,
+ []
+ ],
+ None,
+ "ymbc",
+ None
+ ],
+ "aj",
+ 97.425,
+ "hc",
+ 58
+ ]
+ },
+ "jq": true,
+ "bi": 3333,
+ "hmf": "pl",
+ "mrbj": [
+ true,
+ false
+ ]
+ }
+ },
+ "hfj": "lwk",
+ "utdl": "aku",
+ "alqb": [
+ 74,
+ 534.389,
+ 7235,
+ [
+ None,
+ false,
+ None
+ ]
+ ]
+ },
+ None,
+ {
+ "lbrx": {
+ "vm": "ubdrbb"
+ },
+ "tie": "iok",
+ "br": "ojro"
+ },
+ 70.558,
+ [
+ {
+ "mmo": None,
+ "dryu": None
+ }
+ ]
+ ],
+ true,
+ None,
+ false,
+ {
+ "jqun": 98,
+ "ivhq": [
+ [
+ [
+ 675.936,
+ [
+ 520.15,
+ 1587.4,
+ false
+ ],
+ "jt",
+ true,
+ {
+ "bn": None,
+ "ygn": "cve",
+ "zhh": true,
+ "aak": 9165,
+ "skx": true,
+ "qqsk": 662.28
+ },
+ {
+ "eio": 9933.6,
+ "agl": None,
+ "pf": false,
+ "kv": 5099.631,
+ "no": None,
+ "shly": 58
+ },
+ [
+ None,
+ [
+ "uiundu",
+ 726.652,
+ false,
+ 94.92,
+ 259.62,
+ {
+ "ntqu": None,
+ "frv": None,
+ "rvop": "upefj",
+ "jvdp": {
+ "nhx": [],
+ "bxnu": {},
+ "gs": None,
+ "mqho": None,
+ "xp": 65,
+ "ujj": {}
+ },
+ "ts": false,
+ "kyuk": [
+ false,
+ 58,
+ {},
+ "khqqif"
+ ]
+ },
+ 167,
+ true,
+ "bhlej",
+ 53
+ ],
+ 64,
+ {
+ "eans": "wgzfo",
+ "zfgb": 431.67,
+ "udy": [
+ {
+ "gnt": [],
+ "zeve": {}
+ },
+ {
+ "pg": {},
+ "vsuc": {},
+ "dw": 19,
+ "ffo": "uwsh",
+ "spk": "pjdyam",
+ "mc": [],
+ "wunb": {},
+ "qcze": 2271.15,
+ "mcqx": None
+ },
+ "qob"
+ ],
+ "wo": "zy"
+ },
+ {
+ "dok": None,
+ "ygk": None,
+ "afdw": [
+ 7848,
+ "ah",
+ None
+ ],
+ "foobar": 3.141592,
+ "wnuo": {
+ "zpvi": {
+ "stw": true,
+ "bq": {},
+ "zord": true,
+ "omne": 3061.73,
+ "bnwm": "wuuyy",
+ "tuv": 7053,
+ "lepv": None,
+ "xap": 94.26
+ },
+ "nuv": false,
+ "hhza": 539.615,
+ "rqw": {
+ "dk": 2305,
+ "wibo": 7512.9,
+ "ytbc": 153,
+ "pokp": None,
+ "whzd": None,
+ "judg": [],
+ "zh": None
+ },
+ "bcnu": "ji",
+ "yhqu": None,
+ "gwc": true,
+ "smp": {
+ "fxpl": 75,
+ "gc": [],
+ "vx": 9352.895,
+ "fbzf": 4138.27,
+ "tiaq": 354.306,
+ "kmfb": {},
+ "fxhy": [],
+ "af": 94.46,
+ "wg": {},
+ "fb": None
+ }
+ },
+ "zvym": 2921,
+ "hhlh": [
+ 45,
+ 214.345
+ ],
+ "vv": "gqjoz"
+ },
+ [
+ "uxlu",
+ None,
+ "utl",
+ 64,
+ [
+ 2695
+ ],
+ [
+ false,
+ None,
+ [
+ "cfcrl",
+ [],
+ [],
+ 562,
+ 1654.9,
+ {},
+ None,
+ "sqzud",
+ 934.6
+ ],
+ {
+ "hk": true,
+ "ed": "lodube",
+ "ye": "ziwddj",
+ "ps": None,
+ "ir": {},
+ "heh": false
+ },
+ true,
+ 719,
+ 50.56,
+ [
+ 99,
+ 6409,
+ None,
+ 4886,
+ "esdtkt",
+ {},
+ None
+ ],
+ [
+ false,
+ "bkzqw"
+ ]
+ ],
+ None,
+ 6357
+ ],
+ {
+ "asvv": 22.873,
+ "vqm": {
+ "drmv": 68.12,
+ "tmf": 140.495,
+ "le": None,
+ "sanf": [
+ true,
+ [],
+ "vyawd",
+ false,
+ 76.496,
+ [],
+ "sdfpr",
+ 33.16,
+ "nrxy",
+ "antje"
+ ],
+ "yrkh": 662.426,
+ "vxj": true,
+ "sn": 314.382,
+ "eorg": None
+ },
+ "bavq": [
+ 21.18,
+ 8742.66,
+ {
+ "eq": "urnd"
+ },
+ 56.63,
+ "fw",
+ [
+ {},
+ "pjtr",
+ None,
+ "apyemk",
+ [],
+ [],
+ false,
+ {}
+ ],
+ {
+ "ho": None,
+ "ir": 124,
+ "oevp": 159,
+ "xdrv": 6705,
+ "ff": [],
+ "sx": false
+ },
+ true,
+ None,
+ true
+ ],
+ "zw": "qjqaap",
+ "hr": {
+ "xz": 32,
+ "mj": 8235.32,
+ "yrtv": None,
+ "jcz": "vnemxe",
+ "ywai": [
+ None,
+ 564,
+ false,
+ "vbr",
+ 54.741
+ ],
+ "vw": 82,
+ "wn": true,
+ "pav": true
+ },
+ "vxa": 881
+ },
+ "bgt",
+ "vuzk",
+ 857
+ ]
+ ]
+ ],
+ None,
+ None,
+ {
+ "xyzl": "nvfff"
+ },
+ true,
+ 13
+ ],
+ "npd": None,
+ "ha": [
+ [
+ "du",
+ [
+ 980,
+ {
+ "zdhd": [
+ 129.986,
+ [
+ "liehns",
+ 453,
+ {
+ "fuq": false,
+ "dxpn": {},
+ "hmpx": 49,
+ "zb": "gbpt",
+ "vdqc": None,
+ "ysjg": false,
+ "gug": 7990.66
+ },
+ "evek",
+ [
+ {}
+ ],
+ "dfywcu",
+ 9686,
+ None
+ ]
+ ],
+ "gpi": {
+ "gt": {
+ "qe": 7460,
+ "nh": "nrn",
+ "czj": 66.609,
+ "jwd": true,
+ "rb": "azwwe",
+ "fj": {
+ "csn": true,
+ "foobar": 1.61803398875,
+ "hm": "efsgw",
+ "zn": "vbpizt",
+ "tjo": 138.15,
+ "teo": {},
+ "hecf": [],
+ "ls": false
+ }
+ },
+ "xlc": 7916,
+ "jqst": 48.166,
+ "zj": "ivctu"
+ },
+ "jl": 369.27,
+ "mxkx": None,
+ "sh": [
+ true,
+ 373,
+ false,
+ "sdis",
+ 6217,
+ {
+ "ernm": None,
+ "srbo": 90.798,
+ "py": 677,
+ "jgrq": None,
+ "zujl": None,
+ "odsm": {
+ "pfrd": None,
+ "kwz": "kfvjzb",
+ "ptkp": false,
+ "pu": None,
+ "xty": None,
+ "ntx": [],
+ "nq": 48.19,
+ "lpyx": []
+ },
+ "ff": None,
+ "rvi": [
+ "ych",
+ {},
+ 72,
+ 9379,
+ 7897.383,
+ true,
+ {},
+ 999.751,
+ false
+ ]
+ },
+ true
+ ],
+ "ghe": [
+ 24,
+ {
+ "lpr": true,
+ "qrs": true
+ },
+ true,
+ false,
+ 7951.94,
+ true,
+ 2690.54,
+ [
+ 93,
+ None,
+ None,
+ "rlz",
+ true,
+ "ky",
+ true
+ ]
+ ],
+ "vet": false,
+ "olle": None
+ },
+ "jzm",
+ true
+ ],
+ None,
+ None,
+ 19.17,
+ 7145,
+ "ipsmk"
+ ],
+ false,
+ {
+ "du": 6550.959,
+ "sps": 8783.62,
+ "nblr": {
+ "dko": 9856.616,
+ "lz": {
+ "phng": "dj"
+ },
+ "zeu": 766,
+ "tn": "dkr"
+ },
+ "xa": "trdw",
+ "gn": 9875.687,
+ "dl": None,
+ "vuql": None
+ },
+ {
+ "qpjo": None,
+ "das": {
+ "or": {
+ "xfy": None,
+ "xwvs": 4181.86,
+ "yj": 206.325,
+ "bsr": [
+ "qrtsh"
+ ],
+ "wndm": {
+ "ve": 56,
+ "jyqa": true,
+ "ca": None
+ },
+ "rpd": 9906,
+ "ea": "dvzcyt"
+ },
+ "xwnn": 9272,
+ "rpx": "zpr",
+ "srzg": {
+ "beo": 325.6,
+ "sq": None,
+ "yf": None,
+ "nu": [
+ 377,
+ "qda",
+ true
+ ],
+ "sfz": "zjk"
+ },
+ "kh": "xnpj",
+ "rk": None,
+ "hzhn": [
+ None
+ ],
+ "uio": 6249.12,
+ "nxrv": 1931.635,
+ "pd": None
+ },
+ "pxlc": true,
+ "mjer": false,
+ "hdev": "msr",
+ "er": None
+ },
+ "ug",
+ None,
+ "yrfoix",
+ 503.89,
+ 563
+ ],
+ "tcy": 300,
+ "me": 459.17,
+ "tm": [
+ 134.761,
+ "jcoels",
+ None
+ ],
+ "iig": 945.57,
+ "ad": "be"
+ },
+ "ltpdm",
+ None,
+ 14.53
+ ],
+ "xi": "gxzzs",
+ "zfpw": 1564.87,
+ "ow": None,
+ "tm": [
+ 46,
+ 876.85
+ ],
+ "xejv": None
+}
+""" # noqa