diff options
author | Chayim <chayim@users.noreply.github.com> | 2021-11-04 13:20:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-04 13:20:31 +0200 |
commit | 8d3c61598706eb049caa66a23501018f2f416673 (patch) | |
tree | 0aa3430a7ad3b1a9212194043a9e08c187ecd9fe | |
parent | 72b49263f86f32c9df945433f21d3f3a7444d1a0 (diff) | |
download | redis-py-8d3c61598706eb049caa66a23501018f2f416673.tar.gz |
Support for json multipath ($) (#1663)
-rw-r--r-- | redis/commands/helpers.py | 5 | ||||
-rw-r--r-- | redis/commands/json/__init__.py | 45 | ||||
-rw-r--r-- | redis/commands/json/commands.py | 4 | ||||
-rw-r--r-- | redis/commands/json/decoders.py | 65 | ||||
-rw-r--r-- | redis/commands/json/helpers.py | 25 | ||||
-rw-r--r-- | tests/conftest.py | 3 | ||||
-rw-r--r-- | tests/test_json.py | 1133 | ||||
-rw-r--r-- | tests/testdata/jsontestdata.py | 617 |
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 |