From 7f16a123196b47fd263ddd2f69d4bab167935ce0 Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Mon, 29 Nov 2021 12:13:32 +0200 Subject: [PATCH 1/9] Added support for ACL commands to RedisCluster --- redis/cluster.py | 12 ++ redis/commands/cluster.py | 309 +++++++++++++++++++++++++++++++++++++- tests/test_commands.py | 12 -- 3 files changed, 320 insertions(+), 13 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 91a4d558a2..4e5ee860ba 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -220,6 +220,18 @@ class RedisCluster(ClusterCommands, object): COMMAND_FLAGS = dict_merge( list_keys_to_dict( [ + "ACL CAT", + "ACL DELUSER", + "ACL GENPASS", + "ACL GETUSER", + "ACL HELP", + "ACL LIST", + "ACL LOG", + "ACL LOAD", + "ACL SAVE", + "ACL SETUSER", + "ACL USERS", + "ACL WHOAMI", "CLIENT LIST", "CLIENT SETNAME", "CLIENT GETNAME", diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 6c7740d5e9..7334f93cee 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -8,6 +8,312 @@ from .helpers import list_or_args +class ClusterACLCommands: + """ + Redis Access Control List (ACL) commands for RedisCluster. + see: https://redis.io/topics/acl + + Commands can be executed on specified nodes using the target_nodes argument + By default, if target_nodes is not specified, the command will be + executed on the default cluster node. + + :param :target_nodes: type can be one of the followings: + - nodes flag: 'all', 'primaries', 'replicas', 'random' + - 'ClusterNode' + - 'list(ClusterNodes)' + - 'dict(any:clusterNodes)' + + for example: + # Set user 'virginia' on the cluster's default node + r.acl_setuser('virginia') + # Set user 'virginia' on all primaries + r.acl_setuser('virginia', target_nodes='primaries') + + """ + def acl_cat(self, category=None, target_nodes=None): + """ + Returns a list of categories or commands within a category. + + If ``category`` is not supplied, returns a list of all categories. + If ``category`` is supplied, returns a list of all commands within + that category. + + For more information check https://redis.io/commands/acl-cat + """ + pieces = [category] if category else [] + return self.execute_command('ACL CAT', *pieces, + target_nodes=target_nodes) + + def acl_deluser(self, *username, target_nodes=None): + """ + Delete the ACL for the specified ``username``s + + For more information check https://redis.io/commands/acl-deluser + """ + return self.execute_command('ACL DELUSER', *username, + target_nodes=None) + + def acl_genpass(self, bits=None, target_nodes=None): + """Generate a random password value. + If ``bits`` is supplied then use this number of bits, rounded to + the next multiple of 4. + See: https://redis.io/commands/acl-genpass + """ + pieces = [] + if bits is not None: + try: + b = int(bits) + if b < 0 or b > 4096: + raise ValueError + except ValueError: + raise DataError('genpass optionally accepts a bits argument, ' + 'between 0 and 4096.') + return self.execute_command('ACL GENPASS', *pieces, + target_nodes=target_nodes) + + def acl_getuser(self, username, target_nodes=None): + """ + Get the ACL details for the specified ``username``. + + If ``username`` does not exist, return None + + For more information check https://redis.io/commands/acl-getuser + """ + return self.execute_command('ACL GETUSER', username, + target_nodes=target_nodes) + + def acl_help(self, target_nodes=None): + """The ACL HELP command returns helpful text describing + the different subcommands. + + For more information check https://redis.io/commands/acl-help + """ + return self.execute_command('ACL HELP', target_nodes=target_nodes) + + def acl_list(self, target_nodes=None): + """ + Return a list of all ACLs on the server + + For more information check https://redis.io/commands/acl-list + """ + return self.execute_command('ACL LIST', target_nodes) + + def acl_log(self, count=None, target_nodes=None): + """ + Get ACL logs as a list. + :param int count: Get logs[0:count]. + :rtype: List. + + For more information check https://redis.io/commands/acl-log + """ + args = [] + if count is not None: + if not isinstance(count, int): + raise DataError('ACL LOG count must be an ' + 'integer') + args.append(count) + + return self.execute_command('ACL LOG', *args, + target_nodes=target_nodes) + + def acl_log_reset(self, target_nodes=None): + """ + Reset ACL logs. + :rtype: Boolean. + + For more information check https://redis.io/commands/acl-log + """ + args = [b'RESET'] + return self.execute_command('ACL LOG', *args, + target_nodes=target_nodes) + + def acl_load(self, target_nodes=None): + """ + Load ACL rules from the configured ``aclfile``. + + Note that the server must be configured with the ``aclfile`` + directive to be able to load ACL rules from an aclfile. + + For more information check https://redis.io/commands/acl-load + """ + return self.execute_command('ACL LOAD', target_nodes=target_nodes) + + def acl_save(self, target_nodes=None): + """ + Save ACL rules to the configured ``aclfile``. + + Note that the server must be configured with the ``aclfile`` + directive to be able to save ACL rules to an aclfile. + + For more information check https://redis.io/commands/acl-save + """ + return self.execute_command('ACL SAVE', target_nodes=target_nodes) + + def acl_setuser(self, username, enabled=False, nopass=False, + passwords=None, hashed_passwords=None, categories=None, + commands=None, keys=None, reset=False, reset_keys=False, + reset_passwords=False, target_nodes=None): + """ + Create or update an ACL user. + + Create or update the ACL for ``username``. If the user already exists, + the existing ACL is completely overwritten and replaced with the + specified values. + + ``enabled`` is a boolean indicating whether the user should be allowed + to authenticate or not. Defaults to ``False``. + + ``nopass`` is a boolean indicating whether the can authenticate without + a password. This cannot be True if ``passwords`` are also specified. + + ``passwords`` if specified is a list of plain text passwords + to add to or remove from the user. Each password must be prefixed with + a '+' to add or a '-' to remove. For convenience, the value of + ``passwords`` can be a simple prefixed string when adding or + removing a single password. + + ``hashed_passwords`` if specified is a list of SHA-256 hashed passwords + to add to or remove from the user. Each hashed password must be + prefixed with a '+' to add or a '-' to remove. For convenience, + the value of ``hashed_passwords`` can be a simple prefixed string when + adding or removing a single password. + + ``categories`` if specified is a list of strings representing category + permissions. Each string must be prefixed with either a '+' to add the + category permission or a '-' to remove the category permission. + + ``commands`` if specified is a list of strings representing command + permissions. Each string must be prefixed with either a '+' to add the + command permission or a '-' to remove the command permission. + + ``keys`` if specified is a list of key patterns to grant the user + access to. Keys patterns allow '*' to support wildcard matching. For + example, '*' grants access to all keys while 'cache:*' grants access + to all keys that are prefixed with 'cache:'. ``keys`` should not be + prefixed with a '~'. + + ``reset`` is a boolean indicating whether the user should be fully + reset prior to applying the new ACL. Setting this to True will + remove all existing passwords, flags and privileges from the user and + then apply the specified rules. If this is False, the user's existing + passwords, flags and privileges will be kept and any new specified + rules will be applied on top. + + ``reset_keys`` is a boolean indicating whether the user's key + permissions should be reset prior to applying any new key permissions + specified in ``keys``. If this is False, the user's existing + key permissions will be kept and any new specified key permissions + will be applied on top. + + ``reset_passwords`` is a boolean indicating whether to remove all + existing passwords and the 'nopass' flag from the user prior to + applying any new passwords specified in 'passwords' or + 'hashed_passwords'. If this is False, the user's existing passwords + and 'nopass' status will be kept and any new specified passwords + or hashed_passwords will be applied on top. + + For more information check https://redis.io/commands/acl-setuser + """ + encoder = self.connection_pool.get_encoder() + pieces = [username] + + if reset: + pieces.append(b'reset') + + if reset_keys: + pieces.append(b'resetkeys') + + if reset_passwords: + pieces.append(b'resetpass') + + if enabled: + pieces.append(b'on') + else: + pieces.append(b'off') + + if (passwords or hashed_passwords) and nopass: + raise DataError('Cannot set \'nopass\' and supply ' + '\'passwords\' or \'hashed_passwords\'') + + if passwords: + # as most users will have only one password, allow remove_passwords + # to be specified as a simple string or a list + passwords = list_or_args(passwords, []) + for i, password in enumerate(passwords): + password = encoder.encode(password) + if password.startswith(b'+'): + pieces.append(b'>%s' % password[1:]) + elif password.startswith(b'-'): + pieces.append(b'<%s' % password[1:]) + else: + raise DataError('Password %d must be prefixeed with a ' + '"+" to add or a "-" to remove' % i) + + if hashed_passwords: + # as most users will have only one password, allow remove_passwords + # to be specified as a simple string or a list + hashed_passwords = list_or_args(hashed_passwords, []) + for i, hashed_password in enumerate(hashed_passwords): + hashed_password = encoder.encode(hashed_password) + if hashed_password.startswith(b'+'): + pieces.append(b'#%s' % hashed_password[1:]) + elif hashed_password.startswith(b'-'): + pieces.append(b'!%s' % hashed_password[1:]) + else: + raise DataError('Hashed %d password must be prefixeed ' + 'with a "+" to add or a "-" to remove' % i) + + if nopass: + pieces.append(b'nopass') + + if categories: + for category in categories: + category = encoder.encode(category) + # categories can be prefixed with one of (+@, +, -@, -) + if category.startswith(b'+@'): + pieces.append(category) + elif category.startswith(b'+'): + pieces.append(b'+@%s' % category[1:]) + elif category.startswith(b'-@'): + pieces.append(category) + elif category.startswith(b'-'): + pieces.append(b'-@%s' % category[1:]) + else: + raise DataError('Category "%s" must be prefixed with ' + '"+" or "-"' + % encoder.decode(category, force=True)) + if commands: + for cmd in commands: + cmd = encoder.encode(cmd) + if not cmd.startswith(b'+') and not cmd.startswith(b'-'): + raise DataError('Command "%s" must be prefixed with ' + '"+" or "-"' + % encoder.decode(cmd, force=True)) + pieces.append(cmd) + + if keys: + for key in keys: + key = encoder.encode(key) + pieces.append(b'~%s' % key) + + return self.execute_command('ACL SETUSER', *pieces, + target_nodes=target_nodes) + + def acl_users(self, target_nodes=None): + """Returns a list of all registered users on the server. + + For more information check https://redis.io/commands/acl-users + """ + return self.execute_command('ACL USERS', target_nodes=target_nodes) + + def acl_whoami(self, target_nodes=None): + """Get the username for the current connection + + For more information check https://redis.io/commands/acl-whoami + """ + return self.execute_command('ACL WHOAMI', target_nodes=target_nodes) + + class ClusterMultiKeyCommands: """ A class containing commands that handle more than one key @@ -724,7 +1030,8 @@ def pubsub_numsub(self, *args, target_nodes=None): class ClusterCommands(ClusterManagementCommands, ClusterMultiKeyCommands, - ClusterPubSubCommands, DataAccessCommands): + ClusterACLCommands, ClusterPubSubCommands, + DataAccessCommands): """ Redis Cluster commands diff --git a/tests/test_commands.py b/tests/test_commands.py index f526ae5dd6..3ebb6eea3a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -69,14 +69,12 @@ def test_command_on_invalid_key_type(self, r): r['a'] # SERVER INFORMATION - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") def test_acl_cat_no_category(self, r): categories = r.acl_cat() assert isinstance(categories, list) assert 'read' in categories - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") def test_acl_cat_with_category(self, r): commands = r.acl_cat('read') @@ -109,7 +107,6 @@ def teardown(): assert r.acl_getuser(users[3]) is None assert r.acl_getuser(users[4]) is None - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_genpass(self, r): @@ -124,7 +121,6 @@ def test_acl_genpass(self, r): r.acl_genpass(555) assert isinstance(password, str) - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_getuser_setuser(self, r, request): @@ -213,14 +209,12 @@ def teardown(): hashed_passwords=['-' + hashed_password]) assert len(r.acl_getuser(username)['passwords']) == 1 - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") def test_acl_help(self, r): res = r.acl_help() assert isinstance(res, list) assert len(res) != 0 - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_list(self, r, request): @@ -234,7 +228,6 @@ def teardown(): users = r.acl_list() assert len(users) == 2 - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_log(self, r, request): @@ -271,7 +264,6 @@ def teardown(): assert 'client-info' in r.acl_log(count=1)[0] assert r.acl_log_reset() - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_setuser_categories_without_prefix_fails(self, r, request): @@ -284,7 +276,6 @@ def teardown(): with pytest.raises(exceptions.DataError): r.acl_setuser(username, categories=['list']) - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_setuser_commands_without_prefix_fails(self, r, request): @@ -297,7 +288,6 @@ def teardown(): with pytest.raises(exceptions.DataError): r.acl_setuser(username, commands=['get']) - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise def test_acl_setuser_add_passwords_and_nopass_fails(self, r, request): @@ -310,14 +300,12 @@ def teardown(): with pytest.raises(exceptions.DataError): r.acl_setuser(username, passwords='+mypass', nopass=True) - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") def test_acl_users(self, r): users = r.acl_users() assert isinstance(users, list) assert len(users) > 0 - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") def test_acl_whoami(self, r): username = r.acl_whoami() From 88a6ded781bee9e8820941600dcc9e7373bbb889 Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Mon, 29 Nov 2021 14:22:51 +0200 Subject: [PATCH 2/9] Added support for Monitor command to RedisCluster --- redis/cluster.py | 17 ++++++++++++++ tests/conftest.py | 16 +++++++------ tests/test_cluster.py | 54 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 4e5ee860ba..345074fc8d 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -646,6 +646,23 @@ def set_default_node(self, node): log.info("Changed the default cluster node to {0}".format(node)) return True + def monitor(self, target_node=None): + """ + Returns a Monitor object for the specified target node. + The default cluster node will be selected if no target node was + specified. + Monitor is useful for handling the MONITOR command to the redis server. + next_command() method returns one command from monitor + listen() method yields commands from monitor. + """ + if target_node is None: + target_node = self.get_default_node() + if target_node.redis_connection is None: + raise RedisClusterException( + "Cluster Node {0} has no redis_connection". + format(target_node.name)) + return target_node.redis_connection.monitor() + def pubsub(self, node=None, host=None, port=None, **kwargs): """ Allows passing a ClusterNode, or host&port, to get a pubsub instance diff --git a/tests/conftest.py b/tests/conftest.py index ddc0834037..48d457def5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -312,16 +312,18 @@ def master_host(request): yield parts.hostname, parts.port -def wait_for_command(client, monitor, command): +def wait_for_command(client, monitor, command, key=None): # issue a command with a key name that's local to this process. # if we find a command with our key before the command we're waiting # for, something went wrong - redis_version = REDIS_INFO["version"] - if LooseVersion(redis_version) >= LooseVersion('5.0.0'): - id_str = str(client.client_id()) - else: - id_str = '%08x' % random.randrange(2**32) - key = '__REDIS-PY-%s__' % id_str + if key is None: + # generate key + redis_version = REDIS_INFO["version"] + if LooseVersion(redis_version) >= LooseVersion('5.0.0'): + id_str = str(client.client_id()) + else: + id_str = '%08x' % random.randrange(2**32) + key = '__REDIS-PY-%s__' % id_str client.get(key) while True: monitor_response = monitor.next_command() diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 071cb7d2f1..804ff005dd 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -25,7 +25,8 @@ from .conftest import ( _get_client, skip_if_server_version_lt, - skip_unless_arch_bits + skip_unless_arch_bits, + wait_for_command ) default_host = "127.0.0.1" @@ -2480,3 +2481,54 @@ def test_readonly_pipeline_from_readonly_client(self, request): if executed_on_replica: break assert executed_on_replica is True + + +@pytest.mark.onlycluster +class TestClusterMonitor: + def test_wait_command_not_found(self, r): + "Make sure the wait_for_command func works when command is not found" + key = 'foo' + node = r.get_node_from_key(key) + with r.monitor(target_node=node) as m: + response = wait_for_command(r, m, 'nothing', key=key) + assert response is None + + def test_response_values(self, r): + db = 0 + key = 'foo' + node = r.get_node_from_key(key) + with r.monitor(target_node=node) as m: + r.ping(target_nodes=node) + response = wait_for_command(r, m, 'PING', key=key) + assert isinstance(response['time'], float) + assert response['db'] == db + assert response['client_type'] in ('tcp', 'unix') + assert isinstance(response['client_address'], str) + assert isinstance(response['client_port'], str) + assert response['command'] == 'PING' + + def test_command_with_quoted_key(self, r): + key = '{foo}1' + node = r.get_node_from_key(key) + with r.monitor(node) as m: + r.get('{foo}"bar') + response = wait_for_command(r, m, 'GET {foo}"bar', key=key) + assert response['command'] == 'GET {foo}"bar' + + def test_command_with_binary_data(self, r): + key = '{foo}1' + node = r.get_node_from_key(key) + with r.monitor(target_node=node) as m: + byte_string = b'{foo}bar\x92' + r.get(byte_string) + response = wait_for_command(r, m, 'GET {foo}bar\\x92', key=key) + assert response['command'] == 'GET {foo}bar\\x92' + + def test_command_with_escaped_data(self, r): + key = '{foo}1' + node = r.get_node_from_key(key) + with r.monitor(target_node=node) as m: + byte_string = b'{foo}bar\\x92' + r.get(byte_string) + response = wait_for_command(r, m, 'GET {foo}bar\\\\x92', key=key) + assert response['command'] == 'GET {foo}bar\\\\x92' From f5aeb049b7de728620ec1c9ebaaed899b6b10c4c Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Mon, 29 Nov 2021 16:42:56 +0200 Subject: [PATCH 3/9] Fixed linter test --- redis/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis/cluster.py b/redis/cluster.py index 345074fc8d..df8913e36f 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -660,7 +660,7 @@ def monitor(self, target_node=None): if target_node.redis_connection is None: raise RedisClusterException( "Cluster Node {0} has no redis_connection". - format(target_node.name)) + format(target_node.name)) return target_node.redis_connection.monitor() def pubsub(self, node=None, host=None, port=None, **kwargs): From 39f42d170fecc60693dbb78ba0132fa4f5957805 Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Mon, 29 Nov 2021 18:17:34 +0200 Subject: [PATCH 4/9] Fixed @skip_if_redis_enterprise fixture. Issue #1758 --- redis/cluster.py | 1 + redis/commands/cluster.py | 6 ++-- tests/conftest.py | 4 +-- tests/test_cluster.py | 41 +++++++++++++++++++++++++ tests/test_commands.py | 57 ++++++++++++++++++----------------- tests/test_connection_pool.py | 12 ++++---- tests/test_monitor.py | 4 +-- tests/test_pubsub.py | 2 +- 8 files changed, 85 insertions(+), 42 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index df8913e36f..19f7313380 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -100,6 +100,7 @@ def fix_server(*args): "charset", "connection_class", "connection_pool", + "client_name", "db", "decode_responses", "encoding", diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 7334f93cee..fd0da64ba2 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -51,7 +51,7 @@ def acl_deluser(self, *username, target_nodes=None): For more information check https://redis.io/commands/acl-deluser """ return self.execute_command('ACL DELUSER', *username, - target_nodes=None) + target_nodes=target_nodes) def acl_genpass(self, bits=None, target_nodes=None): """Generate a random password value. @@ -96,7 +96,7 @@ def acl_list(self, target_nodes=None): For more information check https://redis.io/commands/acl-list """ - return self.execute_command('ACL LIST', target_nodes) + return self.execute_command('ACL LIST', target_nodes=target_nodes) def acl_log(self, count=None, target_nodes=None): """ @@ -214,7 +214,7 @@ def acl_setuser(self, username, enabled=False, nopass=False, For more information check https://redis.io/commands/acl-setuser """ - encoder = self.connection_pool.get_encoder() + encoder = self.encoder pieces = [username] if reset: diff --git a/tests/conftest.py b/tests/conftest.py index 48d457def5..e18e0e5f5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -147,12 +147,12 @@ def skip_ifmodversion_lt(min_version: str, module_name: str): raise AttributeError("No redis module named {}".format(module_name)) -def skip_if_redis_enterprise(func): +def skip_if_redis_enterprise(): check = REDIS_INFO["enterprise"] is True return pytest.mark.skipif(check, reason="Redis enterprise") -def skip_ifnot_redis_enterprise(func): +def skip_ifnot_redis_enterprise(): check = REDIS_INFO["enterprise"] is False return pytest.mark.skipif(check, reason="Not running in redis enterprise") diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 804ff005dd..b2d32981a9 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -17,6 +17,7 @@ ClusterDownError, DataError, MovedError, + NoPermissionError, RedisClusterException, RedisError ) @@ -24,6 +25,7 @@ from redis.crc import key_slot from .conftest import ( _get_client, + skip_if_redis_enterprise, skip_if_server_version_lt, skip_unless_arch_bits, wait_for_command @@ -1688,6 +1690,45 @@ def test_cluster_randomkey(self, r): assert r.randomkey(target_nodes=node) in \ (b'{foo}a', b'{foo}b', b'{foo}c') + @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise() + def test_acl_log(self, r, request): + key = '{cache}:' + node = r.get_node_from_key(key) + username = 'redis-py-user' + + def teardown(): + r.acl_deluser(username, target_nodes='primaries') + + request.addfinalizer(teardown) + r.acl_setuser(username, enabled=True, reset=True, + commands=['+get', '+set', '+select', '+cluster', + '+command'], keys=['{cache}:*'], nopass=True, + target_nodes='primaries') + r.acl_log_reset(target_nodes=node) + + user_client = _get_client(RedisCluster, request, flushdb=False, + username=username) + + # Valid operation and key + assert user_client.set('{cache}:0', 1) + assert user_client.get('{cache}:0') == b'1' + + # Invalid key + with pytest.raises(NoPermissionError): + user_client.get('{cache}violated_cache:0') + + # Invalid operation + with pytest.raises(NoPermissionError): + user_client.hset('{cache}:0', 'hkey', 'hval') + + assert isinstance(r.acl_log(target_nodes=node), list) + assert len(r.acl_log(target_nodes=node)) == 2 + assert len(r.acl_log(count=1, target_nodes=node)) == 1 + assert isinstance(r.acl_log(target_nodes=node)[0], dict) + assert 'client-info' in r.acl_log(count=1, target_nodes=node)[0] + assert r.acl_log_reset(target_nodes=node) + @pytest.mark.onlycluster class TestNodesManager: diff --git a/tests/test_commands.py b/tests/test_commands.py index 3ebb6eea3a..69f59e7d63 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -81,9 +81,8 @@ def test_acl_cat_with_category(self, r): assert isinstance(commands, list) assert 'get' in commands - @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_deluser(self, r, request): username = 'redis-py-user' @@ -108,7 +107,7 @@ def teardown(): assert r.acl_getuser(users[4]) is None @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_genpass(self, r): password = r.acl_genpass() assert isinstance(password, str) @@ -122,7 +121,7 @@ def test_acl_genpass(self, r): assert isinstance(password, str) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_getuser_setuser(self, r, request): username = 'redis-py-user' @@ -216,7 +215,7 @@ def test_acl_help(self, r): assert len(res) != 0 @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_list(self, r, request): username = 'redis-py-user' @@ -229,7 +228,8 @@ def teardown(): assert len(users) == 2 @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() + @pytest.mark.onlynoncluster def test_acl_log(self, r, request): username = 'redis-py-user' @@ -265,7 +265,7 @@ def teardown(): assert r.acl_log_reset() @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_setuser_categories_without_prefix_fails(self, r, request): username = 'redis-py-user' @@ -277,7 +277,7 @@ def teardown(): r.acl_setuser(username, categories=['list']) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_setuser_commands_without_prefix_fails(self, r, request): username = 'redis-py-user' @@ -289,7 +289,7 @@ def teardown(): r.acl_setuser(username, commands=['get']) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_acl_setuser_add_passwords_and_nopass_fails(self, r, request): username = 'redis-py-user' @@ -333,7 +333,7 @@ def test_client_list_types_not_replica(self, r): clients = r.client_list(_type=client_type) assert isinstance(clients, list) - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_client_list_replica(self, r): clients = r.client_list(_type='replica') assert isinstance(clients, list) @@ -482,11 +482,12 @@ def test_client_kill_filter_by_laddr(self, r, r2): assert r.client_kill_filter(laddr=client_2_addr) @skip_if_server_version_lt('6.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_client_kill_filter_by_user(self, r, request): killuser = 'user_to_kill' r.acl_setuser(killuser, enabled=True, reset=True, - commands=['+get', '+set', '+select'], + commands=['+get', '+set', '+select', '+cluster', + '+command'], keys=['cache:*'], nopass=True) _get_client(redis.Redis, request, flushdb=False, username=killuser) r.client_kill_filter(user=killuser) @@ -497,7 +498,7 @@ def test_client_kill_filter_by_user(self, r, request): @pytest.mark.onlynoncluster @skip_if_server_version_lt('2.9.50') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_client_pause(self, r): assert r.client_pause(1) assert r.client_pause(timeout=1) @@ -506,7 +507,7 @@ def test_client_pause(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('6.2.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_client_unpause(self, r): assert r.client_unpause() == b'OK' @@ -526,7 +527,7 @@ def test_client_reply(self, r, r_timeout): @pytest.mark.onlynoncluster @skip_if_server_version_lt('6.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_client_getredir(self, r): assert isinstance(r.client_getredir(), int) assert r.client_getredir() == -1 @@ -538,7 +539,7 @@ def test_config_get(self, r): # assert data['maxmemory'].isdigit() @pytest.mark.onlynoncluster - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_config_resetstat(self, r): r.ping() prior_commands_processed = int(r.info()['total_commands_processed']) @@ -547,7 +548,7 @@ def test_config_resetstat(self, r): reset_commands_processed = int(r.info()['total_commands_processed']) assert reset_commands_processed < prior_commands_processed - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_config_set(self, r): r.config_set('timeout', 70) assert r.config_get()['timeout'] == '70' @@ -574,7 +575,7 @@ def test_info(self, r): assert 'redis_version' in info.keys() @pytest.mark.onlynoncluster - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_lastsave(self, r): assert isinstance(r.lastsave(), datetime.datetime) @@ -674,7 +675,7 @@ def test_time(self, r): assert isinstance(t[0], int) assert isinstance(t[1], int) - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_bgsave(self, r): assert r.bgsave() time.sleep(0.3) @@ -1256,7 +1257,7 @@ def test_stralgo_lcs(self, r): value2 = 'mynewtext' res = 'mytext' - if skip_if_redis_enterprise(None).args[0] is True: + if skip_if_redis_enterprise().args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.stralgo('LCS', value1, value2) == res return @@ -1304,7 +1305,7 @@ def test_strlen(self, r): def test_substr(self, r): r['a'] = '0123456789' - if skip_if_redis_enterprise(None).args[0] is True: + if skip_if_redis_enterprise().args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.substr('a', 0) == b'0123456789' return @@ -2572,7 +2573,7 @@ def test_cluster_slaves(self, mock_cluster_resp_slaves): @pytest.mark.onlynoncluster @skip_if_server_version_lt('3.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_readwrite(self, r): assert r.readwrite() @@ -3741,7 +3742,7 @@ def test_memory_doctor(self, r): @skip_if_server_version_lt('4.0.0') def test_memory_malloc_stats(self, r): - if skip_if_redis_enterprise(None).args[0] is True: + if skip_if_redis_enterprise().args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.memory_malloc_stats() return @@ -3754,7 +3755,7 @@ def test_memory_stats(self, r): # has data r.set('foo', 'bar') - if skip_if_redis_enterprise(None).args[0] is True: + if skip_if_redis_enterprise().args[0] is True: with pytest.raises(redis.exceptions.ResponseError): stats = r.memory_stats() return @@ -3772,7 +3773,7 @@ def test_memory_usage(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('4.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_module_list(self, r): assert isinstance(r.module_list(), list) for x in r.module_list(): @@ -3804,7 +3805,7 @@ def test_command(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('4.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_module(self, r): with pytest.raises(redis.exceptions.ModuleError) as excinfo: r.module_load('/some/fake/path') @@ -3860,7 +3861,7 @@ def test_restore_frequency(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('5.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_replicaof(self, r): with pytest.raises(redis.ResponseError): assert r.replicaof("NO ONE") @@ -3938,7 +3939,7 @@ def test_22_info(self, r): assert '6' in parsed['allocation_stats'] assert '>=256' in parsed['allocation_stats'] - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_large_responses(self, r): "The PythonParser has some special cases for return values > 1MB" # load up 5MB of data into a key diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 288d43dfd7..99e060ce20 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -486,7 +486,7 @@ def test_on_connect_error(self): @pytest.mark.onlynoncluster @skip_if_server_version_lt('2.8.8') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_busy_loading_disconnects_socket(self, r): """ If Redis raises a LOADING error, the connection should be @@ -498,7 +498,7 @@ def test_busy_loading_disconnects_socket(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('2.8.8') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_busy_loading_from_pipeline_immediate_command(self, r): """ BusyLoadingErrors should raise from Pipelines that execute a @@ -515,7 +515,7 @@ def test_busy_loading_from_pipeline_immediate_command(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt('2.8.8') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_busy_loading_from_pipeline(self, r): """ BusyLoadingErrors should be raised from a pipeline execution @@ -531,7 +531,7 @@ def test_busy_loading_from_pipeline(self, r): assert not pool._available_connections[0]._sock @skip_if_server_version_lt('2.8.8') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_read_only_error(self, r): "READONLY errors get turned in ReadOnlyError exceptions" with pytest.raises(redis.ReadOnlyError): @@ -557,7 +557,7 @@ def test_connect_from_url_unix(self): 'path=/path/to/socket,db=0', ) - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_connect_no_auth_supplied_when_required(self, r): """ AuthenticationError should be raised when the server requires a @@ -567,7 +567,7 @@ def test_connect_no_auth_supplied_when_required(self, r): r.execute_command('DEBUG', 'ERROR', 'ERR Client sent AUTH, but no password is set') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_connect_invalid_password_supplied(self, r): "AuthenticationError should be raised when sending the wrong password" with pytest.raises(redis.AuthenticationError): diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 6c3ea33bce..ac366cd996 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -46,7 +46,7 @@ def test_command_with_escaped_data(self, r): response = wait_for_command(r, m, 'GET foo\\\\x92') assert response['command'] == 'GET foo\\\\x92' - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_lua_script(self, r): with r.monitor() as m: script = 'return redis.call("GET", "foo")' @@ -57,7 +57,7 @@ def test_lua_script(self, r): assert response['client_address'] == 'lua' assert response['client_port'] == '' - @skip_ifnot_redis_enterprise + @skip_ifnot_redis_enterprise() def test_lua_script_in_enterprise(self, r): with r.monitor() as m: script = 'return redis.call("GET", "foo")' diff --git a/tests/test_pubsub.py b/tests/test_pubsub.py index 95513a09a8..a3ca9a9fb1 100644 --- a/tests/test_pubsub.py +++ b/tests/test_pubsub.py @@ -543,7 +543,7 @@ def test_send_pubsub_ping_message(self, r): class TestPubSubConnectionKilled: @skip_if_server_version_lt('3.0.0') - @skip_if_redis_enterprise + @skip_if_redis_enterprise() def test_connection_error_raised_when_connection_dies(self, r): p = r.pubsub() p.subscribe('foo') From 0456f666d3872400f2fe2225d9153eb844b43a5c Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Wed, 1 Dec 2021 13:51:07 +0200 Subject: [PATCH 5/9] Added **kwargs to Redis' non key-based commands so they could be reused with target_nodes key word in RedisCluster. Removed duplicated commands from commands/cluster.py. --- README.md | 4 +- redis/client.py | 12 + redis/cluster.py | 16 +- redis/commands/__init__.py | 4 +- redis/commands/cluster.py | 921 ++----------------------------------- redis/commands/core.py | 350 +++++++------- tests/test_cluster.py | 10 +- 7 files changed, 262 insertions(+), 1055 deletions(-) diff --git a/README.md b/README.md index d068c68f14..f9d6309231 100644 --- a/README.md +++ b/README.md @@ -1046,7 +1046,7 @@ and attempt to retry executing the command. >>> rc.cluster_meet('127.0.0.1', 6379, target_nodes=Redis.ALL_NODES) >>> # ping all replicas >>> rc.ping(target_nodes=Redis.REPLICAS) - >>> # ping a specific node + >>> # ping a random node >>> rc.ping(target_nodes=Redis.RANDOM) >>> # get the keys from all cluster nodes >>> rc.keys(target_nodes=Redis.ALL_NODES) @@ -1158,7 +1158,7 @@ readwrite() method. >>> from cluster import RedisCluster as Redis # Use 'debug' log level to print the node that the command is executed on >>> rc_readonly = Redis(startup_nodes=startup_nodes, - read_from_replicas=True, debug=True) + read_from_replicas=True) >>> rc_readonly.set('{foo}1', 'bar1') >>> for i in range(0, 4): # Assigns read command to the slot's hosts in a Round-Robin manner diff --git a/redis/client.py b/redis/client.py index 0ae64be9c9..3b996e522c 100755 --- a/redis/client.py +++ b/redis/client.py @@ -920,6 +920,18 @@ def __init__(self, host='localhost', port=6379, def __repr__(self): return "%s<%s>" % (type(self).__name__, repr(self.connection_pool)) + def get_encoder(self): + """ + Get the connection pool's encoder + """ + return self.connection_pool.get_encoder() + + def get_connection_kwargs(self): + """ + Get the connection's key-word arguments + """ + return self.connection_pool.connection_kwargs + def set_response_callback(self, command, callback): "Set a custom Response Callback" self.response_callbacks[command] = callback diff --git a/redis/cluster.py b/redis/cluster.py index 19f7313380..0f8f77f363 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -9,7 +9,7 @@ from collections import OrderedDict from redis.client import CaseInsensitiveDict, Redis, PubSub from redis.commands import ( - ClusterCommands, + RedisClusterCommands, CommandsParser ) from redis.connection import DefaultParser, ConnectionPool, Encoder, parse_url @@ -201,7 +201,7 @@ class ClusterParser(DefaultParser): }) -class RedisCluster(ClusterCommands, object): +class RedisCluster(RedisClusterCommands, object): RedisClusterRequestTTL = 16 PRIMARIES = "primaries" @@ -789,6 +789,18 @@ def determine_slot(self, *args): def reinitialize_caches(self): self.nodes_manager.initialize() + def get_encoder(self): + """ + Get the connections' encoder + """ + return self.encoder + + def get_connection_kwargs(self): + """ + Get the connections' key-word arguments + """ + return self.nodes_manager.connection_kwargs + def _is_nodes_flag(self, target_nodes): return isinstance(target_nodes, str) \ and target_nodes in self.node_flags diff --git a/redis/commands/__init__.py b/redis/commands/__init__.py index a4728d0ac4..f04b98cc4d 100644 --- a/redis/commands/__init__.py +++ b/redis/commands/__init__.py @@ -1,4 +1,4 @@ -from .cluster import ClusterCommands +from .cluster import RedisClusterCommands from .core import CoreCommands from .helpers import list_or_args from .parser import CommandsParser @@ -6,7 +6,7 @@ from .sentinel import SentinelCommands __all__ = [ - 'ClusterCommands', + 'RedisClusterCommands', 'CommandsParser', 'CoreCommands', 'list_or_args', diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index fd0da64ba2..e5d8821eaf 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -1,319 +1,17 @@ from redis.exceptions import ( - ConnectionError, - DataError, + RedisClusterException, RedisError, ) from redis.crc import key_slot -from .core import DataAccessCommands +from .core import ( + ACLCommands, + DataAccessCommands, + ManagementCommands, + PubSubCommands +) from .helpers import list_or_args -class ClusterACLCommands: - """ - Redis Access Control List (ACL) commands for RedisCluster. - see: https://redis.io/topics/acl - - Commands can be executed on specified nodes using the target_nodes argument - By default, if target_nodes is not specified, the command will be - executed on the default cluster node. - - :param :target_nodes: type can be one of the followings: - - nodes flag: 'all', 'primaries', 'replicas', 'random' - - 'ClusterNode' - - 'list(ClusterNodes)' - - 'dict(any:clusterNodes)' - - for example: - # Set user 'virginia' on the cluster's default node - r.acl_setuser('virginia') - # Set user 'virginia' on all primaries - r.acl_setuser('virginia', target_nodes='primaries') - - """ - def acl_cat(self, category=None, target_nodes=None): - """ - Returns a list of categories or commands within a category. - - If ``category`` is not supplied, returns a list of all categories. - If ``category`` is supplied, returns a list of all commands within - that category. - - For more information check https://redis.io/commands/acl-cat - """ - pieces = [category] if category else [] - return self.execute_command('ACL CAT', *pieces, - target_nodes=target_nodes) - - def acl_deluser(self, *username, target_nodes=None): - """ - Delete the ACL for the specified ``username``s - - For more information check https://redis.io/commands/acl-deluser - """ - return self.execute_command('ACL DELUSER', *username, - target_nodes=target_nodes) - - def acl_genpass(self, bits=None, target_nodes=None): - """Generate a random password value. - If ``bits`` is supplied then use this number of bits, rounded to - the next multiple of 4. - See: https://redis.io/commands/acl-genpass - """ - pieces = [] - if bits is not None: - try: - b = int(bits) - if b < 0 or b > 4096: - raise ValueError - except ValueError: - raise DataError('genpass optionally accepts a bits argument, ' - 'between 0 and 4096.') - return self.execute_command('ACL GENPASS', *pieces, - target_nodes=target_nodes) - - def acl_getuser(self, username, target_nodes=None): - """ - Get the ACL details for the specified ``username``. - - If ``username`` does not exist, return None - - For more information check https://redis.io/commands/acl-getuser - """ - return self.execute_command('ACL GETUSER', username, - target_nodes=target_nodes) - - def acl_help(self, target_nodes=None): - """The ACL HELP command returns helpful text describing - the different subcommands. - - For more information check https://redis.io/commands/acl-help - """ - return self.execute_command('ACL HELP', target_nodes=target_nodes) - - def acl_list(self, target_nodes=None): - """ - Return a list of all ACLs on the server - - For more information check https://redis.io/commands/acl-list - """ - return self.execute_command('ACL LIST', target_nodes=target_nodes) - - def acl_log(self, count=None, target_nodes=None): - """ - Get ACL logs as a list. - :param int count: Get logs[0:count]. - :rtype: List. - - For more information check https://redis.io/commands/acl-log - """ - args = [] - if count is not None: - if not isinstance(count, int): - raise DataError('ACL LOG count must be an ' - 'integer') - args.append(count) - - return self.execute_command('ACL LOG', *args, - target_nodes=target_nodes) - - def acl_log_reset(self, target_nodes=None): - """ - Reset ACL logs. - :rtype: Boolean. - - For more information check https://redis.io/commands/acl-log - """ - args = [b'RESET'] - return self.execute_command('ACL LOG', *args, - target_nodes=target_nodes) - - def acl_load(self, target_nodes=None): - """ - Load ACL rules from the configured ``aclfile``. - - Note that the server must be configured with the ``aclfile`` - directive to be able to load ACL rules from an aclfile. - - For more information check https://redis.io/commands/acl-load - """ - return self.execute_command('ACL LOAD', target_nodes=target_nodes) - - def acl_save(self, target_nodes=None): - """ - Save ACL rules to the configured ``aclfile``. - - Note that the server must be configured with the ``aclfile`` - directive to be able to save ACL rules to an aclfile. - - For more information check https://redis.io/commands/acl-save - """ - return self.execute_command('ACL SAVE', target_nodes=target_nodes) - - def acl_setuser(self, username, enabled=False, nopass=False, - passwords=None, hashed_passwords=None, categories=None, - commands=None, keys=None, reset=False, reset_keys=False, - reset_passwords=False, target_nodes=None): - """ - Create or update an ACL user. - - Create or update the ACL for ``username``. If the user already exists, - the existing ACL is completely overwritten and replaced with the - specified values. - - ``enabled`` is a boolean indicating whether the user should be allowed - to authenticate or not. Defaults to ``False``. - - ``nopass`` is a boolean indicating whether the can authenticate without - a password. This cannot be True if ``passwords`` are also specified. - - ``passwords`` if specified is a list of plain text passwords - to add to or remove from the user. Each password must be prefixed with - a '+' to add or a '-' to remove. For convenience, the value of - ``passwords`` can be a simple prefixed string when adding or - removing a single password. - - ``hashed_passwords`` if specified is a list of SHA-256 hashed passwords - to add to or remove from the user. Each hashed password must be - prefixed with a '+' to add or a '-' to remove. For convenience, - the value of ``hashed_passwords`` can be a simple prefixed string when - adding or removing a single password. - - ``categories`` if specified is a list of strings representing category - permissions. Each string must be prefixed with either a '+' to add the - category permission or a '-' to remove the category permission. - - ``commands`` if specified is a list of strings representing command - permissions. Each string must be prefixed with either a '+' to add the - command permission or a '-' to remove the command permission. - - ``keys`` if specified is a list of key patterns to grant the user - access to. Keys patterns allow '*' to support wildcard matching. For - example, '*' grants access to all keys while 'cache:*' grants access - to all keys that are prefixed with 'cache:'. ``keys`` should not be - prefixed with a '~'. - - ``reset`` is a boolean indicating whether the user should be fully - reset prior to applying the new ACL. Setting this to True will - remove all existing passwords, flags and privileges from the user and - then apply the specified rules. If this is False, the user's existing - passwords, flags and privileges will be kept and any new specified - rules will be applied on top. - - ``reset_keys`` is a boolean indicating whether the user's key - permissions should be reset prior to applying any new key permissions - specified in ``keys``. If this is False, the user's existing - key permissions will be kept and any new specified key permissions - will be applied on top. - - ``reset_passwords`` is a boolean indicating whether to remove all - existing passwords and the 'nopass' flag from the user prior to - applying any new passwords specified in 'passwords' or - 'hashed_passwords'. If this is False, the user's existing passwords - and 'nopass' status will be kept and any new specified passwords - or hashed_passwords will be applied on top. - - For more information check https://redis.io/commands/acl-setuser - """ - encoder = self.encoder - pieces = [username] - - if reset: - pieces.append(b'reset') - - if reset_keys: - pieces.append(b'resetkeys') - - if reset_passwords: - pieces.append(b'resetpass') - - if enabled: - pieces.append(b'on') - else: - pieces.append(b'off') - - if (passwords or hashed_passwords) and nopass: - raise DataError('Cannot set \'nopass\' and supply ' - '\'passwords\' or \'hashed_passwords\'') - - if passwords: - # as most users will have only one password, allow remove_passwords - # to be specified as a simple string or a list - passwords = list_or_args(passwords, []) - for i, password in enumerate(passwords): - password = encoder.encode(password) - if password.startswith(b'+'): - pieces.append(b'>%s' % password[1:]) - elif password.startswith(b'-'): - pieces.append(b'<%s' % password[1:]) - else: - raise DataError('Password %d must be prefixeed with a ' - '"+" to add or a "-" to remove' % i) - - if hashed_passwords: - # as most users will have only one password, allow remove_passwords - # to be specified as a simple string or a list - hashed_passwords = list_or_args(hashed_passwords, []) - for i, hashed_password in enumerate(hashed_passwords): - hashed_password = encoder.encode(hashed_password) - if hashed_password.startswith(b'+'): - pieces.append(b'#%s' % hashed_password[1:]) - elif hashed_password.startswith(b'-'): - pieces.append(b'!%s' % hashed_password[1:]) - else: - raise DataError('Hashed %d password must be prefixeed ' - 'with a "+" to add or a "-" to remove' % i) - - if nopass: - pieces.append(b'nopass') - - if categories: - for category in categories: - category = encoder.encode(category) - # categories can be prefixed with one of (+@, +, -@, -) - if category.startswith(b'+@'): - pieces.append(category) - elif category.startswith(b'+'): - pieces.append(b'+@%s' % category[1:]) - elif category.startswith(b'-@'): - pieces.append(category) - elif category.startswith(b'-'): - pieces.append(b'-@%s' % category[1:]) - else: - raise DataError('Category "%s" must be prefixed with ' - '"+" or "-"' - % encoder.decode(category, force=True)) - if commands: - for cmd in commands: - cmd = encoder.encode(cmd) - if not cmd.startswith(b'+') and not cmd.startswith(b'-'): - raise DataError('Command "%s" must be prefixed with ' - '"+" or "-"' - % encoder.decode(cmd, force=True)) - pieces.append(cmd) - - if keys: - for key in keys: - key = encoder.encode(key) - pieces.append(b'~%s' % key) - - return self.execute_command('ACL SETUSER', *pieces, - target_nodes=target_nodes) - - def acl_users(self, target_nodes=None): - """Returns a list of all registered users on the server. - - For more information check https://redis.io/commands/acl-users - """ - return self.execute_command('ACL USERS', target_nodes=target_nodes) - - def acl_whoami(self, target_nodes=None): - """Get the username for the current connection - - For more information check https://redis.io/commands/acl-whoami - """ - return self.execute_command('ACL WHOAMI', target_nodes=target_nodes) - - class ClusterMultiKeyCommands: """ A class containing commands that handle more than one key @@ -453,601 +151,66 @@ def unlink(self, *keys): return self._split_command_across_slots('UNLINK', *keys) -class ClusterManagementCommands: +class ClusterManagementCommands(ManagementCommands): """ - Redis Cluster management commands + A class for Redis Cluster management commands - Commands with the 'target_nodes' argument can be executed on specified - nodes. By default, if target_nodes is not specified, the command will be - executed on the default cluster node. - - :param :target_nodes: type can be one of the followings: - - nodes flag: 'all', 'primaries', 'replicas', 'random' - - 'ClusterNode' - - 'list(ClusterNodes)' - - 'dict(any:clusterNodes)' - - for example: - primary = r.get_primaries()[0] - r.bgsave(target_nodes=primary) - r.bgsave(target_nodes='primaries') + The class inherits from Redis's core ManagementCommands class and do the + required adjustments to work with cluster mode """ - def bgsave(self, schedule=True, target_nodes=None): - """ - Tell the Redis server to save its data to disk. Unlike save(), - this method is asynchronous and returns immediately. - """ - pieces = [] - if schedule: - pieces.append("SCHEDULE") - return self.execute_command('BGSAVE', - *pieces, - target_nodes=target_nodes) - - def client_getname(self, target_nodes=None): - """ - Returns the current connection name from all nodes. - The result will be a dictionary with the IP and - connection name. - """ - return self.execute_command('CLIENT GETNAME', - target_nodes=target_nodes) - - def client_getredir(self, target_nodes=None): - """Returns the ID (an integer) of the client to whom we are - redirecting tracking notifications. - - see: https://redis.io/commands/client-getredir - """ - return self.execute_command('CLIENT GETREDIR', - target_nodes=target_nodes) - - def client_id(self, target_nodes=None): - """Returns the current connection id""" - return self.execute_command('CLIENT ID', - target_nodes=target_nodes) - - def client_info(self, target_nodes=None): - """ - Returns information and statistics about the current - client connection. - """ - return self.execute_command('CLIENT INFO', - target_nodes=target_nodes) + def slaveof(self, *args, **kwargs): + raise RedisClusterException("SLAVEOF is not supported in cluster mode") - def client_kill_filter(self, _id=None, _type=None, addr=None, - skipme=None, laddr=None, user=None, - target_nodes=None): - """ - Disconnects client(s) using a variety of filter options - :param id: Kills a client by its unique ID field - :param type: Kills a client by type where type is one of 'normal', - 'master', 'slave' or 'pubsub' - :param addr: Kills a client by its 'address:port' - :param skipme: If True, then the client calling the command - will not get killed even if it is identified by one of the filter - options. If skipme is not provided, the server defaults to skipme=True - :param laddr: Kills a client by its 'local (bind) address:port' - :param user: Kills a client for a specific user name - """ - args = [] - if _type is not None: - client_types = ('normal', 'master', 'slave', 'pubsub') - if str(_type).lower() not in client_types: - raise DataError("CLIENT KILL type must be one of %r" % ( - client_types,)) - args.extend((b'TYPE', _type)) - if skipme is not None: - if not isinstance(skipme, bool): - raise DataError("CLIENT KILL skipme must be a bool") - if skipme: - args.extend((b'SKIPME', b'YES')) - else: - args.extend((b'SKIPME', b'NO')) - if _id is not None: - args.extend((b'ID', _id)) - if addr is not None: - args.extend((b'ADDR', addr)) - if laddr is not None: - args.extend((b'LADDR', laddr)) - if user is not None: - args.extend((b'USER', user)) - if not args: - raise DataError("CLIENT KILL ... ... " - " must specify at least one filter") - return self.execute_command('CLIENT KILL', *args, - target_nodes=target_nodes) + def replicaof(self, *args, **kwargs): + raise RedisClusterException("REPLICAOF is not supported in cluster" + " mode") - def client_kill(self, address, target_nodes=None): - "Disconnects the client at ``address`` (ip:port)" - return self.execute_command('CLIENT KILL', address, - target_nodes=target_nodes) + def swapdb(self, *args, **kwargs): + raise RedisClusterException("SWAPDB is not supported in cluster" + " mode") - def client_list(self, _type=None, target_nodes=None): - """ - Returns a list of currently connected clients to the entire cluster. - If type of client specified, only that type will be returned. - :param _type: optional. one of the client types (normal, master, - replica, pubsub) - """ - if _type is not None: - client_types = ('normal', 'master', 'replica', 'pubsub') - if str(_type).lower() not in client_types: - raise DataError("CLIENT LIST _type must be one of %r" % ( - client_types,)) - return self.execute_command('CLIENT LIST', - b'TYPE', - _type, - target_noes=target_nodes) - return self.execute_command('CLIENT LIST', - target_nodes=target_nodes) - def client_pause(self, timeout, target_nodes=None): - """ - Suspend all the Redis clients for the specified amount of time - :param timeout: milliseconds to pause clients - """ - if not isinstance(timeout, int): - raise DataError("CLIENT PAUSE timeout must be an integer") - return self.execute_command('CLIENT PAUSE', str(timeout), - target_nodes=target_nodes) - - def client_reply(self, reply, target_nodes=None): - """Enable and disable redis server replies. - ``reply`` Must be ON OFF or SKIP, - ON - The default most with server replies to commands - OFF - Disable server responses to commands - SKIP - Skip the response of the immediately following command. - - Note: When setting OFF or SKIP replies, you will need a client object - with a timeout specified in seconds, and will need to catch the - TimeoutError. - The test_client_reply unit test illustrates this, and - conftest.py has a client with a timeout. - See https://redis.io/commands/client-reply - """ - replies = ['ON', 'OFF', 'SKIP'] - if reply not in replies: - raise DataError('CLIENT REPLY must be one of %r' % replies) - return self.execute_command("CLIENT REPLY", reply, - target_nodes=target_nodes) - - def client_setname(self, name, target_nodes=None): - "Sets the current connection name" - return self.execute_command('CLIENT SETNAME', name, - target_nodes=target_nodes) - - def client_trackinginfo(self, target_nodes=None): - """ - Returns the information about the current client connection's - use of the server assisted client side cache. - See https://redis.io/commands/client-trackinginfo - """ - return self.execute_command('CLIENT TRACKINGINFO', - target_nodes=target_nodes) - - def client_unblock(self, client_id, error=False, target_nodes=None): - """ - Unblocks a connection by its client id. - If ``error`` is True, unblocks the client with a special error message. - If ``error`` is False (default), the client is unblocked using the - regular timeout mechanism. - """ - args = ['CLIENT UNBLOCK', int(client_id)] - if error: - args.append(b'ERROR') - return self.execute_command(*args, target_nodes=target_nodes) - - def client_unpause(self, target_nodes=None): - """ - Unpause all redis clients - """ - return self.execute_command('CLIENT UNPAUSE', - target_nodes=target_nodes) - - def command(self, target_nodes=None): - """ - Returns dict reply of details about all Redis commands. - """ - return self.execute_command('COMMAND', target_nodes=target_nodes) - - def command_count(self, target_nodes=None): - """ - Returns Integer reply of number of total commands in this Redis server. - """ - return self.execute_command('COMMAND COUNT', target_nodes=target_nodes) - - def config_get(self, pattern="*", target_nodes=None): - """ - Return a dictionary of configuration based on the ``pattern`` - """ - return self.execute_command('CONFIG GET', - pattern, - target_nodes=target_nodes) - - def config_resetstat(self, target_nodes=None): - """Reset runtime statistics""" - return self.execute_command('CONFIG RESETSTAT', - target_nodes=target_nodes) - - def config_rewrite(self, target_nodes=None): - """ - Rewrite config file with the minimal change to reflect running config. - """ - return self.execute_command('CONFIG REWRITE', - target_nodes=target_nodes) - - def config_set(self, name, value, target_nodes=None): - "Set config item ``name`` with ``value``" - return self.execute_command('CONFIG SET', - name, - value, - target_nodes=target_nodes) - - def dbsize(self, target_nodes=None): - """ - Sums the number of keys in the target nodes' DB. - - :target_nodes: 'ClusterNode' or 'list(ClusterNodes)' - The node/s to execute the command on - """ - return self.execute_command('DBSIZE', - target_nodes=target_nodes) - - def debug_object(self, key): - raise NotImplementedError( - "DEBUG OBJECT is intentionally not implemented in the client." - ) - - def debug_segfault(self): - raise NotImplementedError( - "DEBUG SEGFAULT is intentionally not implemented in the client." - ) - - def echo(self, value, target_nodes): - """Echo the string back from the server""" - return self.execute_command('ECHO', value, - target_nodes=target_nodes) - - def flushall(self, asynchronous=False, target_nodes=None): - """ - Delete all keys in the database. - In cluster mode this method is the same as flushdb - - ``asynchronous`` indicates whether the operation is - executed asynchronously by the server. - """ - args = [] - if asynchronous: - args.append(b'ASYNC') - return self.execute_command('FLUSHALL', - *args, - target_nodes=target_nodes) - - def flushdb(self, asynchronous=False, target_nodes=None): - """ - Delete all keys in the database. - - ``asynchronous`` indicates whether the operation is - executed asynchronously by the server. - """ - args = [] - if asynchronous: - args.append(b'ASYNC') - return self.execute_command('FLUSHDB', - *args, - target_nodes=target_nodes) - - def info(self, section=None, target_nodes=None): - """ - Returns a dictionary containing information about the Redis server - - The ``section`` option can be used to select a specific section - of information - - The section option is not supported by older versions of Redis Server, - and will generate ResponseError - """ - if section is None: - return self.execute_command('INFO', - target_nodes=target_nodes) - else: - return self.execute_command('INFO', - section, - target_nodes=target_nodes) - - def keys(self, pattern='*', target_nodes=None): - "Returns a list of keys matching ``pattern``" - return self.execute_command('KEYS', pattern, target_nodes=target_nodes) - - def lastsave(self, target_nodes=None): - """ - Return a Python datetime object representing the last time the - Redis database was saved to disk - """ - return self.execute_command('LASTSAVE', - target_nodes=target_nodes) - - def memory_doctor(self): - raise NotImplementedError( - "MEMORY DOCTOR is intentionally not implemented in the client." - ) - - def memory_help(self): - raise NotImplementedError( - "MEMORY HELP is intentionally not implemented in the client." - ) - - def memory_malloc_stats(self, target_nodes=None): - """Return an internal statistics report from the memory allocator.""" - return self.execute_command('MEMORY MALLOC-STATS', - target_nodes=target_nodes) - - def memory_purge(self, target_nodes=None): - """Attempts to purge dirty pages for reclamation by allocator""" - return self.execute_command('MEMORY PURGE', - target_nodes=target_nodes) - - def memory_stats(self, target_nodes=None): - """Return a dictionary of memory stats""" - return self.execute_command('MEMORY STATS', - target_nodes=target_nodes) - - def memory_usage(self, key, samples=None): - """ - Return the total memory usage for key, its value and associated - administrative overheads. - - For nested data structures, ``samples`` is the number of elements to - sample. If left unspecified, the server's default is 5. Use 0 to sample - all elements. - """ - args = [] - if isinstance(samples, int): - args.extend([b'SAMPLES', samples]) - return self.execute_command('MEMORY USAGE', key, *args) - - def object(self, infotype, key): - """Return the encoding, idletime, or refcount about the key""" - return self.execute_command('OBJECT', infotype, key, infotype=infotype) - - def ping(self, target_nodes=None): - """ - Ping the cluster's servers. - If no target nodes are specified, sent to all nodes and returns True if - the ping was successful across all nodes. - """ - return self.execute_command('PING', - target_nodes=target_nodes) - - def randomkey(self, target_nodes=None): - """ - Returns the name of a random key" - """ - return self.execute_command('RANDOMKEY', target_nodes=target_nodes) - - def save(self, target_nodes=None): - """ - Tell the Redis server to save its data to disk, - blocking until the save is complete - """ - return self.execute_command('SAVE', target_nodes=target_nodes) - - def scan(self, cursor=0, match=None, count=None, _type=None, - target_nodes=None): - """ - Incrementally return lists of key names. Also return a cursor - indicating the scan position. - - ``match`` allows for filtering the keys by pattern - - ``count`` provides a hint to Redis about the number of keys to - return per batch. - - ``_type`` filters the returned values by a particular Redis type. - Stock Redis instances allow for the following types: - HASH, LIST, SET, STREAM, STRING, ZSET - Additionally, Redis modules can expose other types as well. - """ - pieces = [cursor] - if match is not None: - pieces.extend([b'MATCH', match]) - if count is not None: - pieces.extend([b'COUNT', count]) - if _type is not None: - pieces.extend([b'TYPE', _type]) - return self.execute_command('SCAN', *pieces, target_nodes=target_nodes) - - def scan_iter(self, match=None, count=None, _type=None, target_nodes=None): - """ - Make an iterator using the SCAN command so that the client doesn't - need to remember the cursor position. - - ``match`` allows for filtering the keys by pattern - - ``count`` provides a hint to Redis about the number of keys to - return per batch. - - ``_type`` filters the returned values by a particular Redis type. - Stock Redis instances allow for the following types: - HASH, LIST, SET, STREAM, STRING, ZSET - Additionally, Redis modules can expose other types as well. - """ - cursor = '0' - while cursor != 0: - cursor, data = self.scan(cursor=cursor, match=match, - count=count, _type=_type, - target_nodes=target_nodes) - yield from data - - def shutdown(self, save=False, nosave=False, target_nodes=None): - """Shutdown the Redis server. If Redis has persistence configured, - data will be flushed before shutdown. If the "save" option is set, - a data flush will be attempted even if there is no persistence - configured. If the "nosave" option is set, no data flush will be - attempted. The "save" and "nosave" options cannot both be set. - """ - if save and nosave: - raise DataError('SHUTDOWN save and nosave cannot both be set') - args = ['SHUTDOWN'] - if save: - args.append('SAVE') - if nosave: - args.append('NOSAVE') - try: - self.execute_command(*args, target_nodes=target_nodes) - except ConnectionError: - # a ConnectionError here is expected - return - raise RedisError("SHUTDOWN seems to have failed.") - - def slowlog_get(self, num=None, target_nodes=None): - """ - Get the entries from the slowlog. If ``num`` is specified, get the - most recent ``num`` items. - """ - args = ['SLOWLOG GET'] - if num is not None: - args.append(num) - - return self.execute_command(*args, - target_nodes=target_nodes) - - def slowlog_len(self, target_nodes=None): - "Get the number of items in the slowlog" - return self.execute_command('SLOWLOG LEN', - target_nodes=target_nodes) - - def slowlog_reset(self, target_nodes=None): - "Remove all items in the slowlog" - return self.execute_command('SLOWLOG RESET', - target_nodes=target_nodes) +class ClusterDataAccessCommands(DataAccessCommands): + """ + A class for Redis Cluster Data Access Commands + The class inherits from Redis's core DataAccessCommand class and do the + required adjustments to work with cluster mode + """ def stralgo(self, algo, value1, value2, specific_argument='strings', len=False, idx=False, minmatchlen=None, withmatchlen=False, - target_nodes=None): - """ - 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 - ``value1`` and ``value2`` Can be two strings or two keys - ``specific_argument`` Specifying if the arguments to the algorithm - will be keys or strings. strings is the default. - ``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 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: - pieces.append(b'LEN') - if idx: - pieces.append(b'IDX') - try: - int(minmatchlen) - pieces.extend([b'MINMATCHLEN', minmatchlen]) - except TypeError: - pass - if withmatchlen: - pieces.append(b'WITHMATCHLEN') + **kwargs): + target_nodes = kwargs.pop('target_nodes', None) if specific_argument == 'strings' and target_nodes is None: target_nodes = 'default-node' - return self.execute_command('STRALGO', *pieces, len=len, idx=idx, - minmatchlen=minmatchlen, - withmatchlen=withmatchlen, - target_nodes=target_nodes) - - def time(self, target_nodes=None): - """ - Returns the server time as a 2-item tuple of ints: - (seconds since epoch, microseconds into this second). - """ - return self.execute_command('TIME', target_nodes=target_nodes) + kwargs.update({'target_nodes': target_nodes}) + return super().stralgo(algo, value1, value2, specific_argument, + len, idx, minmatchlen, withmatchlen, **kwargs) - def wait(self, num_replicas, timeout, target_nodes=None): - """ - Redis synchronous replication - That returns the number of replicas that processed the query when - we finally have at least ``num_replicas``, or when the ``timeout`` was - reached. - - If more than one target node are passed the result will be summed up - """ - return self.execute_command('WAIT', num_replicas, - timeout, - target_nodes=target_nodes) - -class ClusterPubSubCommands: - """ - Redis PubSub commands for RedisCluster use. - see https://redis.io/topics/pubsub +class RedisClusterCommands(ClusterMultiKeyCommands, ClusterManagementCommands, + ACLCommands, PubSubCommands, + ClusterDataAccessCommands): """ - def publish(self, channel, message, target_nodes=None): - """ - Publish ``message`` on ``channel``. - Returns the number of subscribers the message was delivered to. - """ - return self.execute_command('PUBLISH', channel, message, - target_nodes=target_nodes) + A class for all Redis Cluster commands - def pubsub_channels(self, pattern='*', target_nodes=None): - """ - Return a list of channels that have at least one subscriber - """ - return self.execute_command('PUBSUB CHANNELS', pattern, - target_nodes=target_nodes) + For key-based commands, the target node(s) will be internally determined + by the keys' hash slot. + Non-key-based commands can be executed with the 'target_nodes' argument to + target specific nodes. By default, if target_nodes is not specified, the + command will be executed on the default cluster node. - def pubsub_numpat(self, target_nodes=None): - """ - Returns the number of subscriptions to patterns - """ - return self.execute_command('PUBSUB NUMPAT', target_nodes=target_nodes) - - def pubsub_numsub(self, *args, target_nodes=None): - """ - Return a list of (channel, number of subscribers) tuples - for each channel given in ``*args`` - """ - return self.execute_command('PUBSUB NUMSUB', *args, - target_nodes=target_nodes) - - -class ClusterCommands(ClusterManagementCommands, ClusterMultiKeyCommands, - ClusterACLCommands, ClusterPubSubCommands, - DataAccessCommands): - """ - Redis Cluster commands - - Commands with the 'target_nodes' argument can be executed on specified - nodes. By default, if target_nodes is not specified, the command will be - executed on the default cluster node. :param :target_nodes: type can be one of the followings: - - nodes flag: 'all', 'primaries', 'replicas', 'random' + - nodes flag: ALL_NODES, PRIMARIES, REPLICAS, RANDOM - 'ClusterNode' - 'list(ClusterNodes)' - 'dict(any:clusterNodes)' for example: - r.cluster_info(target_nodes='all') + r.cluster_info(target_nodes=RedisCluster.ALL_NODES) """ + def cluster_addslots(self, target_node, *slots): """ Assign new hash slots to receiving node. Sends to specified node. diff --git a/redis/commands/core.py b/redis/commands/core.py index 64e3b6d37e..d08bbb3bd9 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -17,7 +17,7 @@ class ACLCommands: Redis Access Control List (ACL) commands. see: https://redis.io/topics/acl """ - def acl_cat(self, category=None): + def acl_cat(self, category=None, **kwargs): """ Returns a list of categories or commands within a category. @@ -28,17 +28,17 @@ def acl_cat(self, category=None): For more information check https://redis.io/commands/acl-cat """ pieces = [category] if category else [] - return self.execute_command('ACL CAT', *pieces) + return self.execute_command('ACL CAT', *pieces, **kwargs) - def acl_deluser(self, *username): + def acl_deluser(self, *username, **kwargs): """ Delete the ACL for the specified ``username``s For more information check https://redis.io/commands/acl-deluser """ - return self.execute_command('ACL DELUSER', *username) + return self.execute_command('ACL DELUSER', *username, **kwargs) - def acl_genpass(self, bits=None): + def acl_genpass(self, bits=None, **kwargs): """Generate a random password value. If ``bits`` is supplied then use this number of bits, rounded to the next multiple of 4. @@ -53,9 +53,9 @@ def acl_genpass(self, bits=None): except ValueError: raise DataError('genpass optionally accepts a bits argument, ' 'between 0 and 4096.') - return self.execute_command('ACL GENPASS', *pieces) + return self.execute_command('ACL GENPASS', *pieces, **kwargs) - def acl_getuser(self, username): + def acl_getuser(self, username, **kwargs): """ Get the ACL details for the specified ``username``. @@ -63,25 +63,25 @@ def acl_getuser(self, username): For more information check https://redis.io/commands/acl-getuser """ - return self.execute_command('ACL GETUSER', username) + return self.execute_command('ACL GETUSER', username, **kwargs) - def acl_help(self): + def acl_help(self, **kwargs): """The ACL HELP command returns helpful text describing the different subcommands. For more information check https://redis.io/commands/acl-help """ - return self.execute_command('ACL HELP') + return self.execute_command('ACL HELP', **kwargs) - def acl_list(self): + def acl_list(self, **kwargs): """ Return a list of all ACLs on the server For more information check https://redis.io/commands/acl-list """ - return self.execute_command('ACL LIST') + return self.execute_command('ACL LIST', **kwargs) - def acl_log(self, count=None): + def acl_log(self, count=None, **kwargs): """ Get ACL logs as a list. :param int count: Get logs[0:count]. @@ -96,9 +96,9 @@ def acl_log(self, count=None): 'integer') args.append(count) - return self.execute_command('ACL LOG', *args) + return self.execute_command('ACL LOG', *args, **kwargs) - def acl_log_reset(self): + def acl_log_reset(self, **kwargs): """ Reset ACL logs. :rtype: Boolean. @@ -106,9 +106,9 @@ def acl_log_reset(self): For more information check https://redis.io/commands/acl-log """ args = [b'RESET'] - return self.execute_command('ACL LOG', *args) + return self.execute_command('ACL LOG', *args, **kwargs) - def acl_load(self): + def acl_load(self, **kwargs): """ Load ACL rules from the configured ``aclfile``. @@ -117,9 +117,9 @@ def acl_load(self): For more information check https://redis.io/commands/acl-load """ - return self.execute_command('ACL LOAD') + return self.execute_command('ACL LOAD', **kwargs) - def acl_save(self): + def acl_save(self, **kwargs): """ Save ACL rules to the configured ``aclfile``. @@ -128,12 +128,12 @@ def acl_save(self): For more information check https://redis.io/commands/acl-save """ - return self.execute_command('ACL SAVE') + return self.execute_command('ACL SAVE', **kwargs) def acl_setuser(self, username, enabled=False, nopass=False, passwords=None, hashed_passwords=None, categories=None, commands=None, keys=None, reset=False, reset_keys=False, - reset_passwords=False): + reset_passwords=False, **kwargs): """ Create or update an ACL user. @@ -195,7 +195,7 @@ def acl_setuser(self, username, enabled=False, nopass=False, For more information check https://redis.io/commands/acl-setuser """ - encoder = self.connection_pool.get_encoder() + encoder = self.get_encoder() pieces = [username] if reset: @@ -277,35 +277,35 @@ def acl_setuser(self, username, enabled=False, nopass=False, key = encoder.encode(key) pieces.append(b'~%s' % key) - return self.execute_command('ACL SETUSER', *pieces) + return self.execute_command('ACL SETUSER', *pieces, **kwargs) - def acl_users(self): + def acl_users(self, **kwargs): """Returns a list of all registered users on the server. For more information check https://redis.io/commands/acl-users """ - return self.execute_command('ACL USERS') + return self.execute_command('ACL USERS', **kwargs) - def acl_whoami(self): + def acl_whoami(self, **kwargs): """Get the username for the current connection For more information check https://redis.io/commands/acl-whoami """ - return self.execute_command('ACL WHOAMI') + return self.execute_command('ACL WHOAMI', **kwargs) class ManagementCommands: """ Redis management commands """ - def bgrewriteaof(self): + def bgrewriteaof(self, **kwargs): """Tell the Redis server to rewrite the AOF file from data in memory. For more information check https://redis.io/commands/bgrewriteaof """ - return self.execute_command('BGREWRITEAOF') + return self.execute_command('BGREWRITEAOF', **kwargs) - def bgsave(self, schedule=True): + def bgsave(self, schedule=True, **kwargs): """ Tell the Redis server to save its data to disk. Unlike save(), this method is asynchronous and returns immediately. @@ -315,17 +315,17 @@ def bgsave(self, schedule=True): pieces = [] if schedule: pieces.append("SCHEDULE") - return self.execute_command('BGSAVE', *pieces) + return self.execute_command('BGSAVE', *pieces, **kwargs) - def client_kill(self, address): + def client_kill(self, address, **kwargs): """Disconnects the client at ``address`` (ip:port) For more information check https://redis.io/commands/client-kill """ - return self.execute_command('CLIENT KILL', address) + return self.execute_command('CLIENT KILL', address, **kwargs) def client_kill_filter(self, _id=None, _type=None, addr=None, - skipme=None, laddr=None, user=None): + skipme=None, laddr=None, user=None, **kwargs): """ Disconnects client(s) using a variety of filter options :param id: Kills a client by its unique ID field @@ -363,18 +363,18 @@ def client_kill_filter(self, _id=None, _type=None, addr=None, if not args: raise DataError("CLIENT KILL ... ... " " must specify at least one filter") - return self.execute_command('CLIENT KILL', *args) + return self.execute_command('CLIENT KILL', *args, **kwargs) - def client_info(self): + def client_info(self, **kwargs): """ Returns information and statistics about the current client connection. For more information check https://redis.io/commands/client-info """ - return self.execute_command('CLIENT INFO') + return self.execute_command('CLIENT INFO', **kwargs) - def client_list(self, _type=None, client_id=[]): + def client_list(self, _type=None, client_id=[], **kwargs): """ Returns a list of currently connected clients. If type of client specified, only that type will be returned. @@ -397,26 +397,26 @@ def client_list(self, _type=None, client_id=[]): if client_id != []: args.append(b"ID") args.append(' '.join(client_id)) - return self.execute_command('CLIENT LIST', *args) + return self.execute_command('CLIENT LIST', *args, **kwargs) - def client_getname(self): + def client_getname(self, **kwargs): """ Returns the current connection name For more information check https://redis.io/commands/client-getname """ - return self.execute_command('CLIENT GETNAME') + return self.execute_command('CLIENT GETNAME', **kwargs) - def client_getredir(self): + def client_getredir(self, **kwargs): """ Returns the ID (an integer) of the client to whom we are redirecting tracking notifications. see: https://redis.io/commands/client-getredir """ - return self.execute_command('CLIENT GETREDIR') + return self.execute_command('CLIENT GETREDIR', **kwargs) - def client_reply(self, reply): + def client_reply(self, reply, **kwargs): """ Enable and disable redis server replies. ``reply`` Must be ON OFF or SKIP, @@ -435,34 +435,34 @@ def client_reply(self, reply): replies = ['ON', 'OFF', 'SKIP'] if reply not in replies: raise DataError('CLIENT REPLY must be one of %r' % replies) - return self.execute_command("CLIENT REPLY", reply) + return self.execute_command("CLIENT REPLY", reply, **kwargs) - def client_id(self): + def client_id(self, **kwargs): """ Returns the current connection id For more information check https://redis.io/commands/client-id """ - return self.execute_command('CLIENT ID') + return self.execute_command('CLIENT ID', **kwargs) - def client_trackinginfo(self): + def client_trackinginfo(self, **kwargs): """ Returns the information about the current client connection's use of the server assisted client side cache. See https://redis.io/commands/client-trackinginfo """ - return self.execute_command('CLIENT TRACKINGINFO') + return self.execute_command('CLIENT TRACKINGINFO', **kwargs) - def client_setname(self, name): + def client_setname(self, name, **kwargs): """ Sets the current connection name For more information check https://redis.io/commands/client-setname """ - return self.execute_command('CLIENT SETNAME', name) + return self.execute_command('CLIENT SETNAME', name, **kwargs) - def client_unblock(self, client_id, error=False): + def client_unblock(self, client_id, error=False, **kwargs): """ Unblocks a connection by its client id. If ``error`` is True, unblocks the client with a special error message. @@ -474,9 +474,9 @@ def client_unblock(self, client_id, error=False): args = ['CLIENT UNBLOCK', int(client_id)] if error: args.append(b'ERROR') - return self.execute_command(*args) + return self.execute_command(*args, **kwargs) - def client_pause(self, timeout): + def client_pause(self, timeout, **kwargs): """ Suspend all the Redis clients for the specified amount of time :param timeout: milliseconds to pause clients @@ -485,91 +485,80 @@ def client_pause(self, timeout): """ if not isinstance(timeout, int): raise DataError("CLIENT PAUSE timeout must be an integer") - return self.execute_command('CLIENT PAUSE', str(timeout)) + return self.execute_command('CLIENT PAUSE', str(timeout), **kwargs) - def client_unpause(self): + def client_unpause(self, **kwargs): """ Unpause all redis clients For more information check https://redis.io/commands/client-unpause """ - return self.execute_command('CLIENT UNPAUSE') + return self.execute_command('CLIENT UNPAUSE', **kwargs) - def command_info(self): - raise NotImplementedError( - "COMMAND INFO is intentionally not implemented in the client." - ) - - def command_count(self): - return self.execute_command('COMMAND COUNT') - - def readwrite(self): + def command(self, **kwargs): """ - Disables read queries for a connection to a Redis Cluster slave node. + Returns dict reply of details about all Redis commands. - For more information check https://redis.io/commands/readwrite + For more information check https://redis.io/commands/command """ - return self.execute_command('READWRITE') + return self.execute_command('COMMAND', **kwargs) - def readonly(self): - """ - Enables read queries for a connection to a Redis Cluster replica node. + def command_info(self, **kwargs): + raise NotImplementedError( + "COMMAND INFO is intentionally not implemented in the client." + ) - For more information check https://redis.io/commands/readonly - """ - return self.execute_command('READONLY') + def command_count(self, **kwargs): + return self.execute_command('COMMAND COUNT', **kwargs) - def config_get(self, pattern="*"): + def config_get(self, pattern="*", **kwargs): """ Return a dictionary of configuration based on the ``pattern`` For more information check https://redis.io/commands/config-get """ - return self.execute_command('CONFIG GET', pattern) + return self.execute_command('CONFIG GET', pattern, **kwargs) - def config_set(self, name, value): + def config_set(self, name, value, **kwargs): """Set config item ``name`` with ``value`` For more information check https://redis.io/commands/config-set """ - return self.execute_command('CONFIG SET', name, value) + return self.execute_command('CONFIG SET', name, value, **kwargs) - def config_resetstat(self): + def config_resetstat(self, **kwargs): """ Reset runtime statistics For more information check https://redis.io/commands/config-resetstat """ - return self.execute_command('CONFIG RESETSTAT') + return self.execute_command('CONFIG RESETSTAT', **kwargs) - def config_rewrite(self): + def config_rewrite(self, **kwargs): """ Rewrite config file with the minimal change to reflect running config. For more information check https://redis.io/commands/config-rewrite """ - return self.execute_command('CONFIG REWRITE') + return self.execute_command('CONFIG REWRITE', **kwargs) - def cluster(self, cluster_arg, *args): - return self.execute_command('CLUSTER %s' % cluster_arg.upper(), *args) - - def dbsize(self): + def dbsize(self, **kwargs): """ Returns the number of keys in the current database For more information check https://redis.io/commands/dbsize """ - return self.execute_command('DBSIZE') + return self.execute_command('DBSIZE', **kwargs) - def debug_object(self, key): + def debug_object(self, key, **kwargs): """ Returns version specific meta information about a given key For more information check https://redis.io/commands/debug-object """ - return self.execute_command('DEBUG OBJECT', key) + return self.execute_command('DEBUG OBJECT', key, **kwargs) - def debug_segfault(self): + def debug_segfault(self, **kwargs): raise NotImplementedError( """ DEBUG SEGFAULT is intentionally not implemented in the client. @@ -578,15 +567,15 @@ def debug_segfault(self): """ ) - def echo(self, value): + def echo(self, value, **kwargs): """ Echo the string back from the server For more information check https://redis.io/commands/echo """ - return self.execute_command('ECHO', value) + return self.execute_command('ECHO', value, **kwargs) - def flushall(self, asynchronous=False): + def flushall(self, asynchronous=False, **kwargs): """ Delete all keys in all databases on the current host. @@ -598,9 +587,9 @@ def flushall(self, asynchronous=False): args = [] if asynchronous: args.append(b'ASYNC') - return self.execute_command('FLUSHALL', *args) + return self.execute_command('FLUSHALL', *args, **kwargs) - def flushdb(self, asynchronous=False): + def flushdb(self, asynchronous=False, **kwargs): """ Delete all keys in the current database. @@ -612,17 +601,17 @@ def flushdb(self, asynchronous=False): args = [] if asynchronous: args.append(b'ASYNC') - return self.execute_command('FLUSHDB', *args) + return self.execute_command('FLUSHDB', *args, **kwargs) - def swapdb(self, first, second): + def swapdb(self, first, second, **kwargs): """ Swap two databases For more information check https://redis.io/commands/swapdb """ - return self.execute_command('SWAPDB', first, second) + return self.execute_command('SWAPDB', first, second, **kwargs) - def info(self, section=None): + def info(self, section=None, **kwargs): """ Returns a dictionary containing information about the Redis server @@ -635,32 +624,33 @@ def info(self, section=None): For more information check https://redis.io/commands/info """ if section is None: - return self.execute_command('INFO') + return self.execute_command('INFO', **kwargs) else: - return self.execute_command('INFO', section) + return self.execute_command('INFO', section, **kwargs) - def lastsave(self): + def lastsave(self, **kwargs): """ Return a Python datetime object representing the last time the Redis database was saved to disk For more information check https://redis.io/commands/lastsave """ - return self.execute_command('LASTSAVE') + return self.execute_command('LASTSAVE', **kwargs) - def lolwut(self, *version_numbers): + def lolwut(self, *version_numbers, **kwargs): """ Get the Redis version and a piece of generative computer art See: https://redis.io/commands/lolwut """ if version_numbers: - return self.execute_command('LOLWUT VERSION', *version_numbers) + return self.execute_command('LOLWUT VERSION', *version_numbers, + **kwargs) else: - return self.execute_command('LOLWUT') + return self.execute_command('LOLWUT', **kwargs) def migrate(self, host, port, keys, destination_db, timeout, - copy=False, replace=False, auth=None): + copy=False, replace=False, auth=None, **kwargs): """ Migrate 1 or more keys from the current Redis server to a different server specified by the ``host``, ``port`` and ``destination_db``. @@ -694,15 +684,16 @@ def migrate(self, host, port, keys, destination_db, timeout, pieces.append(b'KEYS') pieces.extend(keys) return self.execute_command('MIGRATE', host, port, '', destination_db, - timeout, *pieces) + timeout, *pieces, **kwargs) - def object(self, infotype, key): + def object(self, infotype, key, **kwargs): """ Return the encoding, idletime, or refcount about the key """ - return self.execute_command('OBJECT', infotype, key, infotype=infotype) + return self.execute_command('OBJECT', infotype, key, infotype=infotype, + **kwargs) - def memory_doctor(self): + def memory_doctor(self, **kwargs): raise NotImplementedError( """ MEMORY DOCTOR is intentionally not implemented in the client. @@ -711,7 +702,7 @@ def memory_doctor(self): """ ) - def memory_help(self): + def memory_help(self, **kwargs): raise NotImplementedError( """ MEMORY HELP is intentionally not implemented in the client. @@ -720,23 +711,23 @@ def memory_help(self): """ ) - def memory_stats(self): + def memory_stats(self, **kwargs): """ Return a dictionary of memory stats For more information check https://redis.io/commands/memory-stats """ - return self.execute_command('MEMORY STATS') + return self.execute_command('MEMORY STATS', **kwargs) - def memory_malloc_stats(self): + def memory_malloc_stats(self, **kwargs): """ Return an internal statistics report from the memory allocator. See: https://redis.io/commands/memory-malloc-stats """ - return self.execute_command('MEMORY MALLOC-STATS') + return self.execute_command('MEMORY MALLOC-STATS', **kwargs) - def memory_usage(self, key, samples=None): + def memory_usage(self, key, samples=None, **kwargs): """ Return the total memory usage for key, its value and associated administrative overheads. @@ -750,33 +741,33 @@ def memory_usage(self, key, samples=None): args = [] if isinstance(samples, int): args.extend([b'SAMPLES', samples]) - return self.execute_command('MEMORY USAGE', key, *args) + return self.execute_command('MEMORY USAGE', key, *args, **kwargs) - def memory_purge(self): + def memory_purge(self, **kwargs): """ Attempts to purge dirty pages for reclamation by allocator For more information check https://redis.io/commands/memory-purge """ - return self.execute_command('MEMORY PURGE') + return self.execute_command('MEMORY PURGE', **kwargs) - def ping(self): + def ping(self, **kwargs): """ Ping the Redis server For more information check https://redis.io/commands/ping """ - return self.execute_command('PING') + return self.execute_command('PING', **kwargs) - def quit(self): + def quit(self, **kwargs): """ Ask the server to close the connection. For more information check https://redis.io/commands/quit """ - return self.execute_command('QUIT') + return self.execute_command('QUIT', **kwargs) - def replicaof(self, *args): + def replicaof(self, *args, **kwargs): """ Update the replication settings of a redis replica, on the fly. Examples of valid arguments include: @@ -785,18 +776,18 @@ def replicaof(self, *args): For more information check https://redis.io/commands/replicaof """ - return self.execute_command('REPLICAOF', *args) + return self.execute_command('REPLICAOF', *args, **kwargs) - def save(self): + def save(self, **kwargs): """ Tell the Redis server to save its data to disk, blocking until the save is complete For more information check https://redis.io/commands/save """ - return self.execute_command('SAVE') + return self.execute_command('SAVE', **kwargs) - def shutdown(self, save=False, nosave=False): + def shutdown(self, save=False, nosave=False, **kwargs): """Shutdown the Redis server. If Redis has persistence configured, data will be flushed before shutdown. If the "save" option is set, a data flush will be attempted even if there is no persistence @@ -813,13 +804,13 @@ def shutdown(self, save=False, nosave=False): if nosave: args.append('NOSAVE') try: - self.execute_command(*args) + self.execute_command(*args, **kwargs) except ConnectionError: # a ConnectionError here is expected return raise RedisError("SHUTDOWN seems to have failed.") - def slaveof(self, host=None, port=None): + def slaveof(self, host=None, port=None, **kwargs): """ Set the server to be a replicated slave of the instance identified by the ``host`` and ``port``. If called without arguments, the @@ -828,10 +819,10 @@ def slaveof(self, host=None, port=None): For more information check https://redis.io/commands/slaveof """ if host is None and port is None: - return self.execute_command('SLAVEOF', b'NO', b'ONE') - return self.execute_command('SLAVEOF', host, port) + return self.execute_command('SLAVEOF', b'NO', b'ONE', **kwargs) + return self.execute_command('SLAVEOF', host, port, **kwargs) - def slowlog_get(self, num=None): + def slowlog_get(self, num=None, **kwargs): """ Get the entries from the slowlog. If ``num`` is specified, get the most recent ``num`` items. @@ -841,36 +832,37 @@ def slowlog_get(self, num=None): args = ['SLOWLOG GET'] if num is not None: args.append(num) - decode_responses = self.connection_pool.connection_kwargs.get( + decode_responses = self.get_connection_kwargs().get( 'decode_responses', False) - return self.execute_command(*args, decode_responses=decode_responses) + return self.execute_command(*args, decode_responses=decode_responses, + **kwargs) - def slowlog_len(self): + def slowlog_len(self, **kwargs): """ Get the number of items in the slowlog For more information check https://redis.io/commands/slowlog-len """ - return self.execute_command('SLOWLOG LEN') + return self.execute_command('SLOWLOG LEN', **kwargs) - def slowlog_reset(self): + def slowlog_reset(self, **kwargs): """ Remove all items in the slowlog For more information check https://redis.io/commands/slowlog-reset """ - return self.execute_command('SLOWLOG RESET') + return self.execute_command('SLOWLOG RESET', **kwargs) - def time(self): + def time(self, **kwargs): """ Returns the server time as a 2-item tuple of ints: (seconds since epoch, microseconds into this second). For more information check https://redis.io/commands/time """ - return self.execute_command('TIME') + return self.execute_command('TIME', **kwargs) - def wait(self, num_replicas, timeout): + def wait(self, num_replicas, timeout, **kwargs): """ Redis synchronous replication That returns the number of replicas that processed the query when @@ -879,7 +871,7 @@ def wait(self, num_replicas, timeout): For more information check https://redis.io/commands/wait """ - return self.execute_command('WAIT', num_replicas, timeout) + return self.execute_command('WAIT', num_replicas, timeout, **kwargs) class BasicKeyCommands: @@ -1189,13 +1181,13 @@ def incrbyfloat(self, name, amount=1.0): """ return self.execute_command('INCRBYFLOAT', name, amount) - def keys(self, pattern='*'): + def keys(self, pattern='*', **kwargs): """ Returns a list of keys matching ``pattern`` For more information check https://redis.io/commands/keys """ - return self.execute_command('KEYS', pattern) + return self.execute_command('KEYS', pattern, **kwargs) def lmove(self, first_list, second_list, src="LEFT", dest="RIGHT"): """ @@ -1341,13 +1333,13 @@ def hrandfield(self, key, count=None, withvalues=False): return self.execute_command("HRANDFIELD", key, *params) - def randomkey(self): + def randomkey(self, **kwargs): """ Returns the name of a random key For more information check https://redis.io/commands/randomkey """ - return self.execute_command('RANDOMKEY') + return self.execute_command('RANDOMKEY', **kwargs) def rename(self, src, dst): """ @@ -1531,7 +1523,8 @@ def setrange(self, name, offset, value): return self.execute_command('SETRANGE', name, offset, value) def stralgo(self, algo, value1, value2, specific_argument='strings', - len=False, idx=False, minmatchlen=None, withmatchlen=False): + len=False, idx=False, minmatchlen=None, withmatchlen=False, + **kwargs): """ Implements complex algorithms that operate on strings. Right now the only algorithm implemented is the LCS algorithm @@ -1577,7 +1570,8 @@ def stralgo(self, algo, value1, value2, specific_argument='strings', return self.execute_command('STRALGO', *pieces, len=len, idx=idx, minmatchlen=minmatchlen, - withmatchlen=withmatchlen) + withmatchlen=withmatchlen, + **kwargs) def strlen(self, name): """ @@ -1956,7 +1950,7 @@ class ScanCommands: Redis SCAN commands. see: https://redis.io/commands/scan """ - def scan(self, cursor=0, match=None, count=None, _type=None): + def scan(self, cursor=0, match=None, count=None, _type=None, **kwargs): """ Incrementally return lists of key names. Also return a cursor indicating the scan position. @@ -1980,9 +1974,9 @@ def scan(self, cursor=0, match=None, count=None, _type=None): pieces.extend([b'COUNT', count]) if _type is not None: pieces.extend([b'TYPE', _type]) - return self.execute_command('SCAN', *pieces) + return self.execute_command('SCAN', *pieces, **kwargs) - def scan_iter(self, match=None, count=None, _type=None): + def scan_iter(self, match=None, count=None, _type=None, **kwargs): """ Make an iterator using the SCAN command so that the client doesn't need to remember the cursor position. @@ -2000,7 +1994,7 @@ def scan_iter(self, match=None, count=None, _type=None): cursor = '0' while cursor != 0: cursor, data = self.scan(cursor=cursor, match=match, - count=count, _type=_type) + count=count, _type=_type, **kwargs) yield from data def sscan(self, name, cursor=0, match=None, count=None): @@ -3510,39 +3504,39 @@ class PubSubCommands: Redis PubSub commands. see https://redis.io/topics/pubsub """ - def publish(self, channel, message): + def publish(self, channel, message, **kwargs): """ Publish ``message`` on ``channel``. Returns the number of subscribers the message was delivered to. For more information check https://redis.io/commands/publish """ - return self.execute_command('PUBLISH', channel, message) + return self.execute_command('PUBLISH', channel, message, **kwargs) - def pubsub_channels(self, pattern='*'): + def pubsub_channels(self, pattern='*', **kwargs): """ Return a list of channels that have at least one subscriber For more information check https://redis.io/commands/pubsub-channels """ - return self.execute_command('PUBSUB CHANNELS', pattern) + return self.execute_command('PUBSUB CHANNELS', pattern, **kwargs) - def pubsub_numpat(self): + def pubsub_numpat(self, **kwargs): """ Returns the number of subscriptions to patterns For more information check https://redis.io/commands/pubsub-numpat """ - return self.execute_command('PUBSUB NUMPAT') + return self.execute_command('PUBSUB NUMPAT', **kwargs) - def pubsub_numsub(self, *args): + def pubsub_numsub(self, *args, **kwargs): """ Return a list of (channel, number of subscribers) tuples for each channel given in ``*args`` For more information check https://redis.io/commands/pubsub-numsub """ - return self.execute_command('PUBSUB NUMSUB', *args) + return self.execute_command('PUBSUB NUMSUB', *args, **kwargs) class ScriptCommands: @@ -4133,6 +4127,31 @@ def execute(self): return self.client.execute_command(*command) +class ClusterCommands: + """ + Class for Redis Cluster commands + """ + def cluster(self, cluster_arg, *args, **kwargs): + return self.execute_command('CLUSTER %s' % cluster_arg.upper(), *args, + **kwargs) + + def readwrite(self, **kwargs): + """ + Disables read queries for a connection to a Redis Cluster slave node. + + For more information check https://redis.io/commands/readwrite + """ + return self.execute_command('READWRITE', **kwargs) + + def readonly(self, **kwargs): + """ + Enables read queries for a connection to a Redis Cluster replica node. + + For more information check https://redis.io/commands/readonly + """ + return self.execute_command('READONLY', **kwargs) + + class DataAccessCommands(BasicKeyCommands, ListCommands, ScanCommands, SetCommands, StreamCommands, SortedSetCommands, @@ -4144,8 +4163,9 @@ class DataAccessCommands(BasicKeyCommands, ListCommands, """ -class CoreCommands(ACLCommands, DataAccessCommands, ManagementCommands, - ModuleCommands, PubSubCommands, ScriptCommands): +class CoreCommands(ACLCommands, ClusterCommands, DataAccessCommands, + ManagementCommands, ModuleCommands, PubSubCommands, + ScriptCommands): """ A class containing all of the implemented redis commands. This class is to be used as a mixin. diff --git a/tests/test_cluster.py b/tests/test_cluster.py index b2d32981a9..17619f7218 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -258,7 +258,7 @@ def test_execute_command_node_flag_primaries(self, r): primaries = r.get_primaries() replicas = r.get_replicas() mock_all_nodes_resp(r, 'PONG') - assert r.ping(RedisCluster.PRIMARIES) is True + assert r.ping(target_nodes=RedisCluster.PRIMARIES) is True for primary in primaries: conn = primary.redis_connection.connection assert conn.read_response.called is True @@ -275,7 +275,7 @@ def test_execute_command_node_flag_replicas(self, r): r = get_mocked_redis_client(default_host, default_port) primaries = r.get_primaries() mock_all_nodes_resp(r, 'PONG') - assert r.ping(RedisCluster.REPLICAS) is True + assert r.ping(target_nodes=RedisCluster.REPLICAS) is True for replica in replicas: conn = replica.redis_connection.connection assert conn.read_response.called is True @@ -288,7 +288,7 @@ def test_execute_command_node_flag_all_nodes(self, r): Test command execution with nodes flag ALL_NODES """ mock_all_nodes_resp(r, 'PONG') - assert r.ping(RedisCluster.ALL_NODES) is True + assert r.ping(target_nodes=RedisCluster.ALL_NODES) is True for node in r.get_nodes(): conn = node.redis_connection.connection assert conn.read_response.called is True @@ -298,7 +298,7 @@ def test_execute_command_node_flag_random(self, r): Test command execution with nodes flag RANDOM """ mock_all_nodes_resp(r, 'PONG') - assert r.ping(RedisCluster.RANDOM) is True + assert r.ping(target_nodes=RedisCluster.RANDOM) is True called_count = 0 for node in r.get_nodes(): conn = node.redis_connection.connection @@ -1112,7 +1112,7 @@ def test_lastsave(self, r): def test_cluster_echo(self, r): node = r.get_primaries()[0] - assert r.echo('foo bar', node) == b'foo bar' + assert r.echo('foo bar', target_nodes=node) == b'foo bar' @skip_if_server_version_lt('1.0.0') def test_debug_segfault(self, r): From 11ec84bac8d80b76278458f4db9da6390dda0f51 Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Wed, 1 Dec 2021 15:57:08 +0200 Subject: [PATCH 6/9] Fixed linters --- redis/commands/__init__.py | 2 +- redis/commands/core.py | 2 +- tests/test_cluster.py | 1 - tests/test_connection_pool.py | 74 +++++++++++++++++++++-------------- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/redis/commands/__init__.py b/redis/commands/__init__.py index 8a40769b8b..f04b98cc4d 100644 --- a/redis/commands/__init__.py +++ b/redis/commands/__init__.py @@ -12,4 +12,4 @@ 'list_or_args', 'RedisModuleCommands', 'SentinelCommands' -] \ No newline at end of file +] diff --git a/redis/commands/core.py b/redis/commands/core.py index feb9c80ca3..27f814ccc3 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -374,7 +374,7 @@ def client_info(self, **kwargs): For more information check https://redis.io/commands/client-info """ return self.execute_command('CLIENT INFO', **kwargs) - + def client_list(self, _type=None, client_id=[], **kwargs): """ Returns a list of currently connected clients. diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 3c5e7c82a3..7f4644555f 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -31,7 +31,6 @@ from redis.utils import str_if_bytes from tests.test_pubsub import wait_for_message -from redis.crc import key_slot from .conftest import ( _get_client, skip_if_redis_enterprise, diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index a93c75f793..4aff6bced2 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -9,7 +9,8 @@ import redis from redis.connection import ssl_available, to_bool -from .conftest import _get_client, skip_if_redis_enterprise, skip_if_server_version_lt +from .conftest import _get_client, skip_if_redis_enterprise, \ + skip_if_server_version_lt from .test_pubsub import wait_for_message @@ -45,7 +46,8 @@ def get_pool( def test_connection_creation(self): connection_kwargs = {"foo": "bar", "biz": "baz"} pool = self.get_pool( - connection_kwargs=connection_kwargs, connection_class=DummyConnection + connection_kwargs=connection_kwargs, + connection_class=DummyConnection ) connection = pool.get_connection("_") assert isinstance(connection, DummyConnection) @@ -60,7 +62,8 @@ def test_multiple_connections(self, master_host): def test_max_connections(self, master_host): connection_kwargs = {"host": master_host[0], "port": master_host[1]} - pool = self.get_pool(max_connections=2, connection_kwargs=connection_kwargs) + pool = self.get_pool(max_connections=2, + connection_kwargs=connection_kwargs) pool.get_connection("_") pool.get_connection("_") with pytest.raises(redis.ConnectionError): @@ -82,7 +85,8 @@ def test_repr_contains_db_info_tcp(self): "client_name": "test-client", } pool = self.get_pool( - connection_kwargs=connection_kwargs, connection_class=redis.Connection + connection_kwargs=connection_kwargs, + connection_class=redis.Connection ) expected = ( "ConnectionPool Date: Wed, 1 Dec 2021 16:17:36 +0200 Subject: [PATCH 7/9] Reformatted files with black --- redis/cluster.py | 9 +- redis/commands/__init__.py | 12 +- redis/commands/cluster.py | 65 +++++--- redis/commands/core.py | 296 ++++++++++++++++++++-------------- tests/conftest.py | 6 +- tests/test_cluster.py | 86 +++++----- tests/test_commands.py | 30 ++-- tests/test_connection_pool.py | 78 ++++----- tests/test_pubsub.py | 3 +- 9 files changed, 325 insertions(+), 260 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index c8df4669ab..2d9fd6810e 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -7,10 +7,7 @@ import time from collections import OrderedDict from redis.client import CaseInsensitiveDict, Redis, PubSub -from redis.commands import ( - RedisClusterCommands, - CommandsParser -) +from redis.commands import RedisClusterCommands, CommandsParser from redis.connection import DefaultParser, ConnectionPool, Encoder, parse_url from redis.crc import key_slot, REDIS_CLUSTER_HASH_SLOTS from redis.exceptions import ( @@ -674,8 +671,8 @@ def monitor(self, target_node=None): target_node = self.get_default_node() if target_node.redis_connection is None: raise RedisClusterException( - "Cluster Node {0} has no redis_connection". - format(target_node.name)) + "Cluster Node {0} has no redis_connection".format(target_node.name) + ) return target_node.redis_connection.monitor() def pubsub(self, node=None, host=None, port=None, **kwargs): diff --git a/redis/commands/__init__.py b/redis/commands/__init__.py index f04b98cc4d..07fa7f1431 100644 --- a/redis/commands/__init__.py +++ b/redis/commands/__init__.py @@ -6,10 +6,10 @@ from .sentinel import SentinelCommands __all__ = [ - 'RedisClusterCommands', - 'CommandsParser', - 'CoreCommands', - 'list_or_args', - 'RedisModuleCommands', - 'SentinelCommands' + "RedisClusterCommands", + "CommandsParser", + "CoreCommands", + "list_or_args", + "RedisModuleCommands", + "SentinelCommands", ] diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 1ef292f1f6..70821c84b8 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -3,12 +3,7 @@ RedisError, ) from redis.crc import key_slot -from .core import ( - ACLCommands, - DataAccessCommands, - ManagementCommands, - PubSubCommands -) +from .core import ACLCommands, DataAccessCommands, ManagementCommands, PubSubCommands from .helpers import list_or_args @@ -158,16 +153,15 @@ class ClusterManagementCommands(ManagementCommands): The class inherits from Redis's core ManagementCommands class and do the required adjustments to work with cluster mode """ + def slaveof(self, *args, **kwargs): raise RedisClusterException("SLAVEOF is not supported in cluster mode") def replicaof(self, *args, **kwargs): - raise RedisClusterException("REPLICAOF is not supported in cluster" - " mode") + raise RedisClusterException("REPLICAOF is not supported in cluster" " mode") def swapdb(self, *args, **kwargs): - raise RedisClusterException("SWAPDB is not supported in cluster" - " mode") + raise RedisClusterException("SWAPDB is not supported in cluster" " mode") class ClusterDataAccessCommands(DataAccessCommands): @@ -177,20 +171,43 @@ class ClusterDataAccessCommands(DataAccessCommands): The class inherits from Redis's core DataAccessCommand class and do the required adjustments to work with cluster mode """ - def stralgo(self, algo, value1, value2, specific_argument='strings', - len=False, idx=False, minmatchlen=None, withmatchlen=False, - **kwargs): - target_nodes = kwargs.pop('target_nodes', None) - if specific_argument == 'strings' and target_nodes is None: - target_nodes = 'default-node' - kwargs.update({'target_nodes': target_nodes}) - return super().stralgo(algo, value1, value2, specific_argument, - len, idx, minmatchlen, withmatchlen, **kwargs) - - -class RedisClusterCommands(ClusterMultiKeyCommands, ClusterManagementCommands, - ACLCommands, PubSubCommands, - ClusterDataAccessCommands): + + def stralgo( + self, + algo, + value1, + value2, + specific_argument="strings", + len=False, + idx=False, + minmatchlen=None, + withmatchlen=False, + **kwargs, + ): + target_nodes = kwargs.pop("target_nodes", None) + if specific_argument == "strings" and target_nodes is None: + target_nodes = "default-node" + kwargs.update({"target_nodes": target_nodes}) + return super().stralgo( + algo, + value1, + value2, + specific_argument, + len, + idx, + minmatchlen, + withmatchlen, + **kwargs, + ) + + +class RedisClusterCommands( + ClusterMultiKeyCommands, + ClusterManagementCommands, + ACLCommands, + PubSubCommands, + ClusterDataAccessCommands, +): """ A class for all Redis Cluster commands diff --git a/redis/commands/core.py b/redis/commands/core.py index 27f814ccc3..c30c166bf9 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -13,6 +13,7 @@ class ACLCommands: Redis Access Control List (ACL) commands. see: https://redis.io/topics/acl """ + def acl_cat(self, category=None, **kwargs): """ Returns a list of categories or commands within a category. @@ -24,7 +25,7 @@ def acl_cat(self, category=None, **kwargs): For more information check https://redis.io/commands/acl-cat """ pieces = [category] if category else [] - return self.execute_command('ACL CAT', *pieces, **kwargs) + return self.execute_command("ACL CAT", *pieces, **kwargs) def acl_deluser(self, *username, **kwargs): """ @@ -32,7 +33,7 @@ def acl_deluser(self, *username, **kwargs): For more information check https://redis.io/commands/acl-deluser """ - return self.execute_command('ACL DELUSER', *username, **kwargs) + return self.execute_command("ACL DELUSER", *username, **kwargs) def acl_genpass(self, bits=None, **kwargs): """Generate a random password value. @@ -47,9 +48,10 @@ def acl_genpass(self, bits=None, **kwargs): if b < 0 or b > 4096: raise ValueError except ValueError: - raise DataError('genpass optionally accepts a bits argument, ' - 'between 0 and 4096.') - return self.execute_command('ACL GENPASS', *pieces, **kwargs) + raise DataError( + "genpass optionally accepts a bits argument, " "between 0 and 4096." + ) + return self.execute_command("ACL GENPASS", *pieces, **kwargs) def acl_getuser(self, username, **kwargs): """ @@ -59,7 +61,7 @@ def acl_getuser(self, username, **kwargs): For more information check https://redis.io/commands/acl-getuser """ - return self.execute_command('ACL GETUSER', username, **kwargs) + return self.execute_command("ACL GETUSER", username, **kwargs) def acl_help(self, **kwargs): """The ACL HELP command returns helpful text describing @@ -67,7 +69,7 @@ def acl_help(self, **kwargs): For more information check https://redis.io/commands/acl-help """ - return self.execute_command('ACL HELP', **kwargs) + return self.execute_command("ACL HELP", **kwargs) def acl_list(self, **kwargs): """ @@ -75,7 +77,7 @@ def acl_list(self, **kwargs): For more information check https://redis.io/commands/acl-list """ - return self.execute_command('ACL LIST', **kwargs) + return self.execute_command("ACL LIST", **kwargs) def acl_log(self, count=None, **kwargs): """ @@ -91,7 +93,7 @@ def acl_log(self, count=None, **kwargs): raise DataError("ACL LOG count must be an " "integer") args.append(count) - return self.execute_command('ACL LOG', *args, **kwargs) + return self.execute_command("ACL LOG", *args, **kwargs) def acl_log_reset(self, **kwargs): """ @@ -100,8 +102,8 @@ def acl_log_reset(self, **kwargs): For more information check https://redis.io/commands/acl-log """ - args = [b'RESET'] - return self.execute_command('ACL LOG', *args, **kwargs) + args = [b"RESET"] + return self.execute_command("ACL LOG", *args, **kwargs) def acl_load(self, **kwargs): """ @@ -112,7 +114,7 @@ def acl_load(self, **kwargs): For more information check https://redis.io/commands/acl-load """ - return self.execute_command('ACL LOAD', **kwargs) + return self.execute_command("ACL LOAD", **kwargs) def acl_save(self, **kwargs): """ @@ -123,12 +125,23 @@ def acl_save(self, **kwargs): For more information check https://redis.io/commands/acl-save """ - return self.execute_command('ACL SAVE', **kwargs) + return self.execute_command("ACL SAVE", **kwargs) - def acl_setuser(self, username, enabled=False, nopass=False, - passwords=None, hashed_passwords=None, categories=None, - commands=None, keys=None, reset=False, reset_keys=False, - reset_passwords=False, **kwargs): + def acl_setuser( + self, + username, + enabled=False, + nopass=False, + passwords=None, + hashed_passwords=None, + categories=None, + commands=None, + keys=None, + reset=False, + reset_keys=False, + reset_passwords=False, + **kwargs, + ): """ Create or update an ACL user. @@ -279,33 +292,34 @@ def acl_setuser(self, username, enabled=False, nopass=False, key = encoder.encode(key) pieces.append(b"~%s" % key) - return self.execute_command('ACL SETUSER', *pieces, **kwargs) + return self.execute_command("ACL SETUSER", *pieces, **kwargs) def acl_users(self, **kwargs): """Returns a list of all registered users on the server. For more information check https://redis.io/commands/acl-users """ - return self.execute_command('ACL USERS', **kwargs) + return self.execute_command("ACL USERS", **kwargs) def acl_whoami(self, **kwargs): """Get the username for the current connection For more information check https://redis.io/commands/acl-whoami """ - return self.execute_command('ACL WHOAMI', **kwargs) + return self.execute_command("ACL WHOAMI", **kwargs) class ManagementCommands: """ Redis management commands """ + def bgrewriteaof(self, **kwargs): """Tell the Redis server to rewrite the AOF file from data in memory. For more information check https://redis.io/commands/bgrewriteaof """ - return self.execute_command('BGREWRITEAOF', **kwargs) + return self.execute_command("BGREWRITEAOF", **kwargs) def bgsave(self, schedule=True, **kwargs): """ @@ -317,17 +331,25 @@ def bgsave(self, schedule=True, **kwargs): pieces = [] if schedule: pieces.append("SCHEDULE") - return self.execute_command('BGSAVE', *pieces, **kwargs) + return self.execute_command("BGSAVE", *pieces, **kwargs) def client_kill(self, address, **kwargs): """Disconnects the client at ``address`` (ip:port) For more information check https://redis.io/commands/client-kill """ - return self.execute_command('CLIENT KILL', address, **kwargs) + return self.execute_command("CLIENT KILL", address, **kwargs) - def client_kill_filter(self, _id=None, _type=None, addr=None, - skipme=None, laddr=None, user=None, **kwargs): + def client_kill_filter( + self, + _id=None, + _type=None, + addr=None, + skipme=None, + laddr=None, + user=None, + **kwargs, + ): """ Disconnects client(s) using a variety of filter options :param id: Kills a client by its unique ID field @@ -362,9 +384,11 @@ def client_kill_filter(self, _id=None, _type=None, addr=None, if user is not None: args.extend((b"USER", user)) if not args: - raise DataError("CLIENT KILL ... ... " - " must specify at least one filter") - return self.execute_command('CLIENT KILL', *args, **kwargs) + raise DataError( + "CLIENT KILL ... ... " + " must specify at least one filter" + ) + return self.execute_command("CLIENT KILL", *args, **kwargs) def client_info(self, **kwargs): """ @@ -373,7 +397,7 @@ def client_info(self, **kwargs): For more information check https://redis.io/commands/client-info """ - return self.execute_command('CLIENT INFO', **kwargs) + return self.execute_command("CLIENT INFO", **kwargs) def client_list(self, _type=None, client_id=[], **kwargs): """ @@ -396,8 +420,8 @@ def client_list(self, _type=None, client_id=[], **kwargs): raise DataError("client_id must be a list") if client_id != []: args.append(b"ID") - args.append(' '.join(client_id)) - return self.execute_command('CLIENT LIST', *args, **kwargs) + args.append(" ".join(client_id)) + return self.execute_command("CLIENT LIST", *args, **kwargs) def client_getname(self, **kwargs): """ @@ -405,7 +429,7 @@ def client_getname(self, **kwargs): For more information check https://redis.io/commands/client-getname """ - return self.execute_command('CLIENT GETNAME', **kwargs) + return self.execute_command("CLIENT GETNAME", **kwargs) def client_getredir(self, **kwargs): """ @@ -414,7 +438,7 @@ def client_getredir(self, **kwargs): see: https://redis.io/commands/client-getredir """ - return self.execute_command('CLIENT GETREDIR', **kwargs) + return self.execute_command("CLIENT GETREDIR", **kwargs) def client_reply(self, reply, **kwargs): """ @@ -434,7 +458,7 @@ def client_reply(self, reply, **kwargs): """ replies = ["ON", "OFF", "SKIP"] if reply not in replies: - raise DataError('CLIENT REPLY must be one of %r' % replies) + raise DataError("CLIENT REPLY must be one of %r" % replies) return self.execute_command("CLIENT REPLY", reply, **kwargs) def client_id(self, **kwargs): @@ -443,7 +467,7 @@ def client_id(self, **kwargs): For more information check https://redis.io/commands/client-id """ - return self.execute_command('CLIENT ID', **kwargs) + return self.execute_command("CLIENT ID", **kwargs) def client_trackinginfo(self, **kwargs): """ @@ -452,7 +476,7 @@ def client_trackinginfo(self, **kwargs): See https://redis.io/commands/client-trackinginfo """ - return self.execute_command('CLIENT TRACKINGINFO', **kwargs) + return self.execute_command("CLIENT TRACKINGINFO", **kwargs) def client_setname(self, name, **kwargs): """ @@ -460,7 +484,7 @@ def client_setname(self, name, **kwargs): For more information check https://redis.io/commands/client-setname """ - return self.execute_command('CLIENT SETNAME', name, **kwargs) + return self.execute_command("CLIENT SETNAME", name, **kwargs) def client_unblock(self, client_id, error=False, **kwargs): """ @@ -473,7 +497,7 @@ def client_unblock(self, client_id, error=False, **kwargs): """ args = ["CLIENT UNBLOCK", int(client_id)] if error: - args.append(b'ERROR') + args.append(b"ERROR") return self.execute_command(*args, **kwargs) def client_pause(self, timeout, **kwargs): @@ -485,7 +509,7 @@ def client_pause(self, timeout, **kwargs): """ if not isinstance(timeout, int): raise DataError("CLIENT PAUSE timeout must be an integer") - return self.execute_command('CLIENT PAUSE', str(timeout), **kwargs) + return self.execute_command("CLIENT PAUSE", str(timeout), **kwargs) def client_unpause(self, **kwargs): """ @@ -493,7 +517,7 @@ def client_unpause(self, **kwargs): For more information check https://redis.io/commands/client-unpause """ - return self.execute_command('CLIENT UNPAUSE', **kwargs) + return self.execute_command("CLIENT UNPAUSE", **kwargs) def command(self, **kwargs): """ @@ -501,7 +525,7 @@ def command(self, **kwargs): For more information check https://redis.io/commands/command """ - return self.execute_command('COMMAND', **kwargs) + return self.execute_command("COMMAND", **kwargs) def command_info(self, **kwargs): raise NotImplementedError( @@ -509,7 +533,7 @@ def command_info(self, **kwargs): ) def command_count(self, **kwargs): - return self.execute_command('COMMAND COUNT', **kwargs) + return self.execute_command("COMMAND COUNT", **kwargs) def config_get(self, pattern="*", **kwargs): """ @@ -517,14 +541,14 @@ def config_get(self, pattern="*", **kwargs): For more information check https://redis.io/commands/config-get """ - return self.execute_command('CONFIG GET', pattern, **kwargs) + return self.execute_command("CONFIG GET", pattern, **kwargs) def config_set(self, name, value, **kwargs): """Set config item ``name`` with ``value`` For more information check https://redis.io/commands/config-set """ - return self.execute_command('CONFIG SET', name, value, **kwargs) + return self.execute_command("CONFIG SET", name, value, **kwargs) def config_resetstat(self, **kwargs): """ @@ -532,7 +556,7 @@ def config_resetstat(self, **kwargs): For more information check https://redis.io/commands/config-resetstat """ - return self.execute_command('CONFIG RESETSTAT', **kwargs) + return self.execute_command("CONFIG RESETSTAT", **kwargs) def config_rewrite(self, **kwargs): """ @@ -540,7 +564,7 @@ def config_rewrite(self, **kwargs): For more information check https://redis.io/commands/config-rewrite """ - return self.execute_command('CONFIG REWRITE', **kwargs) + return self.execute_command("CONFIG REWRITE", **kwargs) def dbsize(self, **kwargs): """ @@ -548,7 +572,7 @@ def dbsize(self, **kwargs): For more information check https://redis.io/commands/dbsize """ - return self.execute_command('DBSIZE', **kwargs) + return self.execute_command("DBSIZE", **kwargs) def debug_object(self, key, **kwargs): """ @@ -556,7 +580,7 @@ def debug_object(self, key, **kwargs): For more information check https://redis.io/commands/debug-object """ - return self.execute_command('DEBUG OBJECT', key, **kwargs) + return self.execute_command("DEBUG OBJECT", key, **kwargs) def debug_segfault(self, **kwargs): raise NotImplementedError( @@ -573,7 +597,7 @@ def echo(self, value, **kwargs): For more information check https://redis.io/commands/echo """ - return self.execute_command('ECHO', value, **kwargs) + return self.execute_command("ECHO", value, **kwargs) def flushall(self, asynchronous=False, **kwargs): """ @@ -586,8 +610,8 @@ def flushall(self, asynchronous=False, **kwargs): """ args = [] if asynchronous: - args.append(b'ASYNC') - return self.execute_command('FLUSHALL', *args, **kwargs) + args.append(b"ASYNC") + return self.execute_command("FLUSHALL", *args, **kwargs) def flushdb(self, asynchronous=False, **kwargs): """ @@ -600,8 +624,8 @@ def flushdb(self, asynchronous=False, **kwargs): """ args = [] if asynchronous: - args.append(b'ASYNC') - return self.execute_command('FLUSHDB', *args, **kwargs) + args.append(b"ASYNC") + return self.execute_command("FLUSHDB", *args, **kwargs) def swapdb(self, first, second, **kwargs): """ @@ -609,7 +633,7 @@ def swapdb(self, first, second, **kwargs): For more information check https://redis.io/commands/swapdb """ - return self.execute_command('SWAPDB', first, second, **kwargs) + return self.execute_command("SWAPDB", first, second, **kwargs) def info(self, section=None, **kwargs): """ @@ -624,9 +648,9 @@ def info(self, section=None, **kwargs): For more information check https://redis.io/commands/info """ if section is None: - return self.execute_command('INFO', **kwargs) + return self.execute_command("INFO", **kwargs) else: - return self.execute_command('INFO', section, **kwargs) + return self.execute_command("INFO", section, **kwargs) def lastsave(self, **kwargs): """ @@ -635,7 +659,7 @@ def lastsave(self, **kwargs): For more information check https://redis.io/commands/lastsave """ - return self.execute_command('LASTSAVE', **kwargs) + return self.execute_command("LASTSAVE", **kwargs) def lolwut(self, *version_numbers, **kwargs): """ @@ -644,13 +668,22 @@ def lolwut(self, *version_numbers, **kwargs): See: https://redis.io/commands/lolwut """ if version_numbers: - return self.execute_command('LOLWUT VERSION', *version_numbers, - **kwargs) + return self.execute_command("LOLWUT VERSION", *version_numbers, **kwargs) else: - return self.execute_command('LOLWUT', **kwargs) + return self.execute_command("LOLWUT", **kwargs) - def migrate(self, host, port, keys, destination_db, timeout, - copy=False, replace=False, auth=None, **kwargs): + def migrate( + self, + host, + port, + keys, + destination_db, + timeout, + copy=False, + replace=False, + auth=None, + **kwargs, + ): """ Migrate 1 or more keys from the current Redis server to a different server specified by the ``host``, ``port`` and ``destination_db``. @@ -683,15 +716,17 @@ def migrate(self, host, port, keys, destination_db, timeout, pieces.append(auth) pieces.append(b"KEYS") pieces.extend(keys) - return self.execute_command('MIGRATE', host, port, '', destination_db, - timeout, *pieces, **kwargs) + return self.execute_command( + "MIGRATE", host, port, "", destination_db, timeout, *pieces, **kwargs + ) def object(self, infotype, key, **kwargs): """ Return the encoding, idletime, or refcount about the key """ - return self.execute_command('OBJECT', infotype, key, infotype=infotype, - **kwargs) + return self.execute_command( + "OBJECT", infotype, key, infotype=infotype, **kwargs + ) def memory_doctor(self, **kwargs): raise NotImplementedError( @@ -717,7 +752,7 @@ def memory_stats(self, **kwargs): For more information check https://redis.io/commands/memory-stats """ - return self.execute_command('MEMORY STATS', **kwargs) + return self.execute_command("MEMORY STATS", **kwargs) def memory_malloc_stats(self, **kwargs): """ @@ -725,7 +760,7 @@ def memory_malloc_stats(self, **kwargs): See: https://redis.io/commands/memory-malloc-stats """ - return self.execute_command('MEMORY MALLOC-STATS', **kwargs) + return self.execute_command("MEMORY MALLOC-STATS", **kwargs) def memory_usage(self, key, samples=None, **kwargs): """ @@ -740,8 +775,8 @@ def memory_usage(self, key, samples=None, **kwargs): """ args = [] if isinstance(samples, int): - args.extend([b'SAMPLES', samples]) - return self.execute_command('MEMORY USAGE', key, *args, **kwargs) + args.extend([b"SAMPLES", samples]) + return self.execute_command("MEMORY USAGE", key, *args, **kwargs) def memory_purge(self, **kwargs): """ @@ -749,7 +784,7 @@ def memory_purge(self, **kwargs): For more information check https://redis.io/commands/memory-purge """ - return self.execute_command('MEMORY PURGE', **kwargs) + return self.execute_command("MEMORY PURGE", **kwargs) def ping(self, **kwargs): """ @@ -757,7 +792,7 @@ def ping(self, **kwargs): For more information check https://redis.io/commands/ping """ - return self.execute_command('PING', **kwargs) + return self.execute_command("PING", **kwargs) def quit(self, **kwargs): """ @@ -765,7 +800,7 @@ def quit(self, **kwargs): For more information check https://redis.io/commands/quit """ - return self.execute_command('QUIT', **kwargs) + return self.execute_command("QUIT", **kwargs) def replicaof(self, *args, **kwargs): """ @@ -776,7 +811,7 @@ def replicaof(self, *args, **kwargs): For more information check https://redis.io/commands/replicaof """ - return self.execute_command('REPLICAOF', *args, **kwargs) + return self.execute_command("REPLICAOF", *args, **kwargs) def save(self, **kwargs): """ @@ -785,7 +820,7 @@ def save(self, **kwargs): For more information check https://redis.io/commands/save """ - return self.execute_command('SAVE', **kwargs) + return self.execute_command("SAVE", **kwargs) def shutdown(self, save=False, nosave=False, **kwargs): """Shutdown the Redis server. If Redis has persistence configured, @@ -819,8 +854,8 @@ def slaveof(self, host=None, port=None, **kwargs): For more information check https://redis.io/commands/slaveof """ if host is None and port is None: - return self.execute_command('SLAVEOF', b'NO', b'ONE', **kwargs) - return self.execute_command('SLAVEOF', host, port, **kwargs) + return self.execute_command("SLAVEOF", b"NO", b"ONE", **kwargs) + return self.execute_command("SLAVEOF", host, port, **kwargs) def slowlog_get(self, num=None, **kwargs): """ @@ -832,10 +867,8 @@ def slowlog_get(self, num=None, **kwargs): args = ["SLOWLOG GET"] if num is not None: args.append(num) - decode_responses = self.get_connection_kwargs().get( - 'decode_responses', False) - return self.execute_command(*args, decode_responses=decode_responses, - **kwargs) + decode_responses = self.get_connection_kwargs().get("decode_responses", False) + return self.execute_command(*args, decode_responses=decode_responses, **kwargs) def slowlog_len(self, **kwargs): """ @@ -843,7 +876,7 @@ def slowlog_len(self, **kwargs): For more information check https://redis.io/commands/slowlog-len """ - return self.execute_command('SLOWLOG LEN', **kwargs) + return self.execute_command("SLOWLOG LEN", **kwargs) def slowlog_reset(self, **kwargs): """ @@ -851,7 +884,7 @@ def slowlog_reset(self, **kwargs): For more information check https://redis.io/commands/slowlog-reset """ - return self.execute_command('SLOWLOG RESET', **kwargs) + return self.execute_command("SLOWLOG RESET", **kwargs) def time(self, **kwargs): """ @@ -860,7 +893,7 @@ def time(self, **kwargs): For more information check https://redis.io/commands/time """ - return self.execute_command('TIME', **kwargs) + return self.execute_command("TIME", **kwargs) def wait(self, num_replicas, timeout, **kwargs): """ @@ -871,7 +904,7 @@ def wait(self, num_replicas, timeout, **kwargs): For more information check https://redis.io/commands/wait """ - return self.execute_command('WAIT', num_replicas, timeout, **kwargs) + return self.execute_command("WAIT", num_replicas, timeout, **kwargs) class BasicKeyCommands: @@ -1183,13 +1216,13 @@ def incrbyfloat(self, name, amount=1.0): """ return self.execute_command("INCRBYFLOAT", name, amount) - def keys(self, pattern='*', **kwargs): + def keys(self, pattern="*", **kwargs): """ Returns a list of keys matching ``pattern`` For more information check https://redis.io/commands/keys """ - return self.execute_command('KEYS', pattern, **kwargs) + return self.execute_command("KEYS", pattern, **kwargs) def lmove(self, first_list, second_list, src="LEFT", dest="RIGHT"): """ @@ -1341,7 +1374,7 @@ def randomkey(self, **kwargs): For more information check https://redis.io/commands/randomkey """ - return self.execute_command('RANDOMKEY', **kwargs) + return self.execute_command("RANDOMKEY", **kwargs) def rename(self, src, dst): """ @@ -1542,9 +1575,18 @@ def setrange(self, name, offset, value): """ return self.execute_command("SETRANGE", name, offset, value) - def stralgo(self, algo, value1, value2, specific_argument='strings', - len=False, idx=False, minmatchlen=None, withmatchlen=False, - **kwargs): + def stralgo( + self, + algo, + value1, + value2, + specific_argument="strings", + len=False, + idx=False, + minmatchlen=None, + withmatchlen=False, + **kwargs, + ): """ Implements complex algorithms that operate on strings. Right now the only algorithm implemented is the LCS algorithm @@ -1585,12 +1627,17 @@ def stralgo(self, algo, value1, value2, specific_argument='strings', except TypeError: pass if withmatchlen: - pieces.append(b'WITHMATCHLEN') + pieces.append(b"WITHMATCHLEN") - return self.execute_command('STRALGO', *pieces, len=len, idx=idx, - minmatchlen=minmatchlen, - withmatchlen=withmatchlen, - **kwargs) + return self.execute_command( + "STRALGO", + *pieces, + len=len, + idx=idx, + minmatchlen=minmatchlen, + withmatchlen=withmatchlen, + **kwargs, + ) def strlen(self, name): """ @@ -1980,6 +2027,7 @@ class ScanCommands: Redis SCAN commands. see: https://redis.io/commands/scan """ + def scan(self, cursor=0, match=None, count=None, _type=None, **kwargs): """ Incrementally return lists of key names. Also return a cursor @@ -2003,8 +2051,8 @@ def scan(self, cursor=0, match=None, count=None, _type=None, **kwargs): if count is not None: pieces.extend([b"COUNT", count]) if _type is not None: - pieces.extend([b'TYPE', _type]) - return self.execute_command('SCAN', *pieces, **kwargs) + pieces.extend([b"TYPE", _type]) + return self.execute_command("SCAN", *pieces, **kwargs) def scan_iter(self, match=None, count=None, _type=None, **kwargs): """ @@ -2023,8 +2071,9 @@ def scan_iter(self, match=None, count=None, _type=None, **kwargs): """ cursor = "0" while cursor != 0: - cursor, data = self.scan(cursor=cursor, match=match, - count=count, _type=_type, **kwargs) + cursor, data = self.scan( + cursor=cursor, match=match, count=count, _type=_type, **kwargs + ) yield from data def sscan(self, name, cursor=0, match=None, count=None): @@ -3627,6 +3676,7 @@ class PubSubCommands: Redis PubSub commands. see https://redis.io/topics/pubsub """ + def publish(self, channel, message, **kwargs): """ Publish ``message`` on ``channel``. @@ -3634,15 +3684,15 @@ def publish(self, channel, message, **kwargs): For more information check https://redis.io/commands/publish """ - return self.execute_command('PUBLISH', channel, message, **kwargs) + return self.execute_command("PUBLISH", channel, message, **kwargs) - def pubsub_channels(self, pattern='*', **kwargs): + def pubsub_channels(self, pattern="*", **kwargs): """ Return a list of channels that have at least one subscriber For more information check https://redis.io/commands/pubsub-channels """ - return self.execute_command('PUBSUB CHANNELS', pattern, **kwargs) + return self.execute_command("PUBSUB CHANNELS", pattern, **kwargs) def pubsub_numpat(self, **kwargs): """ @@ -3650,7 +3700,7 @@ def pubsub_numpat(self, **kwargs): For more information check https://redis.io/commands/pubsub-numpat """ - return self.execute_command('PUBSUB NUMPAT', **kwargs) + return self.execute_command("PUBSUB NUMPAT", **kwargs) def pubsub_numsub(self, *args, **kwargs): """ @@ -3659,7 +3709,7 @@ def pubsub_numsub(self, *args, **kwargs): For more information check https://redis.io/commands/pubsub-numsub """ - return self.execute_command('PUBSUB NUMSUB', *args, **kwargs) + return self.execute_command("PUBSUB NUMSUB", *args, **kwargs) class ScriptCommands: @@ -4353,9 +4403,9 @@ class ClusterCommands: """ Class for Redis Cluster commands """ + def cluster(self, cluster_arg, *args, **kwargs): - return self.execute_command('CLUSTER %s' % cluster_arg.upper(), *args, - **kwargs) + return self.execute_command("CLUSTER %s" % cluster_arg.upper(), *args, **kwargs) def readwrite(self, **kwargs): """ @@ -4363,7 +4413,7 @@ def readwrite(self, **kwargs): For more information check https://redis.io/commands/readwrite """ - return self.execute_command('READWRITE', **kwargs) + return self.execute_command("READWRITE", **kwargs) def readonly(self, **kwargs): """ @@ -4371,23 +4421,35 @@ def readonly(self, **kwargs): For more information check https://redis.io/commands/readonly """ - return self.execute_command('READONLY', **kwargs) + return self.execute_command("READONLY", **kwargs) -class DataAccessCommands(BasicKeyCommands, ListCommands, - ScanCommands, SetCommands, StreamCommands, - SortedSetCommands, - HyperlogCommands, HashCommands, GeoCommands, - ): +class DataAccessCommands( + BasicKeyCommands, + HyperlogCommands, + HashCommands, + GeoCommands, + ListCommands, + ScanCommands, + SetCommands, + StreamCommands, + SortedSetCommands, +): """ A class containing all of the implemented data access redis commands. This class is to be used as a mixin. """ -class CoreCommands(ACLCommands, ClusterCommands, DataAccessCommands, - ManagementCommands, ModuleCommands, PubSubCommands, - ScriptCommands): +class CoreCommands( + ACLCommands, + ClusterCommands, + DataAccessCommands, + ManagementCommands, + ModuleCommands, + PubSubCommands, + ScriptCommands, +): """ A class containing all of the implemented redis commands. This class is to be used as a mixin. diff --git a/tests/conftest.py b/tests/conftest.py index 7ef3052f22..32662ba347 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -331,11 +331,11 @@ def wait_for_command(client, monitor, command, key=None): if key is None: # generate key redis_version = REDIS_INFO["version"] - if LooseVersion(redis_version) >= LooseVersion('5.0.0'): + if LooseVersion(redis_version) >= LooseVersion("5.0.0"): id_str = str(client.client_id()) else: - id_str = '%08x' % random.randrange(2**32) - key = '__REDIS-PY-%s__' % id_str + id_str = "%08x" % random.randrange(2 ** 32) + key = "__REDIS-PY-%s__" % id_str client.get(key) while True: monitor_response = monitor.next_command() diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 7f4644555f..b88673125f 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -36,7 +36,7 @@ skip_if_redis_enterprise, skip_if_server_version_lt, skip_unless_arch_bits, - wait_for_command + wait_for_command, ) default_host = "127.0.0.1" @@ -271,7 +271,7 @@ def test_execute_command_node_flag_primaries(self, r): """ primaries = r.get_primaries() replicas = r.get_replicas() - mock_all_nodes_resp(r, 'PONG') + mock_all_nodes_resp(r, "PONG") assert r.ping(target_nodes=RedisCluster.PRIMARIES) is True for primary in primaries: conn = primary.redis_connection.connection @@ -288,7 +288,7 @@ def test_execute_command_node_flag_replicas(self, r): if not replicas: r = get_mocked_redis_client(default_host, default_port) primaries = r.get_primaries() - mock_all_nodes_resp(r, 'PONG') + mock_all_nodes_resp(r, "PONG") assert r.ping(target_nodes=RedisCluster.REPLICAS) is True for replica in replicas: conn = replica.redis_connection.connection @@ -301,7 +301,7 @@ def test_execute_command_node_flag_all_nodes(self, r): """ Test command execution with nodes flag ALL_NODES """ - mock_all_nodes_resp(r, 'PONG') + mock_all_nodes_resp(r, "PONG") assert r.ping(target_nodes=RedisCluster.ALL_NODES) is True for node in r.get_nodes(): conn = node.redis_connection.connection @@ -311,7 +311,7 @@ def test_execute_command_node_flag_random(self, r): """ Test command execution with nodes flag RANDOM """ - mock_all_nodes_resp(r, 'PONG') + mock_all_nodes_resp(r, "PONG") assert r.ping(target_nodes=RedisCluster.RANDOM) is True called_count = 0 for node in r.get_nodes(): @@ -1142,7 +1142,7 @@ def test_lastsave(self, r): def test_cluster_echo(self, r): node = r.get_primaries()[0] - assert r.echo('foo bar', target_nodes=node) == b'foo bar' + assert r.echo("foo bar", target_nodes=node) == b"foo bar" @skip_if_server_version_lt("1.0.0") def test_debug_segfault(self, r): @@ -1774,40 +1774,46 @@ def test_cluster_randomkey(self, r): @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise() def test_acl_log(self, r, request): - key = '{cache}:' + key = "{cache}:" node = r.get_node_from_key(key) - username = 'redis-py-user' + username = "redis-py-user" def teardown(): - r.acl_deluser(username, target_nodes='primaries') + r.acl_deluser(username, target_nodes="primaries") request.addfinalizer(teardown) - r.acl_setuser(username, enabled=True, reset=True, - commands=['+get', '+set', '+select', '+cluster', - '+command'], keys=['{cache}:*'], nopass=True, - target_nodes='primaries') + r.acl_setuser( + username, + enabled=True, + reset=True, + commands=["+get", "+set", "+select", "+cluster", "+command"], + keys=["{cache}:*"], + nopass=True, + target_nodes="primaries", + ) r.acl_log_reset(target_nodes=node) - user_client = _get_client(RedisCluster, request, flushdb=False, - username=username) + user_client = _get_client( + RedisCluster, request, flushdb=False, username=username + ) # Valid operation and key - assert user_client.set('{cache}:0', 1) - assert user_client.get('{cache}:0') == b'1' + assert user_client.set("{cache}:0", 1) + assert user_client.get("{cache}:0") == b"1" # Invalid key with pytest.raises(NoPermissionError): - user_client.get('{cache}violated_cache:0') + user_client.get("{cache}violated_cache:0") # Invalid operation with pytest.raises(NoPermissionError): - user_client.hset('{cache}:0', 'hkey', 'hval') + user_client.hset("{cache}:0", "hkey", "hval") assert isinstance(r.acl_log(target_nodes=node), list) assert len(r.acl_log(target_nodes=node)) == 2 assert len(r.acl_log(count=1, target_nodes=node)) == 1 assert isinstance(r.acl_log(target_nodes=node)[0], dict) - assert 'client-info' in r.acl_log(count=1, target_nodes=node)[0] + assert "client-info" in r.acl_log(count=1, target_nodes=node)[0] assert r.acl_log_reset(target_nodes=node) @@ -2614,48 +2620,48 @@ def test_readonly_pipeline_from_readonly_client(self, request): class TestClusterMonitor: def test_wait_command_not_found(self, r): "Make sure the wait_for_command func works when command is not found" - key = 'foo' + key = "foo" node = r.get_node_from_key(key) with r.monitor(target_node=node) as m: - response = wait_for_command(r, m, 'nothing', key=key) + response = wait_for_command(r, m, "nothing", key=key) assert response is None def test_response_values(self, r): db = 0 - key = 'foo' + key = "foo" node = r.get_node_from_key(key) with r.monitor(target_node=node) as m: r.ping(target_nodes=node) - response = wait_for_command(r, m, 'PING', key=key) - assert isinstance(response['time'], float) - assert response['db'] == db - assert response['client_type'] in ('tcp', 'unix') - assert isinstance(response['client_address'], str) - assert isinstance(response['client_port'], str) - assert response['command'] == 'PING' + response = wait_for_command(r, m, "PING", key=key) + assert isinstance(response["time"], float) + assert response["db"] == db + assert response["client_type"] in ("tcp", "unix") + assert isinstance(response["client_address"], str) + assert isinstance(response["client_port"], str) + assert response["command"] == "PING" def test_command_with_quoted_key(self, r): - key = '{foo}1' + key = "{foo}1" node = r.get_node_from_key(key) with r.monitor(node) as m: r.get('{foo}"bar') response = wait_for_command(r, m, 'GET {foo}"bar', key=key) - assert response['command'] == 'GET {foo}"bar' + assert response["command"] == 'GET {foo}"bar' def test_command_with_binary_data(self, r): - key = '{foo}1' + key = "{foo}1" node = r.get_node_from_key(key) with r.monitor(target_node=node) as m: - byte_string = b'{foo}bar\x92' + byte_string = b"{foo}bar\x92" r.get(byte_string) - response = wait_for_command(r, m, 'GET {foo}bar\\x92', key=key) - assert response['command'] == 'GET {foo}bar\\x92' + response = wait_for_command(r, m, "GET {foo}bar\\x92", key=key) + assert response["command"] == "GET {foo}bar\\x92" def test_command_with_escaped_data(self, r): - key = '{foo}1' + key = "{foo}1" node = r.get_node_from_key(key) with r.monitor(target_node=node) as m: - byte_string = b'{foo}bar\\x92' + byte_string = b"{foo}bar\\x92" r.get(byte_string) - response = wait_for_command(r, m, 'GET {foo}bar\\\\x92', key=key) - assert response['command'] == 'GET {foo}bar\\\\x92' + response = wait_for_command(r, m, "GET {foo}bar\\\\x92", key=key) + assert response["command"] == "GET {foo}bar\\\\x92" diff --git a/tests/test_commands.py b/tests/test_commands.py index fa5dae0cf0..d573b151e2 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -529,14 +529,18 @@ def test_client_kill_filter_by_laddr(self, r, r2): client_2_addr = clients_by_name["redis-py-c2"].get("laddr") assert r.client_kill_filter(laddr=client_2_addr) - @skip_if_server_version_lt('6.0.0') + @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise() def test_client_kill_filter_by_user(self, r, request): - killuser = 'user_to_kill' - r.acl_setuser(killuser, enabled=True, reset=True, - commands=['+get', '+set', '+select', '+cluster', - '+command'], - keys=['cache:*'], nopass=True) + killuser = "user_to_kill" + r.acl_setuser( + killuser, + enabled=True, + reset=True, + commands=["+get", "+set", "+select", "+cluster", "+command"], + keys=["cache:*"], + nopass=True, + ) _get_client(redis.Redis, request, flushdb=False, username=killuser) r.client_kill_filter(user=killuser) clients = r.client_list() @@ -545,7 +549,7 @@ def test_client_kill_filter_by_user(self, r, request): r.acl_deluser(killuser) @pytest.mark.onlynoncluster - @skip_if_server_version_lt('2.9.50') + @skip_if_server_version_lt("2.9.50") @skip_if_redis_enterprise() def test_client_pause(self, r): assert r.client_pause(1) @@ -554,7 +558,7 @@ def test_client_pause(self, r): r.client_pause(timeout="not an integer") @pytest.mark.onlynoncluster - @skip_if_server_version_lt('6.2.0') + @skip_if_server_version_lt("6.2.0") @skip_if_redis_enterprise() def test_client_unpause(self, r): assert r.client_unpause() == b"OK" @@ -574,7 +578,7 @@ def test_client_reply(self, r, r_timeout): assert r.get("foo") == b"bar" @pytest.mark.onlynoncluster - @skip_if_server_version_lt('6.0.0') + @skip_if_server_version_lt("6.0.0") @skip_if_redis_enterprise() def test_client_getredir(self, r): assert isinstance(r.client_getredir(), int) @@ -2654,7 +2658,7 @@ def test_cluster_slaves(self, mock_cluster_resp_slaves): assert isinstance(mock_cluster_resp_slaves.cluster("slaves", "nodeid"), dict) @pytest.mark.onlynoncluster - @skip_if_server_version_lt('3.0.0') + @skip_if_server_version_lt("3.0.0") @skip_if_redis_enterprise() def test_readwrite(self, r): assert r.readwrite() @@ -4036,7 +4040,7 @@ def test_memory_usage(self, r): assert isinstance(r.memory_usage("foo"), int) @pytest.mark.onlynoncluster - @skip_if_server_version_lt('4.0.0') + @skip_if_server_version_lt("4.0.0") @skip_if_redis_enterprise() def test_module_list(self, r): assert isinstance(r.module_list(), list) @@ -4077,7 +4081,7 @@ def test_command(self, r): assert "get" in cmds @pytest.mark.onlynoncluster - @skip_if_server_version_lt('4.0.0') + @skip_if_server_version_lt("4.0.0") @skip_if_redis_enterprise() def test_module(self, r): with pytest.raises(redis.exceptions.ModuleError) as excinfo: @@ -4133,7 +4137,7 @@ def test_restore_frequency(self, r): assert r.get(key) == b"blee!" @pytest.mark.onlynoncluster - @skip_if_server_version_lt('5.0.0') + @skip_if_server_version_lt("5.0.0") @skip_if_redis_enterprise() def test_replicaof(self, r): with pytest.raises(redis.ResponseError): diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 4aff6bced2..4717a7f729 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -9,8 +9,7 @@ import redis from redis.connection import ssl_available, to_bool -from .conftest import _get_client, skip_if_redis_enterprise, \ - skip_if_server_version_lt +from .conftest import _get_client, skip_if_redis_enterprise, skip_if_server_version_lt from .test_pubsub import wait_for_message @@ -46,8 +45,7 @@ def get_pool( def test_connection_creation(self): connection_kwargs = {"foo": "bar", "biz": "baz"} pool = self.get_pool( - connection_kwargs=connection_kwargs, - connection_class=DummyConnection + connection_kwargs=connection_kwargs, connection_class=DummyConnection ) connection = pool.get_connection("_") assert isinstance(connection, DummyConnection) @@ -62,8 +60,7 @@ def test_multiple_connections(self, master_host): def test_max_connections(self, master_host): connection_kwargs = {"host": master_host[0], "port": master_host[1]} - pool = self.get_pool(max_connections=2, - connection_kwargs=connection_kwargs) + pool = self.get_pool(max_connections=2, connection_kwargs=connection_kwargs) pool.get_connection("_") pool.get_connection("_") with pytest.raises(redis.ConnectionError): @@ -85,8 +82,7 @@ def test_repr_contains_db_info_tcp(self): "client_name": "test-client", } pool = self.get_pool( - connection_kwargs=connection_kwargs, - connection_class=redis.Connection + connection_kwargs=connection_kwargs, connection_class=redis.Connection ) expected = ( "ConnectionPool Date: Wed, 1 Dec 2021 16:45:41 +0200 Subject: [PATCH 8/9] Fixed linters --- redis/cluster.py | 11 ++++++----- redis/commands/cluster.py | 6 ++---- redis/commands/core.py | 4 ++-- tests/conftest.py | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 2d9fd6810e..e9db6894c2 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -6,10 +6,11 @@ import threading import time from collections import OrderedDict -from redis.client import CaseInsensitiveDict, Redis, PubSub -from redis.commands import RedisClusterCommands, CommandsParser -from redis.connection import DefaultParser, ConnectionPool, Encoder, parse_url -from redis.crc import key_slot, REDIS_CLUSTER_HASH_SLOTS + +from redis.client import CaseInsensitiveDict, PubSub, Redis +from redis.commands import CommandsParser, RedisClusterCommands +from redis.connection import ConnectionPool, DefaultParser, Encoder, parse_url +from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot from redis.exceptions import ( AskError, BusyLoadingError, @@ -671,7 +672,7 @@ def monitor(self, target_node=None): target_node = self.get_default_node() if target_node.redis_connection is None: raise RedisClusterException( - "Cluster Node {0} has no redis_connection".format(target_node.name) + f"Cluster Node {target_node.name} has no redis_connection" ) return target_node.redis_connection.monitor() diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 70821c84b8..919f4dfc7e 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -1,8 +1,6 @@ -from redis.exceptions import ( - RedisClusterException, - RedisError, -) from redis.crc import key_slot +from redis.exceptions import RedisClusterException, RedisError + from .core import ACLCommands, DataAccessCommands, ManagementCommands, PubSubCommands from .helpers import list_or_args diff --git a/redis/commands/core.py b/redis/commands/core.py index c30c166bf9..462fba7f82 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -458,7 +458,7 @@ def client_reply(self, reply, **kwargs): """ replies = ["ON", "OFF", "SKIP"] if reply not in replies: - raise DataError("CLIENT REPLY must be one of %r" % replies) + raise DataError(f"CLIENT REPLY must be one of {replies!r}") return self.execute_command("CLIENT REPLY", reply, **kwargs) def client_id(self, **kwargs): @@ -4405,7 +4405,7 @@ class ClusterCommands: """ def cluster(self, cluster_arg, *args, **kwargs): - return self.execute_command("CLUSTER %s" % cluster_arg.upper(), *args, **kwargs) + return self.execute_command(f"CLUSTER {cluster_arg.upper()}", *args, **kwargs) def readwrite(self, **kwargs): """ diff --git a/tests/conftest.py b/tests/conftest.py index 32662ba347..ab29ee4fcd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -334,8 +334,8 @@ def wait_for_command(client, monitor, command, key=None): if LooseVersion(redis_version) >= LooseVersion("5.0.0"): id_str = str(client.client_id()) else: - id_str = "%08x" % random.randrange(2 ** 32) - key = "__REDIS-PY-%s__" % id_str + id_str = f"{random.randrange(2 ** 32):08x}" + key = f"__REDIS-PY-{id_str}__" client.get(key) while True: monitor_response = monitor.next_command() From 199f25967f1c793d77cad37258858a764555de5b Mon Sep 17 00:00:00 2001 From: Bar Shaul Date: Wed, 1 Dec 2021 13:51:07 +0200 Subject: [PATCH 9/9] Merged master branch --- redis/cluster.py | 29 +++++-------------- redis/commands/cluster.py | 1 + tests/conftest.py | 20 ++++++------- tests/test_cluster.py | 54 +---------------------------------- tests/test_commands.py | 53 +++++++++++++++++----------------- tests/test_connection_pool.py | 12 ++++---- tests/test_monitor.py | 4 +-- tests/test_pubsub.py | 2 +- 8 files changed, 53 insertions(+), 122 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index e9db6894c2..eead2b4dfe 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -199,7 +199,7 @@ class ClusterParser(DefaultParser): ) -class RedisCluster(RedisClusterCommands, object): +class RedisCluster(RedisClusterCommands): RedisClusterRequestTTL = 16 PRIMARIES = "primaries" @@ -659,23 +659,6 @@ def set_default_node(self, node): log.info(f"Changed the default cluster node to {node}") return True - def monitor(self, target_node=None): - """ - Returns a Monitor object for the specified target node. - The default cluster node will be selected if no target node was - specified. - Monitor is useful for handling the MONITOR command to the redis server. - next_command() method returns one command from monitor - listen() method yields commands from monitor. - """ - if target_node is None: - target_node = self.get_default_node() - if target_node.redis_connection is None: - raise RedisClusterException( - f"Cluster Node {target_node.name} has no redis_connection" - ) - return target_node.redis_connection.monitor() - def pubsub(self, node=None, host=None, port=None, **kwargs): """ Allows passing a ClusterNode, or host&port, to get a pubsub instance @@ -1425,7 +1408,8 @@ def initialize(self): # isn't a full coverage raise RedisClusterException( f"All slots are not covered after query all startup_nodes. " - f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} covered..." + f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} " + f"covered..." ) elif not fully_covered and not self._require_full_coverage: # The user set require_full_coverage to False. @@ -1444,7 +1428,8 @@ def initialize(self): "cluster-require-full-coverage configuration to no on " "all of the cluster nodes if you wish the cluster to " "be able to serve without being fully covered." - f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} covered..." + f"{len(self.slots_cache)} of {REDIS_CLUSTER_HASH_SLOTS} " + f"covered..." ) # Set the tmp variables to the real variables @@ -1992,8 +1977,8 @@ def block_pipeline_command(func): def inner(*args, **kwargs): raise RedisClusterException( - f"ERROR: Calling pipelined function {func.__name__} is blocked when " - f"running redis in cluster mode..." + f"ERROR: Calling pipelined function {func.__name__} is blocked " + f"when running redis in cluster mode..." ) return inner diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 919f4dfc7e..5d0e804628 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -215,6 +215,7 @@ class RedisClusterCommands( target specific nodes. By default, if target_nodes is not specified, the command will be executed on the default cluster node. + :param :target_nodes: type can be one of the followings: - nodes flag: ALL_NODES, PRIMARIES, REPLICAS, RANDOM - 'ClusterNode' diff --git a/tests/conftest.py b/tests/conftest.py index ab29ee4fcd..24783c0466 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -151,12 +151,12 @@ def skip_ifmodversion_lt(min_version: str, module_name: str): raise AttributeError(f"No redis module named {module_name}") -def skip_if_redis_enterprise(): +def skip_if_redis_enterprise(func): check = REDIS_INFO["enterprise"] is True return pytest.mark.skipif(check, reason="Redis enterprise") -def skip_ifnot_redis_enterprise(): +def skip_ifnot_redis_enterprise(func): check = REDIS_INFO["enterprise"] is False return pytest.mark.skipif(check, reason="Not running in redis enterprise") @@ -324,18 +324,16 @@ def master_host(request): yield parts.hostname, parts.port -def wait_for_command(client, monitor, command, key=None): +def wait_for_command(client, monitor, command): # issue a command with a key name that's local to this process. # if we find a command with our key before the command we're waiting # for, something went wrong - if key is None: - # generate key - redis_version = REDIS_INFO["version"] - if LooseVersion(redis_version) >= LooseVersion("5.0.0"): - id_str = str(client.client_id()) - else: - id_str = f"{random.randrange(2 ** 32):08x}" - key = f"__REDIS-PY-{id_str}__" + redis_version = REDIS_INFO["version"] + if LooseVersion(redis_version) >= LooseVersion("5.0.0"): + id_str = str(client.client_id()) + else: + id_str = f"{random.randrange(2 ** 32):08x}" + key = f"__REDIS-PY-{id_str}__" client.get(key) while True: monitor_response = monitor.next_command() diff --git a/tests/test_cluster.py b/tests/test_cluster.py index b88673125f..b76ed80958 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -36,7 +36,6 @@ skip_if_redis_enterprise, skip_if_server_version_lt, skip_unless_arch_bits, - wait_for_command, ) default_host = "127.0.0.1" @@ -1772,7 +1771,7 @@ def test_cluster_randomkey(self, r): assert r.randomkey(target_nodes=node) in (b"{foo}a", b"{foo}b", b"{foo}c") @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_log(self, r, request): key = "{cache}:" node = r.get_node_from_key(key) @@ -2614,54 +2613,3 @@ def test_readonly_pipeline_from_readonly_client(self, request): if executed_on_replica: break assert executed_on_replica is True - - -@pytest.mark.onlycluster -class TestClusterMonitor: - def test_wait_command_not_found(self, r): - "Make sure the wait_for_command func works when command is not found" - key = "foo" - node = r.get_node_from_key(key) - with r.monitor(target_node=node) as m: - response = wait_for_command(r, m, "nothing", key=key) - assert response is None - - def test_response_values(self, r): - db = 0 - key = "foo" - node = r.get_node_from_key(key) - with r.monitor(target_node=node) as m: - r.ping(target_nodes=node) - response = wait_for_command(r, m, "PING", key=key) - assert isinstance(response["time"], float) - assert response["db"] == db - assert response["client_type"] in ("tcp", "unix") - assert isinstance(response["client_address"], str) - assert isinstance(response["client_port"], str) - assert response["command"] == "PING" - - def test_command_with_quoted_key(self, r): - key = "{foo}1" - node = r.get_node_from_key(key) - with r.monitor(node) as m: - r.get('{foo}"bar') - response = wait_for_command(r, m, 'GET {foo}"bar', key=key) - assert response["command"] == 'GET {foo}"bar' - - def test_command_with_binary_data(self, r): - key = "{foo}1" - node = r.get_node_from_key(key) - with r.monitor(target_node=node) as m: - byte_string = b"{foo}bar\x92" - r.get(byte_string) - response = wait_for_command(r, m, "GET {foo}bar\\x92", key=key) - assert response["command"] == "GET {foo}bar\\x92" - - def test_command_with_escaped_data(self, r): - key = "{foo}1" - node = r.get_node_from_key(key) - with r.monitor(target_node=node) as m: - byte_string = b"{foo}bar\\x92" - r.get(byte_string) - response = wait_for_command(r, m, "GET {foo}bar\\\\x92", key=key) - assert response["command"] == "GET {foo}bar\\\\x92" diff --git a/tests/test_commands.py b/tests/test_commands.py index d573b151e2..7c7d0f3d9e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -84,7 +84,7 @@ def test_acl_cat_with_category(self, r): assert "get" in commands @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_deluser(self, r, request): username = "redis-py-user" @@ -109,7 +109,7 @@ def teardown(): assert r.acl_getuser(users[4]) is None @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_genpass(self, r): password = r.acl_genpass() assert isinstance(password, str) @@ -123,7 +123,7 @@ def test_acl_genpass(self, r): assert isinstance(password, str) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_getuser_setuser(self, r, request): username = "redis-py-user" @@ -236,7 +236,7 @@ def test_acl_help(self, r): assert len(res) != 0 @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_list(self, r, request): username = "redis-py-user" @@ -250,8 +250,7 @@ def teardown(): assert len(users) == 2 @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() - @pytest.mark.onlynoncluster + @skip_if_redis_enterprise def test_acl_log(self, r, request): username = "redis-py-user" @@ -293,7 +292,7 @@ def teardown(): assert r.acl_log_reset() @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_setuser_categories_without_prefix_fails(self, r, request): username = "redis-py-user" @@ -306,7 +305,7 @@ def teardown(): r.acl_setuser(username, categories=["list"]) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_setuser_commands_without_prefix_fails(self, r, request): username = "redis-py-user" @@ -319,7 +318,7 @@ def teardown(): r.acl_setuser(username, commands=["get"]) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_acl_setuser_add_passwords_and_nopass_fails(self, r, request): username = "redis-py-user" @@ -364,7 +363,7 @@ def test_client_list_types_not_replica(self, r): clients = r.client_list(_type=client_type) assert isinstance(clients, list) - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_client_list_replica(self, r): clients = r.client_list(_type="replica") assert isinstance(clients, list) @@ -530,7 +529,7 @@ def test_client_kill_filter_by_laddr(self, r, r2): assert r.client_kill_filter(laddr=client_2_addr) @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_client_kill_filter_by_user(self, r, request): killuser = "user_to_kill" r.acl_setuser( @@ -550,7 +549,7 @@ def test_client_kill_filter_by_user(self, r, request): @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.9.50") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_client_pause(self, r): assert r.client_pause(1) assert r.client_pause(timeout=1) @@ -559,7 +558,7 @@ def test_client_pause(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.2.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_client_unpause(self, r): assert r.client_unpause() == b"OK" @@ -579,7 +578,7 @@ def test_client_reply(self, r, r_timeout): @pytest.mark.onlynoncluster @skip_if_server_version_lt("6.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_client_getredir(self, r): assert isinstance(r.client_getredir(), int) assert r.client_getredir() == -1 @@ -591,7 +590,7 @@ def test_config_get(self, r): # assert data['maxmemory'].isdigit() @pytest.mark.onlynoncluster - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_config_resetstat(self, r): r.ping() prior_commands_processed = int(r.info()["total_commands_processed"]) @@ -600,7 +599,7 @@ def test_config_resetstat(self, r): reset_commands_processed = int(r.info()["total_commands_processed"]) assert reset_commands_processed < prior_commands_processed - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_config_set(self, r): r.config_set("timeout", 70) assert r.config_get()["timeout"] == "70" @@ -627,7 +626,7 @@ def test_info(self, r): assert "redis_version" in info.keys() @pytest.mark.onlynoncluster - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_lastsave(self, r): assert isinstance(r.lastsave(), datetime.datetime) @@ -725,7 +724,7 @@ def test_time(self, r): assert isinstance(t[0], int) assert isinstance(t[1], int) - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_bgsave(self, r): assert r.bgsave() time.sleep(0.3) @@ -1306,7 +1305,7 @@ def test_stralgo_lcs(self, r): value2 = "mynewtext" res = "mytext" - if skip_if_redis_enterprise().args[0] is True: + if skip_if_redis_enterprise(None).args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.stralgo("LCS", value1, value2) == res return @@ -1348,7 +1347,7 @@ def test_strlen(self, r): def test_substr(self, r): r["a"] = "0123456789" - if skip_if_redis_enterprise().args[0] is True: + if skip_if_redis_enterprise(None).args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.substr("a", 0) == b"0123456789" return @@ -2659,7 +2658,7 @@ def test_cluster_slaves(self, mock_cluster_resp_slaves): @pytest.mark.onlynoncluster @skip_if_server_version_lt("3.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_readwrite(self, r): assert r.readwrite() @@ -4010,7 +4009,7 @@ def test_memory_doctor(self, r): @skip_if_server_version_lt("4.0.0") def test_memory_malloc_stats(self, r): - if skip_if_redis_enterprise().args[0] is True: + if skip_if_redis_enterprise(None).args[0] is True: with pytest.raises(redis.exceptions.ResponseError): assert r.memory_malloc_stats() return @@ -4023,7 +4022,7 @@ def test_memory_stats(self, r): # has data r.set("foo", "bar") - if skip_if_redis_enterprise().args[0] is True: + if skip_if_redis_enterprise(None).args[0] is True: with pytest.raises(redis.exceptions.ResponseError): stats = r.memory_stats() return @@ -4041,7 +4040,7 @@ def test_memory_usage(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("4.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_module_list(self, r): assert isinstance(r.module_list(), list) for x in r.module_list(): @@ -4082,7 +4081,7 @@ def test_command(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("4.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_module(self, r): with pytest.raises(redis.exceptions.ModuleError) as excinfo: r.module_load("/some/fake/path") @@ -4138,7 +4137,7 @@ def test_restore_frequency(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("5.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_replicaof(self, r): with pytest.raises(redis.ResponseError): assert r.replicaof("NO ONE") @@ -4220,7 +4219,7 @@ def test_22_info(self, r): assert "6" in parsed["allocation_stats"] assert ">=256" in parsed["allocation_stats"] - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_large_responses(self, r): "The PythonParser has some special cases for return values > 1MB" # load up 5MB of data into a key diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 4717a7f729..2602af82e1 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -496,7 +496,7 @@ def test_on_connect_error(self): @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.8.8") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_busy_loading_disconnects_socket(self, r): """ If Redis raises a LOADING error, the connection should be @@ -508,7 +508,7 @@ def test_busy_loading_disconnects_socket(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.8.8") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_busy_loading_from_pipeline_immediate_command(self, r): """ BusyLoadingErrors should raise from Pipelines that execute a @@ -524,7 +524,7 @@ def test_busy_loading_from_pipeline_immediate_command(self, r): @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.8.8") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_busy_loading_from_pipeline(self, r): """ BusyLoadingErrors should be raised from a pipeline execution @@ -540,7 +540,7 @@ def test_busy_loading_from_pipeline(self, r): assert not pool._available_connections[0]._sock @skip_if_server_version_lt("2.8.8") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_read_only_error(self, r): "READONLY errors get turned in ReadOnlyError exceptions" with pytest.raises(redis.ReadOnlyError): @@ -566,7 +566,7 @@ def test_connect_from_url_unix(self): "path=/path/to/socket,db=0", ) - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_connect_no_auth_supplied_when_required(self, r): """ AuthenticationError should be raised when the server requires a @@ -577,7 +577,7 @@ def test_connect_no_auth_supplied_when_required(self, r): "DEBUG", "ERROR", "ERR Client sent AUTH, but no password is set" ) - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_connect_invalid_password_supplied(self, r): "AuthenticationError should be raised when sending the wrong password" with pytest.raises(redis.AuthenticationError): diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 9b07c80201..40d9e43094 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -47,7 +47,7 @@ def test_command_with_escaped_data(self, r): response = wait_for_command(r, m, "GET foo\\\\x92") assert response["command"] == "GET foo\\\\x92" - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_lua_script(self, r): with r.monitor() as m: script = 'return redis.call("GET", "foo")' @@ -58,7 +58,7 @@ def test_lua_script(self, r): assert response["client_address"] == "lua" assert response["client_port"] == "" - @skip_ifnot_redis_enterprise() + @skip_ifnot_redis_enterprise def test_lua_script_in_enterprise(self, r): with r.monitor() as m: script = 'return redis.call("GET", "foo")' diff --git a/tests/test_pubsub.py b/tests/test_pubsub.py index 20ae0a05c1..6df0fafd4b 100644 --- a/tests/test_pubsub.py +++ b/tests/test_pubsub.py @@ -530,7 +530,7 @@ def test_send_pubsub_ping_message(self, r): @pytest.mark.onlynoncluster class TestPubSubConnectionKilled: @skip_if_server_version_lt("3.0.0") - @skip_if_redis_enterprise() + @skip_if_redis_enterprise def test_connection_error_raised_when_connection_dies(self, r): p = r.pubsub() p.subscribe("foo")