Skip to content

Commit

Permalink
Python: add COPY Command (valkey-io#1626)
Browse files Browse the repository at this point in the history
* Python: Added COPY command (#383)

Python: Added COPY command

* Updated CHANGELOG.md

* Addressed review comments
  • Loading branch information
yipin-chen authored Jun 22, 2024
1 parent ef80f54 commit 7e32e1b
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* Python: Added ZREVRANK command ([#1614](https://github.com/aws/glide-for-redis/pull/1614))
* Python: Added XDEL command ([#1619](https://github.com/aws/glide-for-redis/pull/1619))
* Python: Added XRANGE command ([#1624](https://github.com/aws/glide-for-redis/pull/1624))
* Python: Added COPY command ([#1626](https://github.com/aws/glide-for-redis/pull/1626))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
40 changes: 40 additions & 0 deletions python/python/glide/async_commands/cluster_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,43 @@ async def flushall(
TClusterResponse[TOK],
await self._execute_command(RequestType.FlushAll, args, route),
)

async def copy(
self,
source: str,
destination: str,
replace: Optional[bool] = None,
) -> bool:
"""
Copies the value stored at the `source` to the `destination` key. When `replace` is True,
removes the `destination` key first if it already exists, otherwise performs no action.
See https://valkey.io/commands/copy for more details.
Note:
Both `source` and `destination` must map to the same hash slot.
Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.
Returns:
bool: True if the source was copied. Otherwise, returns False.
Examples:
>>> await client.set("source", "sheep")
>>> await client.copy("source", "destination")
True # Source was copied
>>> await client.get("destination")
"sheep"
Since: Redis version 6.2.0.
"""
args = [source, destination]
if replace is True:
args.append("REPLACE")
return cast(
bool,
await self._execute_command(RequestType.Copy, args),
)
44 changes: 44 additions & 0 deletions python/python/glide/async_commands/standalone_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,47 @@ async def flushall(self, flush_mode: Optional[FlushMode] = None) -> TOK:
TOK,
await self._execute_command(RequestType.FlushAll, args),
)

async def copy(
self,
source: str,
destination: str,
destinationDB: Optional[int] = None,
replace: Optional[bool] = None,
) -> bool:
"""
Copies the value stored at the `source` to the `destination` key. If `destinationDB`
is specified, the value will be copied to the database specified by `destinationDB`,
otherwise the current database will be used. When `replace` is True, removes the
`destination` key first if it already exists, otherwise performs no action.
See https://valkey.io/commands/copy for more details.
Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
destinationDB (Optional[int]): The alternative logical database index for the destination key.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.
Returns:
bool: True if the source was copied. Otherwise, return False.
Examples:
>>> await client.set("source", "sheep")
>>> await client.copy("source", "destination", 1, False)
True # Source was copied
>>> await client.select(1)
>>> await client.get("destination")
"sheep"
Since: Redis version 6.2.0.
"""
args = [source, destination]
if destinationDB is not None:
args.extend(["DB", str(destinationDB)])
if replace is True:
args.append("REPLACE")
return cast(
bool,
await self._execute_command(RequestType.Copy, args),
)
62 changes: 62 additions & 0 deletions python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,40 @@ def sort_store(
)
return self.append_command(RequestType.Sort, args)

def copy(
self: TTransaction,
source: str,
destination: str,
destinationDB: Optional[int] = None,
replace: Optional[bool] = None,
) -> TTransaction:
"""
Copies the value stored at the `source` to the `destination` key. If `destinationDB`
is specified, the value will be copied to the database specified by `destinationDB`,
otherwise the current database will be used. When `replace` is True, removes the
`destination` key first if it already exists, otherwise performs no action.
See https://valkey.io/commands/copy for more details.
Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
destinationDB (Optional[int]): The alternative logical database index for the destination key.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.
Command response:
bool: True if the source was copied. Otherwise, return False.
Since: Redis version 6.2.0.
"""
args = [source, destination]
if destinationDB is not None:
args.extend(["DB", str(destinationDB)])
if replace is not None:
args.append("REPLACE")

return self.append_command(RequestType.Copy, args)


class ClusterTransaction(BaseTransaction):
"""
Expand Down Expand Up @@ -3694,4 +3728,32 @@ def sort_store(
args = _build_sort_args(key, None, limit, None, order, alpha, store=destination)
return self.append_command(RequestType.Sort, args)

def copy(
self: TTransaction,
source: str,
destination: str,
replace: Optional[bool] = None,
) -> TTransaction:
"""
Copies the value stored at the `source` to the `destination` key. When `replace` is True,
removes the `destination` key first if it already exists, otherwise performs no action.
See https://valkey.io/commands/copy for more details.
Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.
Command response:
bool: True if the source was copied. Otherwise, return False.
Since: Redis version 6.2.0.
"""
args = [source, destination]
if replace is not None:
args.append("REPLACE")

return self.append_command(RequestType.Copy, args)

# TODO: add all CLUSTER commands
104 changes: 103 additions & 1 deletion python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5632,6 +5632,107 @@ async def test_getex(self, redis_client: TRedisClient):
)
assert await redis_client.ttl(key1) == -1

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_copy_no_database(self, redis_client: TRedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

source = f"{{testKey}}:1-{get_random_string(10)}"
destination = f"{{testKey}}:2-{get_random_string(10)}"
value1 = get_random_string(5)
value2 = get_random_string(5)

# neither key exists
assert await redis_client.copy(source, destination, replace=False) is False
assert await redis_client.copy(source, destination) is False

# source exists, destination does not
await redis_client.set(source, value1)
assert await redis_client.copy(source, destination, replace=False) is True
assert await redis_client.get(destination) == value1

# new value for source key
await redis_client.set(source, value2)

# both exists, no REPLACE
assert await redis_client.copy(source, destination) is False
assert await redis_client.copy(source, destination, replace=False) is False
assert await redis_client.get(destination) == value1

# both exists, with REPLACE
assert await redis_client.copy(source, destination, replace=True) is True
assert await redis_client.get(destination) == value2

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_copy_database(self, redis_client: RedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

source = get_random_string(10)
destination = get_random_string(10)
value1 = get_random_string(5)
value2 = get_random_string(5)
index0 = 0
index1 = 1
index2 = 2

try:
assert await redis_client.select(index0) == OK

# neither key exists
assert (
await redis_client.copy(source, destination, index1, replace=False)
is False
)

# source exists, destination does not
await redis_client.set(source, value1)
assert (
await redis_client.copy(source, destination, index1, replace=False)
is True
)
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value1

# new value for source key
assert await redis_client.select(index0) == OK
await redis_client.set(source, value2)

# no REPLACE, copying to existing key on DB 0 & 1, non-existing key on DB 2
assert (
await redis_client.copy(source, destination, index1, replace=False)
is False
)
assert (
await redis_client.copy(source, destination, index2, replace=False)
is True
)

# new value only gets copied to DB 2
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value1
assert await redis_client.select(index2) == OK
assert await redis_client.get(destination) == value2

# both exists, with REPLACE, when value isn't the same, source always get copied to destination
assert await redis_client.select(index0) == OK
assert (
await redis_client.copy(source, destination, index1, replace=True)
is True
)
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value2

# invalid DB index
with pytest.raises(RequestError):
await redis_client.copy(source, destination, -1, replace=True)
finally:
assert await redis_client.select(0) == OK


class TestMultiKeyCommandCrossSlot:
@pytest.mark.parametrize("cluster_mode", [True])
Expand Down Expand Up @@ -5682,7 +5783,8 @@ async def test_multi_key_command_returns_cross_slot_error(
"zxy",
GeospatialData(15, 37),
GeoSearchByBox(400, 400, GeoUnit.KILOMETERS),
)
),
redis_client.copy("abc", "zxy", replace=True),
]
)

Expand Down
25 changes: 25 additions & 0 deletions python/python/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ async def transaction_test(
transaction.pexpiretime(key)
args.append(-1)

if not await check_if_server_version_lt(redis_client, "6.2.0"):
transaction.copy(key, key2, replace=True)
args.append(True)

transaction.rename(key, key2)
args.append(OK)

Expand Down Expand Up @@ -697,6 +701,27 @@ def test_transaction_clear(self):
transaction.clear()
assert len(transaction.commands) == 0

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_standalone_copy_transaction(self, redis_client: RedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

keyslot = get_random_string(3)
key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot
key1 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot
value = get_random_string(5)
transaction = Transaction()
transaction.select(1)
transaction.set(key, value)
transaction.copy(key, key1, 1, replace=True)
transaction.get(key1)
result = await redis_client.exec(transaction)
assert result is not None
assert result[2] == True
assert result[3] == value

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_transaction_chaining_calls(self, redis_client: TRedisClient):
Expand Down

0 comments on commit 7e32e1b

Please sign in to comment.