Skip to content

Commit

Permalink
Python - Implement JSON.OBJLEN command functionality (valkey-io#2495)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: BoazBD <boazbd@amazon.com>
Signed-off-by: Shoham Elias <116083498+shohamazon@users.noreply.github.com>
Co-authored-by: Shoham Elias <116083498+shohamazon@users.noreply.github.com>
  • Loading branch information
BoazBD and shohamazon authored Oct 28, 2024
1 parent c197f1d commit 52dd474
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Python: Add JSON.OBJLEN command ([#2495](https://github.com/valkey-io/valkey-glide/pull/2495))
* Python: FT.EXPLAIN and FT.EXPLAINCLI commands added([#2508](https://github.com/valkey-io/valkey-glide/pull/2508))
* Python: Python FT.INFO command added([#2429](https://github.com/valkey-io/valkey-glide/pull/2494))
* Python: Add FT.SEARCH command([#2470](https://github.com/valkey-io/valkey-glide/pull/2470))
Expand Down
52 changes: 52 additions & 0 deletions python/python/glide/async_commands/server_modules/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,58 @@ async def nummultby(
return cast(Optional[bytes], await client.custom_command(args))


async def objlen(
client: TGlideClient,
key: TEncodable,
path: Optional[TEncodable] = None,
) -> Optional[TJsonResponse[int]]:
"""
Retrieves the number of key-value pairs in the object stored at the specified `path` within the JSON document stored at `key`.
Args:
client (TGlideClient): The client to execute the command.
key (TEncodable): The key of the JSON document.
path (Optional[TEncodable]): The path within the JSON document. Defaults to None.
Returns:
Optional[TJsonResponse[int]]:
For JSONPath (`path` starts with `$`):
Returns a list of integer replies for every possible path, indicating the length of the object,
or None for JSON values matching the path that are not an object.
If `path` doesn't exist, an empty array will be returned.
For legacy path (`path` doesn't starts with `$`):
Returns the length of the object at `path`.
If multiple paths match, the length of the first object match is returned.
If the JSON value at `path` is not an object or if `path` doesn't exist, an error is raised.
If `key` doesn't exist, None is returned.
Examples:
>>> from glide import json
>>> await json.set(client, "doc", "$", '{"a": 1.0, "b": {"a": {"x": 1, "y": 2}, "b": 2.5, "c": true}}')
b'OK' # Indicates successful setting of the value at the root path '$' in the key `doc`.
>>> await json.objlen(client, "doc", "$")
[2] # Returns the number of key-value pairs at the root object, which has 2 keys: 'a' and 'b'.
>>> await json.objlen(client, "doc", ".")
2 # Returns the number of key-value pairs for the object matching the path '.', which has 2 keys: 'a' and 'b'.
>>> await json.objlen(client, "doc", "$.b")
[3] # Returns the length of the object at path '$.b', which has 3 keys: 'a', 'b', and 'c'.
>>> await json.objlen(client, "doc", ".b")
3 # Returns the length of the nested object at path '.b', which has 3 keys.
>>> await json.objlen(client, "doc", "$..a")
[None, 2]
>>> await json.objlen(client, "doc")
2 # Returns the number of key-value pairs for the object matching the path '.', which has 2 keys: 'a' and 'b'.
"""
args = ["JSON.OBJLEN", key]
if path:
args.append(path)
return cast(
Optional[TJsonResponse[int]],
await client.custom_command(args),
)


async def objkeys(
client: TGlideClient,
key: TEncodable,
Expand Down
50 changes: 50 additions & 0 deletions python/python/tests/tests_server_modules/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,56 @@ async def test_json_type(self, glide_client: TGlideClient):
result = await json.type(glide_client, key, "[*]")
assert result == b"string" # Expecting only the first type (string for key1)

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_json_objlen(self, glide_client: TGlideClient):
key = get_random_string(5)

json_value = {"a": 1.0, "b": {"a": {"x": 1, "y": 2}, "b": 2.5, "c": True}}

assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK

len = await json.objlen(glide_client, key, "$")
assert len == [2]

len = await json.objlen(glide_client, key, ".")
assert len == 2

len = await json.objlen(glide_client, key, "$..")
assert len == [2, 3, 2]

len = await json.objlen(glide_client, key, "..")
assert len == 2

len = await json.objlen(glide_client, key, "$..b")
assert len == [3, None]

len = await json.objlen(glide_client, key, "..b")
assert len == 3

len = await json.objlen(glide_client, key, "..a")
assert len == 2

len = await json.objlen(glide_client, key)
assert len == 2

# path doesn't exist
assert await json.objlen(glide_client, key, "$.non_existing_path") == []
with pytest.raises(RequestError):
await json.objlen(glide_client, key, "non_existing_path")

# Value at path isnt an object
assert await json.objlen(glide_client, key, "$.a") == [None]
with pytest.raises(RequestError):
await json.objlen(glide_client, key, ".a")

# Non-existing key
assert await json.objlen(glide_client, "non_exiting_key", "$") == None
assert await json.objlen(glide_client, "non_exiting_key", ".") == None

assert await json.set(glide_client, key, "$", '{"a": 1, "b": 2, "c":3, "d":4}')
assert await json.objlen(glide_client, key) == 4

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_json_arrlen(self, glide_client: TGlideClient):
Expand Down

0 comments on commit 52dd474

Please sign in to comment.