Skip to content

Commit

Permalink
Python: add DUMP and RESTORE commands (valkey-io#1733)
Browse files Browse the repository at this point in the history
* Initial commit for Python Dump and Restore commands

* Commit dummpy UT

* Added TODO to convert str to bytes

* Updated CHANGELOG.md

* WIP

* Modified glide_client to support bytes as encoded argument. Added more tests

* Updated RESTORE command's examples

* Addressed review comments

* Updated str to TEncodable

* Changed REPLACE and ABSTTL to bool

* Changed UT to include cluster_mode
  • Loading branch information
yipin-chen authored and cyip10 committed Jul 16, 2024
1 parent 1b1b9c4 commit b0a1d62
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
* Python: Added XINFO GROUPS and XINFO CONSUMERS commands ([#1753](https://github.com/aws/glide-for-redis/pull/1753))
* Python: Added LPOS command ([#1740](https://github.com/aws/glide-for-redis/pull/1740))
* Python: Added SCAN command ([#1623](https://github.com/aws/glide-for-redis/pull/1623))
* Python: Added DUMP and Restore commands ([#1733](https://github.com/aws/glide-for-redis/pull/1733))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
82 changes: 82 additions & 0 deletions python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5702,6 +5702,88 @@ async def getex(
await self._execute_command(RequestType.GetEx, args),
)

async def dump(
self,
key: TEncodable,
) -> Optional[bytes]:
"""
Serialize the value stored at `key` in a Valkey-specific format and return it to the user.
See https://valkey.io/commands/dump for more details.
Args:
key (TEncodable): The `key` to serialize.
Returns:
Optional[bytes]: The serialized value of the data stored at `key`.
If `key` does not exist, `None` will be returned.
Examples:
>>> await client.dump("key")
b"value" # The serialized value stored at `key`.
>>> await client.dump("nonExistingKey")
None # Non-existing key will return `None`.
"""
return cast(
Optional[bytes],
await self._execute_command(RequestType.Dump, [key]),
)

async def restore(
self,
key: TEncodable,
ttl: int,
value: TEncodable,
replace: bool = False,
absttl: bool = False,
idletime: Optional[int] = None,
frequency: Optional[int] = None,
) -> TOK:
"""
Create a `key` associated with a `value` that is obtained by deserializing the provided
serialized `value` obtained via `dump`.
See https://valkey.io/commands/restore for more details.
Args:
key (TEncodable): The `key` to create.
ttl (int): The expiry time (in milliseconds). If `0`, the `key` will persist.
value (TEncodable) The serialized value to deserialize and assign to `key`.
replace (bool): Set to `True` to replace the key if it exists.
absttl (bool): Set to `True` to specify that `ttl` represents an absolute Unix
timestamp (in milliseconds).
idletime (Optional[int]): Set the `IDLETIME` option with object idletime to the given key.
frequency (Optional[int]): Set the `FREQ` option with object frequency to the given key.
Returns:
OK: If the `key` was successfully restored with a `value`.
Examples:
>>> await client.restore("newKey", 0, value)
OK # Indicates restore `newKey` without any ttl expiry nor any option
>>> await client.restore("newKey", 0, value, replace=True)
OK # Indicates restore `newKey` with `REPLACE` option
>>> await client.restore("newKey", 0, value, absttl=True)
OK # Indicates restore `newKey` with `ABSTTL` option
>>> await client.restore("newKey", 0, value, idletime=10)
OK # Indicates restore `newKey` with `IDLETIME` option
>>> await client.restore("newKey", 0, value, frequency=5)
OK # Indicates restore `newKey` with `FREQ` option
"""
args = [key, str(ttl), value]
if replace is True:
args.append("REPLACE")
if absttl is True:
args.append("ABSTTL")
if idletime is not None:
args.extend(["IDLETIME", str(idletime)])
if frequency is not None:
args.extend(["FREQ", str(frequency)])
return cast(
TOK,
await self._execute_command(RequestType.Restore, args),
)

async def sscan(
self,
key: TEncodable,
Expand Down
90 changes: 90 additions & 0 deletions python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8258,6 +8258,96 @@ async def test_standalone_client_random_key(self, redis_client: GlideClient):
# DB 0 should still have no keys, so random_key should still return None
assert await redis_client.random_key() is None

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_dump_restore(self, redis_client: TGlideClient):
key1 = f"{{key}}-1{get_random_string(10)}"
key2 = f"{{key}}-2{get_random_string(10)}"
key3 = f"{{key}}-3{get_random_string(10)}"
nonExistingKey = f"{{key}}-4{get_random_string(10)}"
value = get_random_string(5)

await redis_client.set(key1, value)

# Dump an existing key
bytesData = await redis_client.dump(key1)
assert bytesData is not None

# Dump non-existing key
assert await redis_client.dump(nonExistingKey) is None

# Restore to a new key and verify its value
assert await redis_client.restore(key2, 0, bytesData) == OK
newValue = await redis_client.get(key2)
assert newValue == value.encode()

# Restore to an existing key
with pytest.raises(RequestError) as e:
await redis_client.restore(key2, 0, bytesData)
assert "Target key name already exists" in str(e)

# Restore using a value with checksum error
with pytest.raises(RequestError) as e:
await redis_client.restore(key3, 0, value.encode())
assert "payload version or checksum are wrong" in str(e)

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_dump_restore_options(self, redis_client: TGlideClient):
key1 = f"{{key}}-1{get_random_string(10)}"
key2 = f"{{key}}-2{get_random_string(10)}"
key3 = f"{{key}}-3{get_random_string(10)}"
value = get_random_string(5)

await redis_client.set(key1, value)

# Dump an existing key
bytesData = await redis_client.dump(key1)
assert bytesData is not None

# Restore without option
assert await redis_client.restore(key2, 0, bytesData) == OK

# Restore with REPLACE option
assert await redis_client.restore(key2, 0, bytesData, replace=True) == OK

# Restore to an existing key holding different value with REPLACE option
assert await redis_client.sadd(key3, ["a"]) == 1
assert await redis_client.restore(key3, 0, bytesData, replace=True) == OK

# Restore with REPLACE, ABSTTL, and positive TTL
assert (
await redis_client.restore(key2, 1000, bytesData, replace=True, absttl=True)
== OK
)

# Restore with REPLACE, ABSTTL, and negative TTL
with pytest.raises(RequestError) as e:
await redis_client.restore(key2, -10, bytesData, replace=True, absttl=True)
assert "Invalid TTL value" in str(e)

# Restore with REPLACE and positive idletime
assert (
await redis_client.restore(key2, 0, bytesData, replace=True, idletime=10)
== OK
)

# Restore with REPLACE and negative idletime
with pytest.raises(RequestError) as e:
await redis_client.restore(key2, 0, bytesData, replace=True, idletime=-10)
assert "Invalid IDLETIME value" in str(e)

# Restore with REPLACE and positive frequency
assert (
await redis_client.restore(key2, 0, bytesData, replace=True, frequency=10)
== OK
)

# Restore with REPLACE and negative frequency
with pytest.raises(RequestError) as e:
await redis_client.restore(key2, 0, bytesData, replace=True, frequency=-10)
assert "Invalid FREQ value" in str(e)

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

0 comments on commit b0a1d62

Please sign in to comment.