Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python - Implement JSON.OBJLEN command functionality #2495

Merged
merged 11 commits into from
Oct 28, 2024
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
51 changes: 51 additions & 0 deletions python/python/glide/async_commands/server_modules/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,57 @@ 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 array.
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 array 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")
2 # Returns the number of key-value pairs for the object matching the path '$..a', which has 2 keys: 'x' and 'y'.
>>> 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
Loading