From b45251cd21a2854ae7c47d765c46df29e0899d13 Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Thu, 27 Jun 2024 12:57:23 -0400 Subject: [PATCH 1/7] adds redis and rediss protocols Signed-off-by: Jay Miller --- tests/test_asyncio/test_connection_pool.py | 145 +++++++++++---------- tests/test_connection_pool.py | 139 ++++++++++---------- valkey/asyncio/connection.py | 26 ++-- valkey/connection.py | 30 +++-- 4 files changed, 180 insertions(+), 160 deletions(-) diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index ce8d792a..6226398b 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -6,6 +6,8 @@ import valkey.asyncio as valkey from tests.conftest import skip_if_server_version_lt from valkey.asyncio.connection import Connection, to_bool +from valkey.utils import SSL_AVAILABLE + from .compat import aclosing, mock from .conftest import asynccontextmanager @@ -301,33 +303,33 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) - +@pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: - def test_hostname(self): - pool = valkey.ConnectionPool.from_url("valkey://my.host") + def test_hostname(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my.host") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my.host"} - def test_quoted_hostname(self): - pool = valkey.ConnectionPool.from_url("valkey://my %2F host %2B%3D+") + def test_quoted_hostname(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my %2F host %2B%3D+") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my / host +=+"} - def test_port(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost:6380") + def test_port(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost:6380") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "port": 6380} @skip_if_server_version_lt("6.0.0") - def test_username(self): - pool = valkey.ConnectionPool.from_url("valkey://myuser:@localhost") + def test_username(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "username": "myuser"} @skip_if_server_version_lt("6.0.0") - def test_quoted_username(self): + def test_quoted_username(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://%2Fmyuser%2F%2B name%3D%24+:@localhost" + f"{connection_protocol}://%2Fmyuser%2F%2B name%3D%24+:@localhost" ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { @@ -335,14 +337,14 @@ def test_quoted_username(self): "username": "/myuser/+ name=$+", } - def test_password(self): - pool = valkey.ConnectionPool.from_url("valkey://:mypassword@localhost") + def test_password(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://:mypassword@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "password": "mypassword"} - def test_quoted_password(self): + def test_quoted_password(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://:%2Fmypass%2F%2B word%3D%24+@localhost" + f"{connection_protocol}://:%2Fmypass%2F%2B word%3D%24+@localhost" ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { @@ -351,8 +353,8 @@ def test_quoted_password(self): } @skip_if_server_version_lt("6.0.0") - def test_username_and_password(self): - pool = valkey.ConnectionPool.from_url("valkey://myuser:mypass@localhost") + def test_username_and_password(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:mypass@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { "host": "localhost", @@ -360,24 +362,24 @@ def test_username_and_password(self): "password": "mypass", } - def test_db_as_argument(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost", db=1) + def test_db_as_argument(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 1} - def test_db_in_path(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost/2", db=1) + def test_db_in_path(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 2} - def test_db_in_querystring(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost/2?db=3", db=1) + def test_db_in_querystring(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2?db=3", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 3} - def test_extra_typed_querystring_options(self): + def test_extra_typed_querystring_options(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://localhost/2?socket_timeout=20&socket_connect_timeout=10" + f"{connection_protocol}://localhost/2?socket_timeout=20&socket_connect_timeout=10" "&socket_keepalive=&retry_on_timeout=Yes&max_connections=10" ) @@ -391,60 +393,60 @@ def test_extra_typed_querystring_options(self): } assert pool.max_connections == 10 - def test_boolean_parsing(self): - for expected, value in ( - (None, None), - (None, ""), - (False, 0), - (False, "0"), - (False, "f"), - (False, "F"), - (False, "False"), - (False, "n"), - (False, "N"), - (False, "No"), - (True, 1), - (True, "1"), - (True, "y"), - (True, "Y"), - (True, "Yes"), - ): - assert expected is to_bool(value) - def test_client_name_in_querystring(self): + def test_client_name_in_querystring(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://location?client_name=test-client" + f"{connection_protocol}://location?client_name=test-client" ) assert pool.connection_kwargs["client_name"] == "test-client" - def test_invalid_extra_typed_querystring_options(self): + def test_invalid_extra_typed_querystring_options(self, connection_protocol): with pytest.raises(ValueError): valkey.ConnectionPool.from_url( - "valkey://localhost/2?socket_timeout=_&socket_connect_timeout=abc" + f"{connection_protocol}://localhost/2?socket_timeout=_&socket_connect_timeout=abc" ) - def test_extra_querystring_options(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost?a=1&b=2") + def test_extra_querystring_options(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost?a=1&b=2") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "a": "1", "b": "2"} - def test_calling_from_subclass_returns_correct_instance(self): - pool = valkey.BlockingConnectionPool.from_url("valkey://localhost") + def test_calling_from_subclass_returns_correct_instance(self, connection_protocol): + pool = valkey.BlockingConnectionPool.from_url(f"{connection_protocol}://localhost") assert isinstance(pool, valkey.BlockingConnectionPool) - def test_client_creates_connection_pool(self): - r = valkey.Valkey.from_url("valkey://myhost") + def test_client_creates_connection_pool(self, connection_protocol): + r = valkey.Valkey.from_url(f"{connection_protocol}://myhost") assert r.connection_pool.connection_class == valkey.Connection assert r.connection_pool.connection_kwargs == {"host": "myhost"} - def test_invalid_scheme_raises_error(self): - with pytest.raises(ValueError) as cm: - valkey.ConnectionPool.from_url("localhost") - assert str(cm.value) == ( - "Valkey URL must specify one of the following schemes " - "(valkey://, valkeys://, unix://)" - ) - +def test_invalid_scheme_raises_error(): + with pytest.raises(ValueError) as cm: + valkey.ConnectionPool.from_url("localhost") + assert str(cm.value) == ( + "Valkey URL must specify one of the following schemes " + "(valkey://, valkeys://, redis://, rediss://, unix://)" + ) + +def test_boolean_parsing(): + for expected, value in ( + (None, None), + (None, ""), + (False, 0), + (False, "0"), + (False, "f"), + (False, "F"), + (False, "False"), + (False, "n"), + (False, "N"), + (False, "No"), + (True, 1), + (True, "1"), + (True, "y"), + (True, "Y"), + (True, "Yes"), + ): + assert expected is to_bool(value) class TestBlockingConnectionPoolURLParsing: def test_extra_typed_querystring_options(self): @@ -540,33 +542,34 @@ def test_extra_querystring_options(self): assert pool.connection_class == valkey.UnixDomainSocketConnection assert pool.connection_kwargs == {"path": "/socket", "a": "1", "b": "2"} - +@pytest.mark.skipif(not SSL_AVAILABLE, reason="SSL not installed") +@pytest.mark.parametrize("connection_protocol", ["valkeys", "rediss"]) class TestSSLConnectionURLParsing: - def test_host(self): - pool = valkey.ConnectionPool.from_url("valkeys://my.host") + def test_host(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my.host") assert pool.connection_class == valkey.SSLConnection assert pool.connection_kwargs == {"host": "my.host"} - def test_cert_reqs_options(self): + def test_cert_reqs_options(self, connection_protocol): import ssl class DummyConnectionPool(valkey.ConnectionPool): def get_connection(self, *args, **kwargs): return self.make_connection() - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=none") + pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=none") assert pool.get_connection("_").cert_reqs == ssl.CERT_NONE - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=optional") + pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=optional") assert pool.get_connection("_").cert_reqs == ssl.CERT_OPTIONAL - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=required") + pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=required") assert pool.get_connection("_").cert_reqs == ssl.CERT_REQUIRED - pool = DummyConnectionPool.from_url("valkeys://?ssl_check_hostname=False") + pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_check_hostname=False") assert pool.get_connection("_").check_hostname is False - pool = DummyConnectionPool.from_url("valkeys://?ssl_check_hostname=True") + pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_check_hostname=True") assert pool.get_connection("_").check_hostname is True diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 07806fa7..d76ed214 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -196,33 +196,33 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) - +@pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: - def test_hostname(self): - pool = valkey.ConnectionPool.from_url("valkey://my.host") + def test_hostname(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my.host") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my.host"} - def test_quoted_hostname(self): - pool = valkey.ConnectionPool.from_url("valkey://my %2F host %2B%3D+") + def test_quoted_hostname(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my %2F host %2B%3D+") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my / host +=+"} - def test_port(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost:6380") + def test_port(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost:6380") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "port": 6380} @skip_if_server_version_lt("6.0.0") - def test_username(self): - pool = valkey.ConnectionPool.from_url("valkey://myuser:@localhost") + def test_username(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "username": "myuser"} @skip_if_server_version_lt("6.0.0") - def test_quoted_username(self): + def test_quoted_username(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://%2Fmyuser%2F%2B name%3D%24+:@localhost" + f"{connection_protocol}://%2Fmyuser%2F%2B name%3D%24+:@localhost" ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { @@ -230,14 +230,14 @@ def test_quoted_username(self): "username": "/myuser/+ name=$+", } - def test_password(self): - pool = valkey.ConnectionPool.from_url("valkey://:mypassword@localhost") + def test_password(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://:mypassword@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "password": "mypassword"} - def test_quoted_password(self): + def test_quoted_password(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://:%2Fmypass%2F%2B word%3D%24+@localhost" + f"{connection_protocol}://:%2Fmypass%2F%2B word%3D%24+@localhost" ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { @@ -246,8 +246,8 @@ def test_quoted_password(self): } @skip_if_server_version_lt("6.0.0") - def test_username_and_password(self): - pool = valkey.ConnectionPool.from_url("valkey://myuser:mypass@localhost") + def test_username_and_password(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:mypass@localhost") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { "host": "localhost", @@ -255,24 +255,24 @@ def test_username_and_password(self): "password": "mypass", } - def test_db_as_argument(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost", db=1) + def test_db_as_argument(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 1} - def test_db_in_path(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost/2", db=1) + def test_db_in_path(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 2} - def test_db_in_querystring(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost/2?db=3", db=1) + def test_db_in_querystring(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2?db=3", db=1) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 3} - def test_extra_typed_querystring_options(self): + def test_extra_typed_querystring_options(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://localhost/2?socket_timeout=20&socket_connect_timeout=10" + f"{connection_protocol}://localhost/2?socket_timeout=20&socket_connect_timeout=10" "&socket_keepalive=&retry_on_timeout=Yes&max_connections=10" ) @@ -286,67 +286,67 @@ def test_extra_typed_querystring_options(self): } assert pool.max_connections == 10 - def test_boolean_parsing(self): - for expected, value in ( - (None, None), - (None, ""), - (False, 0), - (False, "0"), - (False, "f"), - (False, "F"), - (False, "False"), - (False, "n"), - (False, "N"), - (False, "No"), - (True, 1), - (True, "1"), - (True, "y"), - (True, "Y"), - (True, "Yes"), - ): - assert expected is to_bool(value) - - def test_client_name_in_querystring(self): + def test_client_name_in_querystring(self, connection_protocol): pool = valkey.ConnectionPool.from_url( - "valkey://location?client_name=test-client" + f"{connection_protocol}://location?client_name=test-client" ) assert pool.connection_kwargs["client_name"] == "test-client" - def test_invalid_extra_typed_querystring_options(self): + def test_invalid_extra_typed_querystring_options(self, connection_protocol): with pytest.raises(ValueError): valkey.ConnectionPool.from_url( - "valkey://localhost/2?socket_timeout=_&socket_connect_timeout=abc" + f"{connection_protocol}://localhost/2?socket_timeout=_&socket_connect_timeout=abc" ) - def test_extra_querystring_options(self): - pool = valkey.ConnectionPool.from_url("valkey://localhost?a=1&b=2") + def test_extra_querystring_options(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost?a=1&b=2") assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "a": "1", "b": "2"} - def test_calling_from_subclass_returns_correct_instance(self): + def test_calling_from_subclass_returns_correct_instance(self, connection_protocol): pool = valkey.BlockingConnectionPool.from_url("valkey://localhost") assert isinstance(pool, valkey.BlockingConnectionPool) - def test_client_creates_connection_pool(self): - r = valkey.Valkey.from_url("valkey://myhost") + def test_client_creates_connection_pool(self, connection_protocol): + r = valkey.Valkey.from_url(f"{connection_protocol}://myhost") assert r.connection_pool.connection_class == valkey.Connection assert r.connection_pool.connection_kwargs == {"host": "myhost"} - def test_invalid_scheme_raises_error(self): - with pytest.raises(ValueError) as cm: - valkey.ConnectionPool.from_url("localhost") - assert str(cm.value) == ( - "Valkey URL must specify one of the following schemes " - "(valkey://, valkeys://, unix://)" - ) - - def test_invalid_scheme_raises_error_when_double_slash_missing(self): - with pytest.raises(ValueError) as cm: - valkey.ConnectionPool.from_url("valkey:foo.bar.com:12345") - assert str(cm.value) == ( - "Valkey URL must specify one of the following schemes " - "(valkey://, valkeys://, unix://)" - ) +def test_invalid_scheme_raises_error(): + with pytest.raises(ValueError) as cm: + valkey.ConnectionPool.from_url("localhost") + assert str(cm.value) == ( + "Valkey URL must specify one of the following schemes " + "(valkey://, valkeys://, redis://, rediss://, unix://)" + ) + +def test_invalid_scheme_raises_error_when_double_slash_missing(): + with pytest.raises(ValueError) as cm: + valkey.ConnectionPool.from_url("valkey:foo.bar.com:12345") + assert str(cm.value) == ( + "Valkey URL must specify one of the following schemes " + "(valkey://, valkeys://, redis://, rediss://, unix://)" + ) + +def test_boolean_parsing(): + for expected, value in ( + (None, None), + (None, ""), + (False, 0), + (False, "0"), + (False, "f"), + (False, "F"), + (False, "False"), + (False, "n"), + (False, "N"), + (False, "No"), + (True, 1), + (True, "1"), + (True, "y"), + (True, "Y"), + (True, "Yes"), + ): + assert expected is to_bool(value) class TestBlockingConnectionPoolURLParsing: @@ -454,6 +454,7 @@ class MyConnection(valkey.UnixDomainSocketConnection): @pytest.mark.skipif(not SSL_AVAILABLE, reason="SSL not installed") +@pytest.mark class TestSSLConnectionURLParsing: def test_host(self): pool = valkey.ConnectionPool.from_url("valkeys://my.host") diff --git a/valkey/asyncio/connection.py b/valkey/asyncio/connection.py index 77b329a8..0b5afe86 100644 --- a/valkey/asyncio/connection.py +++ b/valkey/asyncio/connection.py @@ -1023,6 +1023,20 @@ class ConnectKwargs(TypedDict, total=False): def parse_url(url: str) -> ConnectKwargs: + valid_schemes = ( + "valkey://", + "valkeys://", + "redis://", + "rediss://", + "unix://", + ) + + if not any([url.startswith(scheme) for scheme in valid_schemes]): + raise ValueError( + "Valkey URL must specify one of the following " + f"schemes ({', '.join(valid_schemes)})" + ) + parsed: ParseResult = urlparse(url) kwargs: ConnectKwargs = {} @@ -1043,13 +1057,13 @@ def parse_url(url: str) -> ConnectKwargs: if parsed.password: kwargs["password"] = unquote(parsed.password) - # We only support valkey://, valkeys:// and unix:// schemes. + # We only support valkey://, valkeys://, redis://, rediss://, and unix:// schemes. if parsed.scheme == "unix": if parsed.path: kwargs["path"] = unquote(parsed.path) kwargs["connection_class"] = UnixDomainSocketConnection - elif parsed.scheme in ("valkey", "valkeys"): + elif parsed.scheme in ("valkey", "valkeys", "redis", "rediss"): if parsed.hostname: kwargs["host"] = unquote(parsed.hostname) if parsed.port: @@ -1063,13 +1077,9 @@ def parse_url(url: str) -> ConnectKwargs: except (AttributeError, ValueError): pass - if parsed.scheme == "valkeys": + if parsed.scheme in ("valkeys", "rediss"): kwargs["connection_class"] = SSLConnection - else: - valid_schemes = "valkey://, valkeys://, unix://" - raise ValueError( - f"Valkey URL must specify one of the following schemes ({valid_schemes})" - ) + return kwargs diff --git a/valkey/connection.py b/valkey/connection.py index 29d3fbb0..b15ab195 100644 --- a/valkey/connection.py +++ b/valkey/connection.py @@ -971,14 +971,18 @@ def to_bool(value): def parse_url(url): - if not ( - url.startswith("valkey://") - or url.startswith("valkeys://") - or url.startswith("unix://") - ): + valid_schemes = ( + "valkey://", + "valkeys://", + "redis://", + "rediss://", + "unix://", + ) + + if not any([url.startswith(scheme) for scheme in valid_schemes]): raise ValueError( "Valkey URL must specify one of the following " - "schemes (valkey://, valkeys://, unix://)" + f"schemes ({', '.join(valid_schemes)})" ) url = urlparse(url) @@ -1001,13 +1005,13 @@ def parse_url(url): if url.password: kwargs["password"] = unquote(url.password) - # We only support valkey://, valkeys:// and unix:// schemes. + # We only support valkey://, valkeys://, redis://, rediss://, and unix:// schemes. if url.scheme == "unix": if url.path: kwargs["path"] = unquote(url.path) kwargs["connection_class"] = UnixDomainSocketConnection - else: # implied: url.scheme in ("valkey", "valkeys"): + else: # implied: url.scheme in ("valkey", "valkeys", "redis", "rediss"): if url.hostname: kwargs["host"] = unquote(url.hostname) if url.port: @@ -1021,7 +1025,7 @@ def parse_url(url): except (AttributeError, ValueError): pass - if url.scheme == "valkeys": + if url.scheme in ("valkeys", "rediss"): kwargs["connection_class"] = SSLConnection return kwargs @@ -1050,12 +1054,14 @@ def from_url(cls, url, **kwargs): valkey://[[username]:[password]]@localhost:6379/0 valkeys://[[username]:[password]]@localhost:6379/0 + redis://[[username]:[password]]@localhost:6379/0 + rediss://[[username]:[password]]@localhost:6379/0 unix://[username@]/path/to/socket.sock?db=0[&password=password] Three URL schemes are supported: - - `valkey://` creates a TCP socket connection. - - `valkeys://` creates a SSL wrapped TCP socket connection. + - `valkey://` and `redis://` creates a TCP socket connection. + - `valkeys://` and `rediss://` creates a SSL wrapped TCP socket connection. - ``unix://``: creates a Unix Domain Socket connection. The username, password, hostname, path and all querystring values @@ -1066,7 +1072,7 @@ def from_url(cls, url, **kwargs): found will be used: 1. A ``db`` querystring option, e.g. valkey://localhost?db=0 - 2. If using the valkey:// or valkeys:// schemes, the path argument + 2. If using the valkey://, valkeys://, redis://, or rediss:// schemes, the path argument of the url, e.g. valkey://localhost/0 3. A ``db`` keyword argument to this function. From 1933b22426c090e77c8de417410e32afcda44de5 Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Thu, 27 Jun 2024 12:58:49 -0400 Subject: [PATCH 2/7] lint with black Signed-off-by: Jay Miller --- tests/test_asyncio/test_connection_pool.py | 62 +++++++++++++++----- tests/test_connection_pool.py | 68 ++++++++++++++++------ valkey/asyncio/connection.py | 3 +- valkey/connection.py | 2 +- 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index 6226398b..b337f0e1 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -303,6 +303,7 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) + @pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: def test_hostname(self, connection_protocol): @@ -311,7 +312,9 @@ def test_hostname(self, connection_protocol): assert pool.connection_kwargs == {"host": "my.host"} def test_quoted_hostname(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my %2F host %2B%3D+") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://my %2F host %2B%3D+" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my / host +=+"} @@ -322,7 +325,9 @@ def test_port(self, connection_protocol): @skip_if_server_version_lt("6.0.0") def test_username(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://myuser:@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "username": "myuser"} @@ -338,7 +343,9 @@ def test_quoted_username(self, connection_protocol): } def test_password(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://:mypassword@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://:mypassword@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "password": "mypassword"} @@ -354,7 +361,9 @@ def test_quoted_password(self, connection_protocol): @skip_if_server_version_lt("6.0.0") def test_username_and_password(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:mypass@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://myuser:mypass@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { "host": "localhost", @@ -363,17 +372,23 @@ def test_username_and_password(self, connection_protocol): } def test_db_as_argument(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 1} def test_db_in_path(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost/2", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 2} def test_db_in_querystring(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2?db=3", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost/2?db=3", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 3} @@ -393,7 +408,6 @@ def test_extra_typed_querystring_options(self, connection_protocol): } assert pool.max_connections == 10 - def test_client_name_in_querystring(self, connection_protocol): pool = valkey.ConnectionPool.from_url( f"{connection_protocol}://location?client_name=test-client" @@ -407,12 +421,16 @@ def test_invalid_extra_typed_querystring_options(self, connection_protocol): ) def test_extra_querystring_options(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost?a=1&b=2") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost?a=1&b=2" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "a": "1", "b": "2"} def test_calling_from_subclass_returns_correct_instance(self, connection_protocol): - pool = valkey.BlockingConnectionPool.from_url(f"{connection_protocol}://localhost") + pool = valkey.BlockingConnectionPool.from_url( + f"{connection_protocol}://localhost" + ) assert isinstance(pool, valkey.BlockingConnectionPool) def test_client_creates_connection_pool(self, connection_protocol): @@ -420,6 +438,7 @@ def test_client_creates_connection_pool(self, connection_protocol): assert r.connection_pool.connection_class == valkey.Connection assert r.connection_pool.connection_kwargs == {"host": "myhost"} + def test_invalid_scheme_raises_error(): with pytest.raises(ValueError) as cm: valkey.ConnectionPool.from_url("localhost") @@ -428,6 +447,7 @@ def test_invalid_scheme_raises_error(): "(valkey://, valkeys://, redis://, rediss://, unix://)" ) + def test_boolean_parsing(): for expected, value in ( (None, None), @@ -448,6 +468,7 @@ def test_boolean_parsing(): ): assert expected is to_bool(value) + class TestBlockingConnectionPoolURLParsing: def test_extra_typed_querystring_options(self): pool = valkey.BlockingConnectionPool.from_url( @@ -542,6 +563,7 @@ def test_extra_querystring_options(self): assert pool.connection_class == valkey.UnixDomainSocketConnection assert pool.connection_kwargs == {"path": "/socket", "a": "1", "b": "2"} + @pytest.mark.skipif(not SSL_AVAILABLE, reason="SSL not installed") @pytest.mark.parametrize("connection_protocol", ["valkeys", "rediss"]) class TestSSLConnectionURLParsing: @@ -557,19 +579,29 @@ class DummyConnectionPool(valkey.ConnectionPool): def get_connection(self, *args, **kwargs): return self.make_connection() - pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=none") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=none" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_NONE - pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=optional") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=optional" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_OPTIONAL - pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_cert_reqs=required") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=required" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_REQUIRED - pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_check_hostname=False") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_check_hostname=False" + ) assert pool.get_connection("_").check_hostname is False - pool = DummyConnectionPool.from_url(f"{connection_protocol}://?ssl_check_hostname=True") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_check_hostname=True" + ) assert pool.get_connection("_").check_hostname is True diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index d76ed214..646ad114 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -196,6 +196,7 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) + @pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: def test_hostname(self, connection_protocol): @@ -204,7 +205,9 @@ def test_hostname(self, connection_protocol): assert pool.connection_kwargs == {"host": "my.host"} def test_quoted_hostname(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my %2F host %2B%3D+") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://my %2F host %2B%3D+" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "my / host +=+"} @@ -215,7 +218,9 @@ def test_port(self, connection_protocol): @skip_if_server_version_lt("6.0.0") def test_username(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://myuser:@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "username": "myuser"} @@ -231,7 +236,9 @@ def test_quoted_username(self, connection_protocol): } def test_password(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://:mypassword@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://:mypassword@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "password": "mypassword"} @@ -247,7 +254,9 @@ def test_quoted_password(self, connection_protocol): @skip_if_server_version_lt("6.0.0") def test_username_and_password(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://myuser:mypass@localhost") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://myuser:mypass@localhost" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == { "host": "localhost", @@ -256,17 +265,23 @@ def test_username_and_password(self, connection_protocol): } def test_db_as_argument(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 1} def test_db_in_path(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost/2", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 2} def test_db_in_querystring(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost/2?db=3", db=1) + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost/2?db=3", db=1 + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "db": 3} @@ -299,7 +314,9 @@ def test_invalid_extra_typed_querystring_options(self, connection_protocol): ) def test_extra_querystring_options(self, connection_protocol): - pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://localhost?a=1&b=2") + pool = valkey.ConnectionPool.from_url( + f"{connection_protocol}://localhost?a=1&b=2" + ) assert pool.connection_class == valkey.Connection assert pool.connection_kwargs == {"host": "localhost", "a": "1", "b": "2"} @@ -312,6 +329,7 @@ def test_client_creates_connection_pool(self, connection_protocol): assert r.connection_pool.connection_class == valkey.Connection assert r.connection_pool.connection_kwargs == {"host": "myhost"} + def test_invalid_scheme_raises_error(): with pytest.raises(ValueError) as cm: valkey.ConnectionPool.from_url("localhost") @@ -320,6 +338,7 @@ def test_invalid_scheme_raises_error(): "(valkey://, valkeys://, redis://, rediss://, unix://)" ) + def test_invalid_scheme_raises_error_when_double_slash_missing(): with pytest.raises(ValueError) as cm: valkey.ConnectionPool.from_url("valkey:foo.bar.com:12345") @@ -328,6 +347,7 @@ def test_invalid_scheme_raises_error_when_double_slash_missing(): "(valkey://, valkeys://, redis://, rediss://, unix://)" ) + def test_boolean_parsing(): for expected, value in ( (None, None), @@ -454,42 +474,52 @@ class MyConnection(valkey.UnixDomainSocketConnection): @pytest.mark.skipif(not SSL_AVAILABLE, reason="SSL not installed") -@pytest.mark +@pytest.mark.parametrize("connection_protocol", ["valkeys", "rediss"]) class TestSSLConnectionURLParsing: - def test_host(self): - pool = valkey.ConnectionPool.from_url("valkeys://my.host") + def test_host(self, connection_protocol): + pool = valkey.ConnectionPool.from_url(f"{connection_protocol}://my.host") assert pool.connection_class == valkey.SSLConnection assert pool.connection_kwargs == {"host": "my.host"} - def test_connection_class_override(self): + def test_connection_class_override(self, connection_protocol): class MyConnection(valkey.SSLConnection): pass pool = valkey.ConnectionPool.from_url( - "valkeys://my.host", connection_class=MyConnection + f"{connection_protocol}://my.host", connection_class=MyConnection ) assert pool.connection_class == MyConnection - def test_cert_reqs_options(self): + def test_cert_reqs_options(self, connection_protocol): import ssl class DummyConnectionPool(valkey.ConnectionPool): def get_connection(self, *args, **kwargs): return self.make_connection() - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=none") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=none" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_NONE - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=optional") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=optional" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_OPTIONAL - pool = DummyConnectionPool.from_url("valkeys://?ssl_cert_reqs=required") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_cert_reqs=required" + ) assert pool.get_connection("_").cert_reqs == ssl.CERT_REQUIRED - pool = DummyConnectionPool.from_url("valkeys://?ssl_check_hostname=False") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_check_hostname=False" + ) assert pool.get_connection("_").check_hostname is False - pool = DummyConnectionPool.from_url("valkeys://?ssl_check_hostname=True") + pool = DummyConnectionPool.from_url( + f"{connection_protocol}://?ssl_check_hostname=True" + ) assert pool.get_connection("_").check_hostname is True diff --git a/valkey/asyncio/connection.py b/valkey/asyncio/connection.py index 0b5afe86..c8be7cc0 100644 --- a/valkey/asyncio/connection.py +++ b/valkey/asyncio/connection.py @@ -1030,7 +1030,7 @@ def parse_url(url: str) -> ConnectKwargs: "rediss://", "unix://", ) - + if not any([url.startswith(scheme) for scheme in valid_schemes]): raise ValueError( "Valkey URL must specify one of the following " @@ -1080,7 +1080,6 @@ def parse_url(url: str) -> ConnectKwargs: if parsed.scheme in ("valkeys", "rediss"): kwargs["connection_class"] = SSLConnection - return kwargs diff --git a/valkey/connection.py b/valkey/connection.py index b15ab195..d4815a42 100644 --- a/valkey/connection.py +++ b/valkey/connection.py @@ -978,7 +978,7 @@ def parse_url(url): "rediss://", "unix://", ) - + if not any([url.startswith(scheme) for scheme in valid_schemes]): raise ValueError( "Valkey URL must specify one of the following " From cf816ae8623f687ad6094c025f946e43259f939b Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Thu, 27 Jun 2024 13:02:42 -0400 Subject: [PATCH 3/7] adds change to changes Signed-off-by: Jay Miller --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 82c5b6db..8c428be5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,4 @@ + * Add 'redis://' and 'rediss://' protocol support * Update `ResponseT` type hint * Allow to control the minimum SSL version * Add an optional lock_name attribute to LockError. From 12803f1c83bfb7db225c2ec11ffbb1366fca5ff1 Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Thu, 27 Jun 2024 12:57:23 -0400 Subject: [PATCH 4/7] adds redis and rediss protocols Signed-off-by: Jay Miller --- tests/test_connection_pool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 646ad114..bfb5c839 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -196,7 +196,6 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) - @pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: def test_hostname(self, connection_protocol): From 1c2e367d9a58536d2713d4eb2283fcb6da47673a Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Thu, 27 Jun 2024 12:58:49 -0400 Subject: [PATCH 5/7] lint with black Signed-off-by: Jay Miller --- tests/test_connection_pool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index bfb5c839..646ad114 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -196,6 +196,7 @@ def test_repr_contains_db_info_unix(self): expected = "path=abc,db=0,client_name=test-client" assert expected in repr(pool) + @pytest.mark.parametrize("connection_protocol", ["valkey", "redis"]) class TestConnectionPoolURLParsing: def test_hostname(self, connection_protocol): From 2db11c2a1cc17fe6c0a98724be51ad4a02b219ca Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Fri, 28 Jun 2024 07:58:07 -0400 Subject: [PATCH 6/7] removes non-case-sensitive check Signed-off-by: Jay Miller --- valkey/asyncio/connection.py | 14 -------------- valkey/connection.py | 13 ------------- 2 files changed, 27 deletions(-) diff --git a/valkey/asyncio/connection.py b/valkey/asyncio/connection.py index c8be7cc0..174e1d9e 100644 --- a/valkey/asyncio/connection.py +++ b/valkey/asyncio/connection.py @@ -1023,20 +1023,6 @@ class ConnectKwargs(TypedDict, total=False): def parse_url(url: str) -> ConnectKwargs: - valid_schemes = ( - "valkey://", - "valkeys://", - "redis://", - "rediss://", - "unix://", - ) - - if not any([url.startswith(scheme) for scheme in valid_schemes]): - raise ValueError( - "Valkey URL must specify one of the following " - f"schemes ({', '.join(valid_schemes)})" - ) - parsed: ParseResult = urlparse(url) kwargs: ConnectKwargs = {} diff --git a/valkey/connection.py b/valkey/connection.py index d4815a42..c51e9327 100644 --- a/valkey/connection.py +++ b/valkey/connection.py @@ -971,19 +971,6 @@ def to_bool(value): def parse_url(url): - valid_schemes = ( - "valkey://", - "valkeys://", - "redis://", - "rediss://", - "unix://", - ) - - if not any([url.startswith(scheme) for scheme in valid_schemes]): - raise ValueError( - "Valkey URL must specify one of the following " - f"schemes ({', '.join(valid_schemes)})" - ) url = urlparse(url) kwargs = {} From 7b487a4da3b3763ea20d09b815f9905407c748ec Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Fri, 28 Jun 2024 08:50:42 -0400 Subject: [PATCH 7/7] reintroduce test in async Signed-off-by: Jay Miller --- tests/test_connection_pool.py | 18 ------------------ valkey/asyncio/connection.py | 6 ++++++ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 646ad114..bc4ce5cd 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -330,24 +330,6 @@ def test_client_creates_connection_pool(self, connection_protocol): assert r.connection_pool.connection_kwargs == {"host": "myhost"} -def test_invalid_scheme_raises_error(): - with pytest.raises(ValueError) as cm: - valkey.ConnectionPool.from_url("localhost") - assert str(cm.value) == ( - "Valkey URL must specify one of the following schemes " - "(valkey://, valkeys://, redis://, rediss://, unix://)" - ) - - -def test_invalid_scheme_raises_error_when_double_slash_missing(): - with pytest.raises(ValueError) as cm: - valkey.ConnectionPool.from_url("valkey:foo.bar.com:12345") - assert str(cm.value) == ( - "Valkey URL must specify one of the following schemes " - "(valkey://, valkeys://, redis://, rediss://, unix://)" - ) - - def test_boolean_parsing(): for expected, value in ( (None, None), diff --git a/valkey/asyncio/connection.py b/valkey/asyncio/connection.py index 174e1d9e..17b76eea 100644 --- a/valkey/asyncio/connection.py +++ b/valkey/asyncio/connection.py @@ -1066,6 +1066,12 @@ def parse_url(url: str) -> ConnectKwargs: if parsed.scheme in ("valkeys", "rediss"): kwargs["connection_class"] = SSLConnection + else: + valid_schemes = "valkey://, valkeys://, redis://, rediss://, unix://" + raise ValueError( + f"Valkey URL must specify one of the following schemes ({valid_schemes})" + ) + return kwargs