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: add LCS command #406

Merged
merged 4 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
149 changes: 149 additions & 0 deletions python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5428,3 +5428,152 @@ def try_get_pubsub_message(self) -> Optional[PubSubMsg]:
>>> pubsub_msg = listening_client.try_get_pubsub_message()
"""
...

async def lcs(
self,
key1: str,
key2: str,
) -> str:
"""
Returns the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.

Returns:
A String containing the longest common subsequence between the 2 strings.
An empty String is returned if the keys do not exist or have no common subsequences.

Examples:
>>> await client.mset({"testKey1" : "abcd", "testKey2": "axcd"})
'OK'
>>> await client.lcs("testKey1", "testKey2")
'acd'

Since: Redis version 7.0.0.
"""
args = [key1, key2]

return cast(
str,
await self._execute_command(RequestType.LCS, args),
)

async def lcs_len(
self,
key1: str,
key2: str,
) -> int:
"""
Returns the length of the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.

Returns:
The length of the longest common subsequence between the 2 strings.

Examples:
>>> await client.mset({"testKey1" : "abcd", "testKey2": "axcd"})
'OK'
>>> await client.lcs_len("testKey1", "testKey2")
3 # the length of the longest common subsequence between these 2 stings is 2.
jamesx-improving marked this conversation as resolved.
Show resolved Hide resolved

Since: Redis version 7.0.0.
"""
args = [key1, key2, "LEN"]

return cast(
int,
await self._execute_command(RequestType.LCS, args),
)

async def lcs_idx(
self,
key1: str,
key2: str,
min_match_len: Optional[int] = None,
with_match_len: Optional[bool] = False,
) -> Mapping[str, Union[list[list[Union[list[int], int]]], int]]:
"""
Returns the indices and length of the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.
min_match_len (Optional[int]): The minimum length of matches to include in the result.
with_match_len (Optional[bool]): If True, include the length of the substring matched for each substring.

Returns:
A Map containing the indices of the longest common subsequence between the
jamesx-improving marked this conversation as resolved.
Show resolved Hide resolved
2 strings and the length of the longest common subsequence. The resulting map contains two
keys, "matches" and "len":
- "len" is mapped to the length of the longest common subsequence between the 2 strings.
- "matches" is mapped to a three dimensional int array that stores pairs of indices that
represent the location of the common subsequences in the strings held by key1 and key2,
with the length of the match after each matches, if with_match_len is enabled.

Examples:
>>> await client.mset({"testKey1" : "abcd1234", "testKey2": "bcdef1234"})
'OK'
>>> await client.lcs_idx("testKey1", "testKey2")
{
'matches': [
[
[4, 7], # starting and ending indices of the subsequence "1234" in "abcd1234" (testKey1)
[5, 8], # starting and ending indices of the subsequence "1234" in "bcdef1234" (testKey2)
],
[
[1, 3], # starting and ending indices of the subsequence "bcd" in "abcd1234" (testKey1)
[0, 2], # starting and ending indices of the subsequence "bcd" in "bcdef1234" (testKey2)
],
],
'len': 7 # length of the entire longest common subsequence
}
>>> await client.lcs_idx("testKey1", "testKey2", min_match_len=4)
{
'matches': [
[
[4, 7],
[5, 8],
],
# the other match with a length of 3 is excluded
],
'len': 7
}
>>> await client.lcs_idx("testKey1", "testKey2", with_match_len=True)
{
'matches': [
[
[4, 7],
[5, 8],
4, # length of this match ("1234")
jamesx-improving marked this conversation as resolved.
Show resolved Hide resolved
],
[
[1, 3],
[0, 2],
3, # length of this match ("bcd")
],
],
'len': 7
}

Since: Redis version 7.0.0.
"""
args = [key1, key2, "IDX"]

if min_match_len is not None:
args.extend(["MINMATCHLEN", str(min_match_len)])

if with_match_len:
args.append("WITHMATCHLEN")

return cast(
Mapping[str, Union[list[list[Union[list[int], int]]], int]],
await self._execute_command(RequestType.LCS, args),
)
83 changes: 83 additions & 0 deletions python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3891,6 +3891,89 @@ def random_key(self: TTransaction) -> TTransaction:
"""
return self.append_command(RequestType.RandomKey, [])

def lcs(
self: TTransaction,
key1: str,
key2: str,
) -> TTransaction:
"""
Returns the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.

Command Response:
A String containing the longest common subsequence between the 2 strings.
An empty String is returned if the keys do not exist or have no common subsequences.

Since: Redis version 7.0.0.
"""
args = [key1, key2]

return self.append_command(RequestType.LCS, args)

def lcs_len(
self: TTransaction,
key1: str,
key2: str,
) -> TTransaction:
"""
Returns the length of the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.

Command Response:
The length of the longest common subsequence between the 2 strings.

Since: Redis version 7.0.0.
"""
args = [key1, key2, "LEN"]

return self.append_command(RequestType.LCS, args)

def lcs_idx(
self: TTransaction,
key1: str,
key2: str,
min_match_len: Optional[int] = None,
with_match_len: Optional[bool] = False,
) -> TTransaction:
"""
Returns the indices and length of the longest common subsequence between strings stored at key1 and key2.
See https://valkey.io/commands/lcs for more details.

Args:
key1 (str): The key that stores the first string.
key2 (str): The key that stores the second string.
min_match_len (Optional[int]): The minimum length of matches to include in the result.
with_match_len (Optional[bool]): If True, include the length of the substring matched for each substring.

Command Response:
A Map containing the indices of the longest common subsequence between the
2 strings and the length of the longest common subsequence. The resulting map contains two
keys, "matches" and "len":
- "len" is mapped to the length of the longest common subsequence between the 2 strings.
- "matches" is mapped to a three dimensional int array that stores pairs of indices that
represent the location of the common subsequences in the strings held by key1 and key2,
with the length of the match after each matches, if with_match_len is enabled.

Since: Redis version 7.0.0.
"""
args = [key1, key2, "IDX"]

if min_match_len is not None:
args.extend(["MINMATCHLEN", str(min_match_len)])

if with_match_len:
args.append("WITHMATCHLEN")

return self.append_command(RequestType.LCS, args)


class Transaction(BaseTransaction):
"""
Expand Down
116 changes: 116 additions & 0 deletions python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7102,6 +7102,119 @@ 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", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_lcs(self, redis_client: GlideClient):
min_version = "7.0.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
key1 = "testKey1"
value1 = "abcd"
key2 = "testKey2"
value2 = "axcd"
nonexistent_key = "nonexistent_key"
expected_subsequence = "acd"
Copy link

Choose a reason for hiding this comment

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

I don't get this -- why is acd the longest common subsequence? Shouldn't it be cd?

Copy link
Author

Choose a reason for hiding this comment

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

This is Longest common subsequence, not to be confused with Longest common substring.

expected_subsequence_with_nonexistent_key = ""
assert await redis_client.set(key1, value1) == OK
assert await redis_client.set(key2, value2) == OK
assert await redis_client.lcs(key1, key2) == expected_subsequence
assert (
await redis_client.lcs(key1, nonexistent_key)
== expected_subsequence_with_nonexistent_key
)

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_lcs_len(self, redis_client: GlideClient):
min_version = "7.0.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
key1 = "testKey1"
value1 = "abcd"
key2 = "testKey2"
value2 = "axcd"
nonexistent_key = "nonexistent_key"
expected_subsequence_length = 3
expected_subsequence_length_with_nonexistent_key = 0
assert await redis_client.set(key1, value1) == OK
assert await redis_client.set(key2, value2) == OK
assert await redis_client.lcs_len(key1, key2) == expected_subsequence_length
assert (
await redis_client.lcs_len(key1, nonexistent_key)
== expected_subsequence_length_with_nonexistent_key
)

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_lcs_len_idx(self, redis_client: GlideClient):
jamesx-improving marked this conversation as resolved.
Show resolved Hide resolved
min_version = "7.0.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
key1 = "testKey1"
value1 = "abcd1234"
key2 = "testKey2"
value2 = "bcdef1234"
nonexistent_key = "nonexistent_key"
expected_response_no_min_match_len_no_with_match_len = {
"matches": [
[
[4, 7],
[5, 8],
],
[
[1, 3],
[0, 2],
],
],
"len": 7,
}
expected_response_with_min_match_len_equals_four_no_with_match_len = {
"matches": [
[
[4, 7],
[5, 8],
],
],
"len": 7,
}
expected_response_no_min_match_len_with_match_len = {
"matches": [
[
[4, 7],
[5, 8],
4,
],
[
[1, 3],
[0, 2],
3,
],
],
"len": 7,
}
expected_response_with_nonexistent_key = {
"matches": [],
"len": 0,
}
assert await redis_client.set(key1, value1) == OK
jamesx-improving marked this conversation as resolved.
Show resolved Hide resolved
assert await redis_client.set(key2, value2) == OK
assert (
await redis_client.lcs_idx(key1, key2)
== expected_response_no_min_match_len_no_with_match_len
)
assert (
await redis_client.lcs_idx(key1, key2, min_match_len=4)
== expected_response_with_min_match_len_equals_four_no_with_match_len
)
assert (
await redis_client.lcs_idx(key1, key2, with_match_len=True)
== expected_response_no_min_match_len_with_match_len
)
assert (
await redis_client.lcs_idx(key1, nonexistent_key)
== expected_response_with_nonexistent_key
)


class TestMultiKeyCommandCrossSlot:
@pytest.mark.parametrize("cluster_mode", [True])
Expand Down Expand Up @@ -7167,6 +7280,9 @@ async def test_multi_key_command_returns_cross_slot_error(
redis_client.sintercard(["def", "ghi"]),
redis_client.lmpop(["def", "ghi"], ListDirection.LEFT),
redis_client.blmpop(["def", "ghi"], ListDirection.LEFT, 1),
redis_client.lcs("abc", "def"),
redis_client.lcs_len("abc", "def"),
redis_client.lcs_idx("abc", "def"),
]
)

Expand Down
Loading
Loading