From 71855eb2270b3168ca660cd1ae266444e1a2f162 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Tue, 27 Jul 2021 16:10:39 +0300 Subject: [PATCH 1/8] add support to STRALDO command --- redis/client.py | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/redis/client.py b/redis/client.py index 160f495c1f..d7d6c75251 100755 --- a/redis/client.py +++ b/redis/client.py @@ -413,6 +413,33 @@ def parse_slowlog_get(response, **options): } for item in response] +def parse_stralgo(response, **options): + """ + Parse the response from `STRALGO`. + Without modifiers the returned value is a string. + When LEN is given the command returns the length of the result + (i.e integer). + When IDX is given the command returns an dictionary with the LCS + length and all the ranges in both the strings, start and end + offset for each string, where there are matches. + When WITHMATCHLEN is given, each array representing a match will + also have the length of the match at the beginning of the array. + """ + if options['len']: + return int(response) + if options['idx']: + if options['withmatchlen']: + matches = [[(int(match[-1]))] + list(map(tuple, match[:-1])) + for match in response[1]] + else: + matches = [list(map(tuple, match)) for match in response[1]] + return { + str_if_bytes(response[0]): matches, + str_if_bytes(response[2]): int(response[3]) + } + return str_if_bytes(response) + + def parse_cluster_info(response, **options): response = str_if_bytes(response) return dict(line.split(':') for line in response.splitlines() if line) @@ -681,6 +708,7 @@ class Redis: 'SLOWLOG GET': parse_slowlog_get, 'SLOWLOG LEN': int, 'SLOWLOG RESET': bool_ok, + 'STRALGO': parse_stralgo, 'SSCAN': parse_scan, 'TIME': lambda x: (int(x[0]), int(x[1])), 'XCLAIM': parse_xclaim, @@ -2029,6 +2057,62 @@ def setrange(self, name, offset, value): """ return self.execute_command('SETRANGE', name, offset, value) + def stralgo(self, algo, input1, input2, specific_argument=None, len=False, + idx=False, minmatchlen=None, withmatchlen=False): + """ + Implements complex algorithms that operate on strings. + Right now the only algorithm implemented is the LCS algorithm + (longest common substring). However new algorithms could be + implemented in the future. + + ``algo`` Right now must be LCS + + ``input1`` and ``input2`` Can be two strings or two keys + + ``specific_argument`` Specifying if the arguments to the algorithm + will be keys or strings. Can be only STRINGS (default) or KEYS. + + ``len`` Returns just the len of the match. + + ``idx`` Returns the match positions in each string. + + ``minmatchlen`` Restrict the list of matches to the ones of a given + minimal length. Can be provided only when ``idx`` set to True. + + ``withmatchlen`` Returns the matches with the len of the match. + Can be provided only when ``idx`` set to True. + """ + # check validity + supported_algo = ['LCS'] + if algo not in supported_algo: + raise DataError("The supported algorithms are: %s" + % (', '.join(supported_algo))) + if specific_argument: + if specific_argument not in ['KEYS', 'STRINGS']: + raise DataError("specific_argument can be only" + " KEYS or STRINGS") + else: + specific_argument = b'STRINGS' + + pieces = [algo, specific_argument, input1, input2] + if len: + if idx: + raise DataError("len and idx cannot be provided together." + " Just use idx.") + pieces.append(b'LEN') + if idx: + pieces.append(b'IDX') + if minmatchlen: + if not isinstance(minmatchlen, int): + raise DataError('minmatchlen argument must be a integer') + pieces.extend([b'MINMATCHLEN', str(minmatchlen)]) + if withmatchlen: + pieces.append(b'WITHMATCHLEN') + + return self.execute_command('STRALGO', *pieces, len=len, idx=idx, + minmatchlen=minmatchlen, + withmatchlen=withmatchlen) + def strlen(self, name): "Return the number of bytes stored in the value of ``name``" return self.execute_command('STRLEN', name) From b83820d6f16483d73007ea377e31e94091712cf8 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Tue, 27 Jul 2021 16:10:58 +0300 Subject: [PATCH 2/8] add tests --- tests/test_commands.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 3f0a82f721..9373c3cb5a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1030,6 +1030,47 @@ def test_setrange(self, r): assert r.setrange('a', 6, '12345') == 11 assert r['a'] == b'abcdef12345' + def test_stralgo_lcs(self, r): + key1 = 'key1' + key2 = 'key2' + value1 = 'ohmytext' + value2 = 'mynewtext' + res = 'mytext' + # test LCS of strings + assert r.stralgo('LCS', value1, value2) == res + # test using keys + r.mset({key1: value1, key2: value2}) + assert r.stralgo('LCS', key1, key2, specific_argument="KEYS") == res + # test other labels + assert r.stralgo('LCS', value1, value2, len=True) == len(res) + assert r.stralgo('LCS', value1, value2, idx=True) == \ + { + 'len': len(res), + 'matches': [[(4, 7), (5, 8)], [(2, 3), (0, 1)]] + } + assert r.stralgo('LCS', value1, value2, + idx=True, withmatchlen=True) == \ + { + 'len': len(res), + 'matches': [[4, (4, 7), (5, 8)], [2, (2, 3), (0, 1)]] + } + assert r.stralgo('LCS', value1, value2, + idx=True, minmatchlen=4, withmatchlen=True) == \ + { + 'len': len(res), + 'matches': [[4, (4, 7), (5, 8)]] + } + + def test_stralgo_negative(self, r): + with pytest.raises(exceptions.DataError): + r.stralgo('ISSUB', 'value1', 'value2') + with pytest.raises(exceptions.DataError): + r.stralgo('LCS', 'value1', 'value2', len=True, idx=True) + with pytest.raises(exceptions.DataError): + r.stralgo('LCS', 'value1', 'value2', specific_argument="INT") + with pytest.raises(exceptions.DataError): + r.stralgo('LCS', 'value1', 'value2', idx=True, minmatchlen=1.5) + def test_strlen(self, r): r['a'] = 'foo' assert r.strlen('a') == 3 From f6c98f6ac34529c01e7a800db58d44aa3386ef19 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Tue, 27 Jul 2021 16:18:53 +0300 Subject: [PATCH 3/8] skip if version .. --- redis/client.py | 20 ++++++++++---------- tests/test_commands.py | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/redis/client.py b/redis/client.py index d7d6c75251..d41756e9d1 100755 --- a/redis/client.py +++ b/redis/client.py @@ -415,14 +415,14 @@ def parse_slowlog_get(response, **options): def parse_stralgo(response, **options): """ - Parse the response from `STRALGO`. - Without modifiers the returned value is a string. + Parse the response from `STRALGO` command. + Without modifiers the returned value is string. When LEN is given the command returns the length of the result (i.e integer). - When IDX is given the command returns an dictionary with the LCS + When IDX is given the command returns a dictionary with the LCS length and all the ranges in both the strings, start and end offset for each string, where there are matches. - When WITHMATCHLEN is given, each array representing a match will + When WITHMATCHLEN is given, each array representing a match will also have the length of the match at the beginning of the array. """ if options['len']: @@ -2057,7 +2057,7 @@ def setrange(self, name, offset, value): """ return self.execute_command('SETRANGE', name, offset, value) - def stralgo(self, algo, input1, input2, specific_argument=None, len=False, + def stralgo(self, algo, value1, value2, specific_argument=None, len=False, idx=False, minmatchlen=None, withmatchlen=False): """ Implements complex algorithms that operate on strings. @@ -2067,10 +2067,10 @@ def stralgo(self, algo, input1, input2, specific_argument=None, len=False, ``algo`` Right now must be LCS - ``input1`` and ``input2`` Can be two strings or two keys + ``value1`` and ``value2`` Can be two strings or two keys ``specific_argument`` Specifying if the arguments to the algorithm - will be keys or strings. Can be only STRINGS (default) or KEYS. + will be keys or strings. strings is the default. ``len`` Returns just the len of the match. @@ -2088,13 +2088,13 @@ def stralgo(self, algo, input1, input2, specific_argument=None, len=False, raise DataError("The supported algorithms are: %s" % (', '.join(supported_algo))) if specific_argument: - if specific_argument not in ['KEYS', 'STRINGS']: + if specific_argument not in ['keys', 'strings']: raise DataError("specific_argument can be only" - " KEYS or STRINGS") + " keys or strings") else: specific_argument = b'STRINGS' - pieces = [algo, specific_argument, input1, input2] + pieces = [algo, specific_argument.upper(), value1, value2] if len: if idx: raise DataError("len and idx cannot be provided together." diff --git a/tests/test_commands.py b/tests/test_commands.py index 9373c3cb5a..3613bf89c7 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1030,6 +1030,7 @@ def test_setrange(self, r): assert r.setrange('a', 6, '12345') == 11 assert r['a'] == b'abcdef12345' + @skip_if_server_version_lt('6.0.0') def test_stralgo_lcs(self, r): key1 = 'key1' key2 = 'key2' @@ -1061,6 +1062,7 @@ def test_stralgo_lcs(self, r): 'matches': [[4, (4, 7), (5, 8)]] } + @skip_if_server_version_lt('6.0.0') def test_stralgo_negative(self, r): with pytest.raises(exceptions.DataError): r.stralgo('ISSUB', 'value1', 'value2') From cadf6a1bada938541657294924c17e5aaadbfde6 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Tue, 27 Jul 2021 16:19:58 +0300 Subject: [PATCH 4/8] new line --- redis/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redis/client.py b/redis/client.py index d41756e9d1..205ee9459c 100755 --- a/redis/client.py +++ b/redis/client.py @@ -432,7 +432,8 @@ def parse_stralgo(response, **options): matches = [[(int(match[-1]))] + list(map(tuple, match[:-1])) for match in response[1]] else: - matches = [list(map(tuple, match)) for match in response[1]] + matches = [list(map(tuple, match)) + for match in response[1]] return { str_if_bytes(response[0]): matches, str_if_bytes(response[2]): int(response[3]) From d57146ad6b346d1023f69926dd00742d8f9d9615 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Tue, 27 Jul 2021 16:32:08 +0300 Subject: [PATCH 5/8] lower case --- tests/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 3613bf89c7..5d8b645150 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1041,7 +1041,7 @@ def test_stralgo_lcs(self, r): assert r.stralgo('LCS', value1, value2) == res # test using keys r.mset({key1: value1, key2: value2}) - assert r.stralgo('LCS', key1, key2, specific_argument="KEYS") == res + assert r.stralgo('LCS', key1, key2, specific_argument="keys") == res # test other labels assert r.stralgo('LCS', value1, value2, len=True) == len(res) assert r.stralgo('LCS', value1, value2, idx=True) == \ From 43b7f784706d05bc79071d085d6b7268f739ff92 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Thu, 29 Jul 2021 18:40:31 +0300 Subject: [PATCH 6/8] fix comments --- redis/client.py | 27 ++++++++++++--------------- tests/test_commands.py | 4 ++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/redis/client.py b/redis/client.py index 205ee9459c..681f0f35c7 100755 --- a/redis/client.py +++ b/redis/client.py @@ -2058,8 +2058,8 @@ def setrange(self, name, offset, value): """ return self.execute_command('SETRANGE', name, offset, value) - def stralgo(self, algo, value1, value2, specific_argument=None, len=False, - idx=False, minmatchlen=None, withmatchlen=False): + def stralgo(self, algo, value1, value2, specific_argument='strings', + len=False, idx=False, minmatchlen=None, withmatchlen=False): """ Implements complex algorithms that operate on strings. Right now the only algorithm implemented is the LCS algorithm @@ -2088,25 +2088,22 @@ def stralgo(self, algo, value1, value2, specific_argument=None, len=False, if algo not in supported_algo: raise DataError("The supported algorithms are: %s" % (', '.join(supported_algo))) - if specific_argument: - if specific_argument not in ['keys', 'strings']: - raise DataError("specific_argument can be only" - " keys or strings") - else: - specific_argument = b'STRINGS' + if specific_argument not in ['keys', 'strings']: + raise DataError("specific_argument can be only" + " keys or strings") + if len and idx: + raise DataError("len and idx cannot be provided together.") pieces = [algo, specific_argument.upper(), value1, value2] if len: - if idx: - raise DataError("len and idx cannot be provided together." - " Just use idx.") pieces.append(b'LEN') if idx: pieces.append(b'IDX') - if minmatchlen: - if not isinstance(minmatchlen, int): - raise DataError('minmatchlen argument must be a integer') - pieces.extend([b'MINMATCHLEN', str(minmatchlen)]) + try: + int(minmatchlen) + pieces.extend([b'MINMATCHLEN', minmatchlen]) + except TypeError: + pass if withmatchlen: pieces.append(b'WITHMATCHLEN') diff --git a/tests/test_commands.py b/tests/test_commands.py index 5d8b645150..d5fb69bd62 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1070,8 +1070,8 @@ def test_stralgo_negative(self, r): r.stralgo('LCS', 'value1', 'value2', len=True, idx=True) with pytest.raises(exceptions.DataError): r.stralgo('LCS', 'value1', 'value2', specific_argument="INT") - with pytest.raises(exceptions.DataError): - r.stralgo('LCS', 'value1', 'value2', idx=True, minmatchlen=1.5) + with pytest.raises(ValueError): + r.stralgo('LCS', 'value1', 'value2', idx=True, minmatchlen="one") def test_strlen(self, r): r['a'] = 'foo' From 8ca1c39c1dbd0185697b301464ca0df1b0582a56 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 18 Aug 2021 12:10:15 +0300 Subject: [PATCH 7/8] callback --- redis/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/redis/client.py b/redis/client.py index cc0009145e..eda2038da7 100755 --- a/redis/client.py +++ b/redis/client.py @@ -695,6 +695,7 @@ class Redis(Commands, object): 'MODULE LIST': lambda r: [pairs_to_dict(m) for m in r], 'OBJECT': parse_object, 'PING': lambda r: str_if_bytes(r) == 'PONG', + 'STRALGO': parse_stralgo, 'PUBSUB NUMSUB': parse_pubsub_numsub, 'RANDOMKEY': lambda r: r and r or None, 'SCAN': parse_scan, From b2174ba823c80194593a6a53391fea4290995922 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Mon, 23 Aug 2021 17:43:05 +0300 Subject: [PATCH 8/8] change to get --- redis/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redis/client.py b/redis/client.py index bbd52d1663..0aef6740dc 100755 --- a/redis/client.py +++ b/redis/client.py @@ -417,10 +417,10 @@ def parse_stralgo(response, **options): When WITHMATCHLEN is given, each array representing a match will also have the length of the match at the beginning of the array. """ - if options['len']: + if options.get('len', False): return int(response) - if options['idx']: - if options['withmatchlen']: + if options.get('idx', False): + if options.get('withmatchlen', False): matches = [[(int(match[-1]))] + list(map(tuple, match[:-1])) for match in response[1]] else: