Skip to content

Commit a52ebf5

Browse files
committed
Python: adds JSON.ARRPOP command
Signed-off-by: Shoham Elias <shohame@amazon.com>
1 parent 855bb33 commit a52ebf5

File tree

3 files changed

+171
-3
lines changed

3 files changed

+171
-3
lines changed

python/python/glide/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
VectorFieldAttributesHnsw,
5050
VectorType,
5151
)
52+
from glide.async_commands.server_modules.json import JsonArrPopOptions, JsonGetOptions
5253
from glide.async_commands.sorted_set import (
5354
AggregationType,
5455
GeoSearchByBox,
@@ -200,8 +201,6 @@
200201
"InfBound",
201202
"InfoSection",
202203
"InsertPosition",
203-
"json",
204-
"ft",
205204
"LexBoundary",
206205
"Limit",
207206
"ListDirection",
@@ -229,6 +228,12 @@
229228
"ClusterScanCursor"
230229
# PubSub
231230
"PubSubMsg",
231+
# Json
232+
"json",
233+
"JsonGetOptions",
234+
"JsonArrPopOptions",
235+
# Search
236+
"ft",
232237
# Logger
233238
"Logger",
234239
"LogLevel",

python/python/glide/async_commands/server_modules/json.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ def get_options(self) -> List[str]:
5454
return args
5555

5656

57+
from typing import List, Optional, Union
58+
59+
60+
class JsonArrPopOptions:
61+
"""
62+
Options for the JSON.ARRPOP command.
63+
64+
Args:
65+
path (TEncodable): The path within the JSON document.
66+
index (Optional[int]): The index of the element to pop. If not specified, will pop the last element.
67+
Out of boundary indexes are rounded to their respective array boundaries. Defaults to None.
68+
"""
69+
70+
def __init__(self, path: TEncodable, index: Optional[int] = None):
71+
self.path = path
72+
self.index = index
73+
74+
def get_options(self) -> List[TEncodable]:
75+
"""
76+
Get the options as a list of arguments for the JSON.ARRPOP command.
77+
78+
Returns:
79+
List[TEncodable]: A list containing the path and, if specified, the index.
80+
"""
81+
args = [self.path]
82+
if self.index is not None:
83+
args.append(str(self.index))
84+
return args
85+
86+
5787
async def set(
5888
client: TGlideClient,
5989
key: TEncodable,
@@ -193,6 +223,66 @@ async def arrlen(
193223
)
194224

195225

226+
async def arrpop(
227+
client: TGlideClient,
228+
key: TEncodable,
229+
options: Optional[JsonArrPopOptions] = None,
230+
) -> Optional[TJsonResponse[bytes]]:
231+
"""
232+
Pops the last element from the array located at the specified path within the JSON document stored at `key`.
233+
If `options.index` is provided, it pops the element at that index instead of the last element.
234+
235+
See https://valkey.io/commands/json.arrpop/ for more details.
236+
237+
Args:
238+
client (TGlideClient): The client to execute the command.
239+
key (TEncodable): The key of the JSON document.
240+
options (Optional[JsonArrPopOptions]): Options including the path and optional index. See `JsonArrPopOptions`.
241+
242+
Returns:
243+
Optional[TJsonResponse[bytes]]:
244+
For JSONPath (`path` starts with `$`):
245+
Returns a list of bytes string replies for every possible path, representing the popped JSON values,
246+
or None for JSON values matching the path that are not an array or are an empty array.
247+
If a value is not an array, its corresponding return value is null.
248+
For legacy path (`path` starts with `.`):
249+
Returns a bytes string representing the popped JSON value, or None if the array at `path` is empty.
250+
If multiple paths match, the value from the first matching array that is not empty is returned.
251+
If the JSON value at `path` is not a array or if `path` doesn't exist, an error is raised.
252+
If `key` doesn't exist, an error is raised.
253+
254+
Examples:
255+
>>> from glide import json
256+
>>> await json.set(client, "doc", "$", '{"a": [1, 2, true], "b": {"a": [3, 4, ["value", 3, false], 5], "c": {"a": 42}}}')
257+
b'OK' # JSON is successfully set
258+
>>> await json.arrpop(client, "doc", JsonArrPopOptions(path="$.a", index=1))
259+
[b'2'] # Pop second element from array at path $.a
260+
>>> await json.arrpop(client, "doc", JsonArrPopOptions(path="$..a"))
261+
[b'true', b'5', None] # Pop last elements from all arrays matching path `..a`
262+
263+
#### Using a legacy path (..) to pop the first matching array
264+
>>> await json.arrpop(client, "doc", JsonArrPopOptions(path="..a"))
265+
b"1" # First match popped (from array at path $.a)
266+
267+
#### Even though only one value is returned from `..a`, subsequent arrays are also affected
268+
>>> await json.get(client, "doc", "$..a")
269+
b"[[], [3, 4], 42]" # Remaining elements after pop show the changes
270+
271+
>>> await json.set(client, "doc", "$", '[[], ["a"], ["a", "b", "c"]]')
272+
b'OK' # JSON is successfully set
273+
>>> await json.arrpop(client, "doc", JsonArrPopOptions(path=".", index=-1))
274+
b'["a","b","c"]' # Pop last elements at path `.`
275+
"""
276+
args = ["JSON.ARRPOP", key]
277+
if options:
278+
args.extend(options.get_options())
279+
280+
return cast(
281+
Optional[TJsonResponse[bytes]],
282+
await client.custom_command(args),
283+
)
284+
285+
196286
async def clear(
197287
client: TGlideClient,
198288
key: TEncodable,

python/python/tests/tests_server_modules/test_json.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from glide.async_commands.core import ConditionalChange, InfoSection
77
from glide.async_commands.server_modules import json
8-
from glide.async_commands.server_modules.json import JsonGetOptions
8+
from glide.async_commands.server_modules.json import JsonArrPopOptions, JsonGetOptions
99
from glide.config import ProtocolVersion
1010
from glide.constants import OK
1111
from glide.exceptions import RequestError
@@ -359,3 +359,76 @@ async def test_json_clear(self, glide_client: TGlideClient):
359359

360360
with pytest.raises(RequestError):
361361
await json.clear(glide_client, "non_existing_key", ".")
362+
363+
@pytest.mark.parametrize("cluster_mode", [True, False])
364+
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
365+
async def test_json_arrpop(self, glide_client: TGlideClient):
366+
key = get_random_string(5)
367+
key2 = get_random_string(5)
368+
369+
json_value = '{"a": [1, 2, true], "b": {"a": [3, 4, ["value", 3, false] ,5], "c": {"a": 42}}}'
370+
assert await json.set(glide_client, key, "$", json_value) == OK
371+
372+
assert await json.arrpop(
373+
glide_client, key, JsonArrPopOptions(path="$.a", index=1)
374+
) == [b"2"]
375+
assert (
376+
await json.arrpop(glide_client, key, JsonArrPopOptions(path="$..a"))
377+
) == [b"true", b"5", None]
378+
379+
assert (
380+
await json.arrpop(glide_client, key, JsonArrPopOptions(path="..a")) == b"1"
381+
)
382+
# Even if only one array element was returned, ensure second array at `..a` was popped
383+
assert await json.get(glide_client, key, "$..a") == b"[[],[3,4],42]"
384+
385+
# Out of index
386+
assert await json.arrpop(
387+
glide_client, key, JsonArrPopOptions(path="$..a", index=10)
388+
) == [None, b"4", None]
389+
390+
assert (
391+
await json.arrpop(
392+
glide_client, key, JsonArrPopOptions(path="..a", index=-10)
393+
)
394+
== b"3"
395+
)
396+
397+
# Path is not an array
398+
assert await json.arrpop(glide_client, key, JsonArrPopOptions(path="$")) == [
399+
None
400+
]
401+
with pytest.raises(RequestError):
402+
assert await json.arrpop(glide_client, key, JsonArrPopOptions(path="."))
403+
with pytest.raises(RequestError):
404+
assert await json.arrpop(glide_client, key)
405+
406+
# Non existing path
407+
assert (
408+
await json.arrpop(
409+
glide_client, key, JsonArrPopOptions(path="$.non_existing_path")
410+
)
411+
== []
412+
)
413+
with pytest.raises(RequestError):
414+
assert await json.arrpop(
415+
glide_client, key, JsonArrPopOptions(path="non_existing_path")
416+
)
417+
418+
with pytest.raises(RequestError):
419+
await json.arrpop(
420+
glide_client, "non_existing_key", JsonArrPopOptions(path="$.a")
421+
)
422+
with pytest.raises(RequestError):
423+
await json.arrpop(
424+
glide_client, "non_existing_key", JsonArrPopOptions(path=".a")
425+
)
426+
427+
assert (
428+
await json.set(glide_client, key2, "$", '[[], ["a"], ["a", "b", "c"]]')
429+
== OK
430+
)
431+
assert (
432+
await json.arrpop(glide_client, key2, JsonArrPopOptions(path=".", index=-1))
433+
== b'["a","b","c"]'
434+
)

0 commit comments

Comments
 (0)