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: added LMOVE and BLMOVE commands #1536

Merged
merged 5 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion python/python/glide/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0

from glide.async_commands.command_args import Limit, OrderBy
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.core import (
ConditionalChange,
ExpireOptions,
Expand Down Expand Up @@ -98,6 +98,7 @@
"json",
"LexBoundary",
"Limit",
"ListDirection",
"RangeByIndex",
"RangeByLex",
"RangeByScore",
Expand Down
16 changes: 16 additions & 0 deletions python/python/glide/async_commands/command_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ class OrderBy(Enum):
"""
DESC: Sort in descending order.
"""


class ListDirection(Enum):
"""
Enumeration representing element popping or adding direction for List commands.
"""

LEFT = "LEFT"
"""
LEFT: Represents the option that elements should be popped from or added to the left side of a list.
"""

RIGHT = "RIGHT"
"""
RIGHT: Represents the option that elements should be popped from or added to the right side of a list.
"""
90 changes: 89 additions & 1 deletion python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
get_args,
)

from glide.async_commands.command_args import Limit, OrderBy
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.sorted_set import (
AggregationType,
InfBound,
Expand Down Expand Up @@ -1496,6 +1496,94 @@ async def linsert(
),
)

async def lmove(
self,
source: str,
destination: str,
wherefrom: ListDirection,
whereto: ListDirection,
Copy link
Collaborator

Choose a reason for hiding this comment

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

in python the syntax is:

Suggested change
wherefrom: ListDirection,
whereto: ListDirection,
where_from: ListDirection,
where_to: ListDirection,

) -> Optional[str]:
"""
Atomically pops and removes the left/right-most element to the list stored at `source`
depending on `wherefrom`, and pushes the element at the first/last element of the list
Copy link
Contributor

Choose a reason for hiding this comment

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

if you change as per Sho's suggestions, please update here too

stored at `destination` depending on `whereto`.

GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
See https://redis.io/commands/lmove/ for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
See https://redis.io/commands/lmove/ for details.
See https://valkey.io/commands/lmove/ for details.


Args:
source (str): The key to the source list.
destination (str): The key to the destination list.
wherefrom (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`).
whereto (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`).

Returns:
Optional[str]: The popped element, or `None` if `source` does not exist.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Optional[str]: The popped element, or `None` if `source` does not exist.
Optional[str]: The popped element, or None if `source` does not exist.


Examples:
>>> await client.lpush("testKey1", ["two", "one"])
>>> await client.lpush("testKey2", ["four", "three"])
>>> result = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT)
>>> assert result == "one"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
>>> result = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT)
>>> assert result == "one"
>>> await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT)
"one"

>>> updated_array1 = await client.lrange("testKey1", 0, -1)
>>> updated_array2 = await client.lrange("testKey2", 0, -1)
>>> assert updated_array1 == ["two"]
>>> assert updated_array2 == ["one", "three", "four"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
>>> updated_array1 = await client.lrange("testKey1", 0, -1)
>>> updated_array2 = await client.lrange("testKey2", 0, -1)
>>> assert updated_array1 == ["two"]
>>> assert updated_array2 == ["one", "three", "four"]
>>> await client.lrange("testKey1", 0, -1)
["two"]
>>> await client.lrange("testKey2", 0, -1)
["one", "three", "four"]

"""
return cast(
Optional[str],
await self._execute_command(
RequestType.LMove, [source, destination, wherefrom.value, whereto.value]
),
)

async def blmove(
self,
source: str,
destination: str,
wherefrom: ListDirection,
Copy link
Collaborator

Choose a reason for hiding this comment

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

same

whereto: ListDirection,
timeout: float,
) -> Optional[str]:
"""
Blocks the connection until it pops atomically and removes the left/right-most element to the
list stored at `source` depending on `wherefrom`, and pushes the element at the first/last element
of the list stored at `destination` depending on `whereto`.
`blmove` is the blocking variant of `lmove`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
`blmove` is the blocking variant of `lmove`.
`BLMOVE` is the blocking variant of `LMOVE`.

see other blocking commands


Notes:
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
1. When in cluster mode, both `source` and `destination` must map to the same hash slot.
2. `blmove` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices.

See https://redis.io/commands/blmove/ for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
See https://redis.io/commands/blmove/ for details.
See https://valkey.io/commands/blmove/ for details.


Args:
source (str): The key to the source list.
destination (str): The key to the destination list.
wherefrom (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`).
whereto (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`).
timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely.

Returns:
Optional[str]: The popped element, or `None` if `source` does not exist or if the operation timed-out.

Examples:
>>> await client.lpush("testKey1", ["two", "one"])
>>> await client.lpush("testKey2", ["four", "three"])
>>> result = await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1)
>>> assert result == "one"
>>> updated_array1 = await client.lrange("testKey1", 0, -1)
>>> updated_array2 = await client.lrange("testKey2", 0, -1)
>>> assert updated_array1 == ["two"]
>>> assert updated_array2 == ["one", "three", "four"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
>>> result = await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1)
>>> assert result == "one"
>>> updated_array1 = await client.lrange("testKey1", 0, -1)
>>> updated_array2 = await client.lrange("testKey2", 0, -1)
>>> assert updated_array1 == ["two"]
>>> assert updated_array2 == ["one", "three", "four"]
>>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1)
"one"
>>> await client.lrange("testKey1", 0, -1)
["two"]
>>> await client.lrange("testKey2", 0, -1)
["one", "three", "four"]

"""
return cast(
Optional[str],
await self._execute_command(
RequestType.BLMove,
[source, destination, wherefrom.value, whereto.value, str(timeout)],
),
)

async def sadd(self, key: str, members: List[str]) -> int:
"""
Add specified members to the set stored at `key`.
Expand Down
63 changes: 62 additions & 1 deletion python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import threading
from typing import List, Mapping, Optional, Tuple, TypeVar, Union

from glide.async_commands.command_args import Limit, OrderBy
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.core import (
ConditionalChange,
ExpireOptions,
Expand Down Expand Up @@ -949,6 +949,67 @@ def linsert(
RequestType.LInsert, [key, position.value, pivot, element]
)

def lmove(
self: TTransaction,
source: str,
destination: str,
wherefrom: ListDirection,
whereto: ListDirection,
) -> TTransaction:
"""
Atomically pops and removes the left/right-most element to the list stored at `source`
depending on `wherefrom`, and pushes the element at the first/last element of the list
stored at `destination` depending on `whereto`.

Notes:
When in cluster mode, both `source` and `destination` must map to the same hash slot.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't add this to transction.
It is applicable to the entire transaction, rather than to a particular command(s) in it.

Please Add since section


See https://redis.io/commands/lmove/ for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
See https://redis.io/commands/lmove/ for details.
See https://valkey.io/commands/lmove/ for details.


Args:
source (str): The key to the source list.
destination (str): The key to the destination list.
wherefrom (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`).
whereto (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`).

Command response:
Optional[str]: The popped element, or `None` if `source` does not exist.
"""
return self.append_command(
RequestType.LMove, [source, destination, wherefrom.value, whereto.value]
)

def blmove(
self: TTransaction,
source: str,
destination: str,
wherefrom: ListDirection,
whereto: ListDirection,
timeout: float,
) -> TTransaction:
"""
Blocks the connection until it pops atomically and removes the left/right-most element to the
list stored at `source` depending on `wherefrom`, and pushes the element at the first/last element
of the list stored at `destination` depending on `whereto`.
`blmove` is the blocking variant of `lmove`.

See https://redis.io/commands/blmove/ for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
See https://redis.io/commands/blmove/ for details.
See https://valkey.io/commands/blmove/ for details.


GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
Args:
source (str): The key to the source list.
destination (str): The key to the destination list.
wherefrom (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`).
whereto (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`).
timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely.

Command response:
Optional[str]: The popped element, or `None` if `source` does not exist or if the operation timed-out.
"""
return self.append_command(
RequestType.BLMove,
[source, destination, wherefrom.value, whereto.value, str(timeout)],
)

def sadd(self: TTransaction, key: str, members: List[str]) -> TTransaction:
"""
Add specified members to the set stored at `key`.
Expand Down
142 changes: 141 additions & 1 deletion python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import pytest
from glide import ClosingError, RequestError, Script
from glide.async_commands.command_args import Limit, OrderBy
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.core import (
ConditionalChange,
ExpireOptions,
Expand Down Expand Up @@ -1063,6 +1063,144 @@ async def test_linsert(self, redis_client: TRedisClient):
with pytest.raises(RequestError):
await redis_client.linsert(key2, InsertPosition.AFTER, "p", "e")

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_lmove(self, redis_client: TRedisClient):
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
key1 = "{SameSlot}" + get_random_string(10)
key2 = "{SameSlot}" + get_random_string(10)

GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
# Initialize the lists
assert await redis_client.lpush(key1, ["2", "1"]) == 2
assert await redis_client.lpush(key2, ["4", "3"]) == 2

# Move from LEFT to LEFT
assert (
await redis_client.lmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT)
== "1"
)
assert await redis_client.lrange(key1, 0, -1) == ["2"]
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"]

# Move from LEFT to RIGHT
assert (
await redis_client.lmove(
key1, key2, ListDirection.LEFT, ListDirection.RIGHT
)
== "2"
)
assert await redis_client.lrange(key1, 0, -1) == []
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"]

# Move from RIGHT to LEFT
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
assert (
await redis_client.lmove(
key2, key1, ListDirection.RIGHT, ListDirection.LEFT
)
== "2"
)
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"]
assert await redis_client.lrange(key1, 0, -1) == ["2"]

# Move from RIGHT to RIGHT
assert (
await redis_client.lmove(
key2, key1, ListDirection.RIGHT, ListDirection.RIGHT
)
== "4"
)
assert await redis_client.lrange(key2, 0, -1) == ["1", "3"]
assert await redis_client.lrange(key1, 0, -1) == ["2", "4"]

# Non-existing source key
assert (
await redis_client.lmove(
"{SameSlot}non_existing_key", key1, ListDirection.LEFT, ListDirection.LEFT
)
is None
)

# Non-list source key
key3 = get_random_string(10)
assert await redis_client.set(key3, "value") == OK
with pytest.raises(RequestError):
await redis_client.lmove(key3, key1, ListDirection.LEFT, ListDirection.LEFT)

# Non-list destination key
with pytest.raises(RequestError):
await redis_client.lmove(key1, key3, ListDirection.LEFT, ListDirection.LEFT)

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_blmove(self, redis_client: TRedisClient):
key1 = "{SameSlot}" + get_random_string(10)
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
key2 = "{SameSlot}" + get_random_string(10)

# Initialize the lists
assert await redis_client.lpush(key1, ["2", "1"]) == 2
assert await redis_client.lpush(key2, ["4", "3"]) == 2

# Move from LEFT to LEFT with blocking
assert (
await redis_client.blmove(
key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0.1
)
== "1"
)
assert await redis_client.lrange(key1, 0, -1) == ["2"]
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"]

# Move from LEFT to RIGHT with blocking
assert (
await redis_client.blmove(
key1, key2, ListDirection.LEFT, ListDirection.RIGHT, 0.1
)
== "2"
)
assert await redis_client.lrange(key1, 0, -1) == []
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"]

# Move from RIGHT to LEFT with blocking
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
assert (
await redis_client.blmove(
key2, key1, ListDirection.RIGHT, ListDirection.LEFT, 0.1
)
== "2"
)
assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"]
assert await redis_client.lrange(key1, 0, -1) == ["2"]

# Move from RIGHT to RIGHT with blocking
assert (
await redis_client.blmove(
key2, key1, ListDirection.RIGHT, ListDirection.RIGHT, 0.1
)
== "4"
)
assert await redis_client.lrange(key2, 0, -1) == ["1", "3"]
assert await redis_client.lrange(key1, 0, -1) == ["2", "4"]

# Non-existing source key with blocking
assert (
await redis_client.blmove(
"{SameSlot}non_existing_key", key1, ListDirection.LEFT, ListDirection.LEFT, 0.1
)
is None
)

# Non-list source key with blocking
key3 = get_random_string(10)
assert await redis_client.set(key3, "value") == OK
with pytest.raises(RequestError):
await redis_client.blmove(
key3, key1, ListDirection.LEFT, ListDirection.LEFT, 0.1
)

# Non-list destination key with blocking
with pytest.raises(RequestError):
await redis_client.blmove(
key1, key3, ListDirection.LEFT, ListDirection.LEFT, 0.1
)
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_sadd_srem_smembers_scard(self, redis_client: TRedisClient):
Expand Down Expand Up @@ -3933,6 +4071,8 @@ async def test_multi_key_command_returns_cross_slot_error(
redis_client.zunion(["def", "ghi"]),
redis_client.zunion_withscores(["def", "ghi"]),
redis_client.sort_store("abc", "zxy"),
redis_client.lmove("abc", "zxy", ListDirection.LEFT, ListDirection.LEFT),
redis_client.blmove("abc", "zxy", ListDirection.LEFT, ListDirection.LEFT, 1),
]

if not await check_if_server_version_lt(redis_client, "7.0.0"):
Expand Down
6 changes: 5 additions & 1 deletion python/python/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest
from glide import RequestError
from glide.async_commands.command_args import Limit, OrderBy
from glide.async_commands.command_args import Limit, ListDirection, OrderBy
from glide.async_commands.core import (
GeospatialData,
InsertPosition,
Expand Down Expand Up @@ -182,6 +182,10 @@ async def transaction_test(
args.append(OK)
transaction.lrange(key5, 0, -1)
args.append([value2, value])
transaction.lmove(key5, key6, ListDirection.LEFT, ListDirection.LEFT)
GilboaAWS marked this conversation as resolved.
Show resolved Hide resolved
args.append(value2)
transaction.blmove(key6, key5, ListDirection.LEFT, ListDirection.LEFT, 1)
args.append(value2)
transaction.lpop_count(key5, 2)
args.append([value2, value])
transaction.linsert(key5, InsertPosition.BEFORE, "non_existing_pivot", "element")
Expand Down
Loading