Skip to content

Commit

Permalink
Python: LPos (valkey-io#1740)
Browse files Browse the repository at this point in the history
* Python: LPos

* linter

* updating docs

* update description

* update return statement

---------

Co-authored-by: TJ Zhang <[email protected]>
  • Loading branch information
2 people authored and cyip10 committed Jul 16, 2024
1 parent c4d29f9 commit 426eb30
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* Python: Add ZSCAN and HSCAN commands ([#1732](https://github.com/aws/glide-for-redis/pull/1732))
* Python: Added FCALL_RO command ([#1721](https://github.com/aws/glide-for-redis/pull/1721))
* Python: Added WATCH and UNWATCH command ([#1736](https://github.com/aws/glide-for-redis/pull/1736))
* Python: Added LPos command ([#1740](https://github.com/aws/glide-for-redis/pull/1740))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
58 changes: 58 additions & 0 deletions python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5999,3 +5999,61 @@ async def lcs_idx(
Mapping[str, Union[list[list[Union[list[int], int]]], int]],
await self._execute_command(RequestType.LCS, args),
)

async def lpos(
self,
key: str,
element: str,
rank: Optional[int] = None,
count: Optional[int] = None,
max_len: Optional[int] = None,
) -> Union[int, list[int], None]:
"""
Returns the index or indexes of element(s) matching `element` in the `key` list. If no match is found,
None is returned.
See https://valkey.io/commands/lpos for more details.
Args:
key (str): The name of the list.
element (str): The value to search for within the list.
rank (Optional[int]): The rank of the match to return.
count (Optional[int]): The number of matches wanted. A `count` of 0 returns all the matches.
max_len (Optional[int]): The maximum number of comparisons to make between the element and the items
in the list. A `max_len` of 0 means unlimited amount of comparisons.
Returns:
Union[int, list[int], None]: The index of the first occurrence of `element`,
or None if `element` is not in the list.
With the `count` option, a list of indices of matching elements will be returned.
Examples:
>>> await client.rpush(key, ['a', 'b', 'c', '1', '2', '3', 'c', 'c'])
>>> await client.lpos(key, 'c')
2
>>> await client.lpos(key, 'c', rank = 2)
6
>>> await client.lpos(key, 'c', rank = -1)
7
>>> await client.lpos(key, 'c', count = 2)
[2, 6]
>>> await client.lpos(key, 'c', count = 0)
[2, 6, 7]
Since: Redis version 6.0.6.
"""
args = [key, element]

if rank is not None:
args.extend(["RANK", str(rank)])

if count is not None:
args.extend(["COUNT", str(count)])

if max_len is not None:
args.extend(["MAXLEN", str(max_len)])

return cast(
Union[int, list[int], None],
await self._execute_command(RequestType.LPos, args),
)
42 changes: 42 additions & 0 deletions python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4275,6 +4275,48 @@ def wait(
args = [str(numreplicas), str(timeout)]
return self.append_command(RequestType.Wait, args)

def lpos(
self: TTransaction,
key: str,
element: str,
rank: Optional[int] = None,
count: Optional[int] = None,
max_len: Optional[int] = None,
) -> TTransaction:
"""
Returns the index or indexes of element(s) matching `element` in the `key` list. If no match is found,
None is returned.
See https://valkey.io/commands/lpos for more details.
Args:
key (str): The name of the list.
element (str): The value to search for within the list.
rank (Optional[int]): The rank of the match to return.
count (Optional[int]): The number of matches wanted. A `count` of 0 returns all the matches.
max_len (Optional[int]): The maximum number of comparisons to make between the element and the items
in the list. A `max_len` of 0 means unlimited amount of comparisons.
Command Response:
Union[int, list[int], None]: The index of the first occurrence of `element`,
or None if `element` is not in the list.
With the `count` option, a list of indices of matching elements will be returned.
Since: Redis version 6.0.6.
"""
args = [key, element]

if rank is not None:
args.extend(["RANK", str(rank)])

if count is not None:
args.extend(["COUNT", str(count)])

if max_len is not None:
args.extend(["MAXLEN", str(max_len)])

return self.append_command(RequestType.LPos, args)


class Transaction(BaseTransaction):
"""
Expand Down
53 changes: 53 additions & 0 deletions python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7899,6 +7899,59 @@ async def test_unwatch(self, redis_client: GlideClient):
async def test_unwatch_with_route(self, redis_client: GlideClusterClient):
assert await redis_client.unwatch(RandomNode()) == OK

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_lpos(self, redis_client: TGlideClient):
min_version = "6.0.6"
if await check_if_server_version_lt(redis_client, min_version):
# TODO: change it to pytest fixture after we'll implement a sync client
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
key = f"{{key}}-1{get_random_string(5)}"
non_list_key = f"{{key}}-2{get_random_string(5)}"
mylist = ["a", "a", "b", "c", "a", "b"]

# basic case
await redis_client.rpush(key, mylist)
assert await redis_client.lpos(key, "b") == 2

# reverse traversal
assert await redis_client.lpos(key, "b", -2) == 2

# unlimited comparisons
assert await redis_client.lpos(key, "a", 1, None, 0) == 0

# limited comparisons
assert await redis_client.lpos(key, "c", 1, None, 2) is None

# element does not exist
assert await redis_client.lpos(key, "non_existing") is None

# with count
assert await redis_client.lpos(key, "a", 1, 0, 0) == [0, 1, 4]

# with count and rank
assert await redis_client.lpos(key, "a", -2, 0, 0) == [1, 0]

# key does not exist
assert await redis_client.lpos("non_existing", "non_existing") is None

# invalid rank value
with pytest.raises(RequestError):
await redis_client.lpos(key, "a", 0)

# invalid count
with pytest.raises(RequestError):
await redis_client.lpos(non_list_key, "a", None, -1)

# invalid max_len
with pytest.raises(RequestError):
await redis_client.lpos(non_list_key, "a", None, None, -1)

# wrong data type
await redis_client.set(non_list_key, "non_list_value")
with pytest.raises(RequestError):
await redis_client.lpos(non_list_key, "a")


class TestMultiKeyCommandCrossSlot:
@pytest.mark.parametrize("cluster_mode", [True])
Expand Down
10 changes: 10 additions & 0 deletions python/python/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async def transaction_test(
key22 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # getex
key23 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # string
key24 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # string
key25 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # list

value = datetime.now(timezone.utc).strftime("%m/%d/%Y, %H:%M:%S")
value_bytes = value.encode()
Expand Down Expand Up @@ -634,6 +635,15 @@ async def transaction_test(
transaction.random_key()
args.append(key.encode())

min_version = "6.0.6"
if not await check_if_server_version_lt(redis_client, min_version):
transaction.rpush(key25, ["a", "a", "b", "c", "a", "b"])
args.append(6)
transaction.lpos(key25, "a")
args.append(0)
transaction.lpos(key25, "a", 1, 0, 0)
args.append([0, 1, 4])

min_version = "6.2.0"
if not await check_if_server_version_lt(redis_client, min_version):
transaction.flushall(FlushMode.SYNC)
Expand Down

0 comments on commit 426eb30

Please sign in to comment.