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: adds JSON.STRLEN, JSON.STRAPPEND commands #1187

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Python: Added ZLEXCOUNT command ([#1305](https://github.com/aws/glide-for-redis/pull/1305))
* Python: Added ZREMRANGEBYLEX command ([#1306](https://github.com/aws/glide-for-redis/pull/1306))
* Python: Added LINSERT command ([#1304](https://github.com/aws/glide-for-redis/pull/1304))
* Python: Added JSON.STRLEN, JSON.STAPPEND commands ([#1187](https://github.com/aws/glide-for-redis/pull/1187))

#### Fixes
* Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/aws/glide-for-redis/pull/1203))
Expand Down
134 changes: 125 additions & 9 deletions python/python/glide/async_commands/redis_modules/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async def set(

Returns:
Optional[TOK]: If the value is successfully set, returns OK.
If value isn't set because of `set_condition`, returns None.
If `value` isn't set because of `set_condition`, returns None.

Examples:
>>> from glide import json as redisJson
Expand Down Expand Up @@ -112,8 +112,20 @@ async def get(
options (Optional[JsonGetOptions]): Options for formatting the string representation of the JSON data. See `JsonGetOptions`.

Returns:
str: A bulk string representation of the returned value.
If `key` doesn't exists, returns None.
TJsonResponse[Optional[string]]:
If one path is given:
For JSONPath (path starts with `$`):
Returns a stringified JSON list of string replies for every possible path,
or a string representation of an empty array, if path doesn't exists.
If `key` doesn't exist, returns None.
For legacy path (path doesn't start with `$`):
Returns a bulk string representation of the value in `path`.
If `path` doesn't exist, an error is raised.
If `key` doesn't exist, returns None.
If multiple paths are given:
Returns a stringified JSON object, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path.
In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths.
For more information about the returned type, see `TJsonResponse`.

Examples:
>>> from glide import json as redisJson
Expand Down Expand Up @@ -157,7 +169,7 @@ async def delete(

Returns:
int: The number of elements removed.
If `key` or path doesn't exist, returns 0.
If `key` or `path` doesn't exist, returns 0.

Examples:
>>> from glide import json as redisJson
Expand Down Expand Up @@ -194,7 +206,7 @@ async def forget(

Returns:
int: The number of elements removed.
If `key` or path doesn't exist, returns 0.
If `key` or `path` doesn't exist, returns 0.

Examples:
>>> from glide import json as redisJson
Expand All @@ -214,6 +226,105 @@ async def forget(
)


async def strappend(
client: TRedisClient,
key: str,
value: str,
path: Optional[str] = None,
) -> TJsonResponse[int]:
"""
Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`.

See https://redis.io/commands/json.strapped/ for more details.

Args:
client (TRedisClient): The Redis client to execute the command.
key (str): The key of the JSON document.
value (str): The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'.
path (Optional[str]): The JSONPath to specify. Default is root `$`.

Returns:
TJsonResponse[int]:
For JSONPath (`path` starts with `$`):
Returns a list of integer replies for every possible path, indicating the length of the resulting string after appending `value`,
or None for JSON values matching the path that are not string.
If `key` doesn't exist, an error is raised.
For legacy path (`path` doesn't start with `$`):
Returns the length of the resulting string after appending `value` to the string at `path`.
If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised.
If `key` doesn't exist, an error is raised.
For more information about the returned type, see `TJsonResponse`.

Examples:
>>> from glide import json as redisJson
>>> import json
>>> await redisJson.set(client, "doc", "$", json.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}))
'OK'
>>> await redisJson.strappend(client, "doc", json.dumps("baz"), "$..a")
[6, 8, None] # The new length of the string values at path '$..a' in the key stored at `doc` after the append operation.
>>> await redisJson.strappend(client, "doc", '"foo"', "nested.a")
11 # The length of the string value after appending "foo" to the string at path 'nested.array' in the key stored at `doc`.
>>> json.loads(await redisJson.get(client, json.dumps("doc"), "$"))
[{"a":"foobaz", "nested": {"a": "hellobazfoo"}, "nested2": {"a": 31}}] # The updated JSON value in the key stored at `doc`.
"""

return cast(
TJsonResponse[int],
await client.custom_command(
["JSON.STRAPPEND", key] + ([path, value] if path else [value])
),
)


async def strlen(
client: TRedisClient,
key: str,
path: Optional[str] = None,
) -> TJsonResponse[Optional[int]]:
"""
Returns the length of the JSON string value stored at the specified `path` within the JSON document stored at `key`.

See https://redis.io/commands/json.strlen/ for more details.

Args:
client (TRedisClient): The Redis client to execute the command.
key (str): The key of the JSON document.
path (Optional[str]): The JSONPath to specify. Default is root `$`.

Returns:
TJsonResponse[Optional[int]]:
For JSONPath (`path` starts with `$`):
Returns a list of integer replies for every possible path, indicating the length of the JSON string value,
or None for JSON values matching the path that are not string. If `key` doesn't exist, an error is raised.
For legacy path (`path` doesn't start with `$`):
Returns the length of the JSON value at `path` or None if `key` doesn't exist.
If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised.
If `key` doesn't exist, None is returned.
For more information about the returned type, see `TJsonResponse`.

Examples:
>>> from glide import json as redisJson
>>> import json
>>> await redisJson.set(client, "doc", "$", json.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}))
'OK'
>>> await redisJson.strlen(client, "doc", "$..a")
[3, 5, None] # The length of the string values at path '$..a' in the key stored at `doc`.
>>> await redisJson.strlen(client, "doc", "nested.a")
5 # The length of the JSON value at path 'nested.a' in the key stored at `doc`.
>>> await redisJson.strlen(client, "doc", "$")
[None] # Returns an array with None since the value at root path does in the JSON document stored at `doc` is not a string.
>>> await redisJson.strlen(client, "non_existing_key", ".")
None # `key` doesn't exist and the provided path is in legacy path syntax.
"""

return cast(
TJsonResponse[Optional[int]],
await client.custom_command(
["JSON.STRLEN", key, path] if path else ["JSON.STRLEN", key]
),
)


async def toggle(
client: TRedisClient,
key: str,
Expand All @@ -230,10 +341,15 @@ async def toggle(
path (str): The JSONPath to specify.

Returns:
TJsonResponse[bool]: For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value,
or None for JSON values matching the path that are not boolean.
For legacy path (`path` doesn't starts with `$`), returns the value of the toggled boolean in `path`.
Note that when sending legacy path syntax, If `path` doesn't exist or the value at `path` isn't a boolean, an error is raised.
TJsonResponse[bool]:
For JSONPath (`path` starts with `$`):
Returns a list of boolean replies for every possible path, with the toggled boolean value,
or None for JSON values matching the path that are not boolean.
If `key` doesn't exist, an error is raised.
For legacy path (`path` doesn't start with `$`):
Returns the value of the toggled boolean in `path`.
If the JSON value at `path` is not a boolean of if `path` doesn't exist, an error is raised.
If `key` doesn't exist, an error is raised.
For more information about the returned type, see `TJsonResponse`.

Examples:
Expand Down
76 changes: 76 additions & 0 deletions python/python/tests/tests_redis_modules/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,86 @@ async def test_json_toggle(self, redis_client: TRedisClient):

assert await json.toggle(redis_client, key, "$..bool") == [False, True, None]
assert await json.toggle(redis_client, key, "bool") is True
assert await json.toggle(redis_client, key, "$.not_existing") == []

assert await json.toggle(redis_client, key, "$.nested") == [None]
with pytest.raises(RequestError):
assert await json.toggle(redis_client, key, "nested")

with pytest.raises(RequestError):
assert await json.toggle(redis_client, key, ".not_existing")

with pytest.raises(RequestError):
assert await json.toggle(redis_client, "non_exiting_key", "$")

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about tests without specifying path?

async def test_json_strlen(self, redis_client: TRedisClient):
key = get_random_string(10)
json_value = {"a": "foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}
assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK

assert await json.strlen(redis_client, key, "$..a") == [3, 5, None]
assert await json.strlen(redis_client, key, "a") == 3

assert await json.strlen(redis_client, key, "$.nested") == [None]
with pytest.raises(RequestError):
assert await json.strlen(redis_client, key, "nested")

with pytest.raises(RequestError):
assert await json.strlen(redis_client, key)

assert await json.strlen(redis_client, key, "$.non_existing_path") == []
with pytest.raises(RequestError):
assert await json.strlen(redis_client, key, ".non_existing_path")

assert await json.strlen(redis_client, "non_exiting_key", ".") is None

with pytest.raises(RequestError):
assert await json.strlen(redis_client, "non_exiting_key", "$")

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_json_strappend(self, redis_client: TRedisClient):
key = get_random_string(10)
json_value = {"a": "foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}
assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK

assert await json.strappend(redis_client, key, '"bar"', "$..a") == [6, 8, None]
assert await json.strappend(redis_client, key, OuterJson.dumps("foo"), "a") == 9

json_str = await json.get(redis_client, key, ".")
assert isinstance(json_str, str)
assert OuterJson.loads(json_str) == {
"a": "foobarfoo",
"nested": {"a": "hellobar"},
"nested2": {"a": 31},
}

assert await json.strappend(
redis_client, key, OuterJson.dumps("bar"), "$.nested"
) == [None]

with pytest.raises(RequestError):
assert await json.strappend(
redis_client, key, OuterJson.dumps("bar"), ".nested"
)

with pytest.raises(RequestError):
assert await json.strappend(redis_client, key, OuterJson.dumps("bar"))

assert (
await json.strappend(
redis_client, key, OuterJson.dumps("try"), "$.non_existing_path"
)
== []
)
with pytest.raises(RequestError):
assert await json.strappend(
redis_client, key, OuterJson.dumps("try"), "non_existing_path"
)

with pytest.raises(RequestError):
assert await json.strappend(
redis_client, "non_exiting_key", OuterJson.dumps("try")
)