Skip to content

Commit 01db8ba

Browse files
authored
Merge pull request redis#29 from valkey-io/issue-25
Implement a single validation function around the service_uri and add support for redis and rediss protocols.
2 parents d661041 + 0ca204b commit 01db8ba

14 files changed

+141
-202
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
autosectionlabel_maxdepth = 2
4848

4949
# AutodocTypehints settings.
50-
autodoc_typehints = 'description'
50+
autodoc_typehints = "description"
5151
always_document_param_types = True
5252
typehints_defaults = "comma"
5353

docs/examples/opentelemetry/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import time
44

5-
import valkey
65
import uptrace
6+
import valkey
77
from opentelemetry import trace
88
from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
99

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import valkey
1010
from packaging.version import Version
1111
from valkey import Sentinel
12+
from valkey._parsers import parse_url
1213
from valkey.backoff import NoBackoff
13-
from valkey.connection import Connection, parse_url
14+
from valkey.connection import Connection
1415
from valkey.exceptions import ValkeyClusterException
1516
from valkey.retry import Retry
1617

@@ -275,7 +276,7 @@ def _get_client(
275276

276277
cluster_mode = VALKEY_INFO["cluster_enabled"]
277278
if not cluster_mode:
278-
url_options = parse_url(valkey_url)
279+
url_options = parse_url(valkey_url, False)
279280
url_options.update(kwargs)
280281
pool = valkey.ConnectionPool(**url_options)
281282
client = cls(connection_pool=pool)

tests/test_asyncio/conftest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import pytest_asyncio
66
import valkey.asyncio as valkey
77
from tests.conftest import VALKEY_INFO
8+
from valkey._parsers import parse_url
89
from valkey.asyncio import Sentinel
910
from valkey.asyncio.client import Monitor
10-
from valkey.asyncio.connection import Connection, parse_url
11+
from valkey.asyncio.connection import Connection
1112
from valkey.asyncio.retry import Retry
1213
from valkey.backoff import NoBackoff
1314

@@ -54,7 +55,7 @@ async def client_factory(
5455
cluster_mode = VALKEY_INFO["cluster_enabled"]
5556
if not cluster_mode:
5657
single = kwargs.pop("single_connection_client", False) or single_connection
57-
url_options = parse_url(url)
58+
url_options = parse_url(url, True)
5859
url_options.update(kwargs)
5960
pool = valkey.ConnectionPool(**url_options)
6061
client = cls(connection_pool=pool)
@@ -269,4 +270,4 @@ def valkey_url(request):
269270
@pytest.fixture()
270271
def connect_args(request):
271272
url = request.config.getoption("--valkey-url")
272-
return parse_url(url)
273+
return parse_url(url, True)

tests/test_asyncio/test_connection.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
_AsyncRESP2Parser,
1212
_AsyncRESP3Parser,
1313
_AsyncRESPBase,
14+
parse_url,
1415
)
1516
from valkey.asyncio import ConnectionPool, Valkey
16-
from valkey.asyncio.connection import Connection, UnixDomainSocketConnection, parse_url
17+
from valkey.asyncio.connection import Connection, UnixDomainSocketConnection
1718
from valkey.asyncio.retry import Retry
1819
from valkey.backoff import NoBackoff
1920
from valkey.exceptions import ConnectionError, InvalidResponse, TimeoutError
@@ -300,7 +301,7 @@ async def test_pool_auto_close(request, from_url):
300301
"""Verify that basic Valkey instances have auto_close_connection_pool set to True"""
301302

302303
url: str = request.config.getoption("--valkey-url")
303-
url_args = parse_url(url)
304+
url_args = parse_url(url, True)
304305

305306
async def get_valkey_connection():
306307
if from_url:
@@ -342,7 +343,7 @@ async def test_pool_auto_close_disable(request):
342343
"""Verify that auto_close_connection_pool can be disabled (deprecated)"""
343344

344345
url: str = request.config.getoption("--valkey-url")
345-
url_args = parse_url(url)
346+
url_args = parse_url(url, True)
346347

347348
async def get_valkey_connection():
348349
url_args["auto_close_connection_pool"] = False
@@ -361,7 +362,7 @@ async def test_valkey_connection_pool(request, from_url):
361362
have auto_close_connection_pool set to False"""
362363

363364
url: str = request.config.getoption("--valkey-url")
364-
url_args = parse_url(url)
365+
url_args = parse_url(url, True)
365366

366367
pool = None
367368

@@ -393,7 +394,7 @@ async def test_valkey_from_pool(request, from_url):
393394
have auto_close_connection_pool set to True"""
394395

395396
url: str = request.config.getoption("--valkey-url")
396-
url_args = parse_url(url)
397+
url_args = parse_url(url, True)
397398

398399
pool = None
399400

tests/test_asyncio/test_connection_pool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import pytest_asyncio
66
import valkey.asyncio as valkey
77
from tests.conftest import skip_if_server_version_lt
8-
from valkey.asyncio.connection import Connection, to_bool
8+
from valkey._parsers.url_parser import to_bool
9+
from valkey.asyncio.connection import Connection
910

1011
from .compat import aclosing, mock
1112
from .conftest import asynccontextmanager
@@ -441,8 +442,8 @@ def test_invalid_scheme_raises_error(self):
441442
with pytest.raises(ValueError) as cm:
442443
valkey.ConnectionPool.from_url("localhost")
443444
assert str(cm.value) == (
444-
"Valkey URL must specify one of the following schemes "
445-
"(valkey://, valkeys://, unix://)"
445+
"Valkey URL must specify one of the following schemes"
446+
" ['valkey', 'valkeys', 'redis', 'rediss', 'unix']"
446447
)
447448

448449

tests/test_connection.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@
66
import pytest
77
import valkey
88
from valkey import ConnectionPool, Valkey
9-
from valkey._parsers import _HiredisParser, _RESP2Parser, _RESP3Parser
9+
from valkey._parsers import _HiredisParser, _RESP2Parser, _RESP3Parser, parse_url
1010
from valkey.backoff import NoBackoff
11-
from valkey.connection import (
12-
Connection,
13-
SSLConnection,
14-
UnixDomainSocketConnection,
15-
parse_url,
16-
)
11+
from valkey.connection import Connection, SSLConnection, UnixDomainSocketConnection
1712
from valkey.exceptions import ConnectionError, InvalidResponse, TimeoutError
1813
from valkey.retry import Retry
1914
from valkey.utils import HIREDIS_AVAILABLE
@@ -222,7 +217,7 @@ def test_pool_auto_close(request, from_url):
222217
"""Verify that basic Valkey instances have auto_close_connection_pool set to True"""
223218

224219
url: str = request.config.getoption("--valkey-url")
225-
url_args = parse_url(url)
220+
url_args = parse_url(url, False)
226221

227222
def get_valkey_connection():
228223
if from_url:
@@ -240,7 +235,7 @@ def test_valkey_connection_pool(request, from_url):
240235
have auto_close_connection_pool set to False"""
241236

242237
url: str = request.config.getoption("--valkey-url")
243-
url_args = parse_url(url)
238+
url_args = parse_url(url, True)
244239

245240
pool = None
246241

@@ -272,7 +267,7 @@ def test_valkey_from_pool(request, from_url):
272267
have auto_close_connection_pool set to True"""
273268

274269
url: str = request.config.getoption("--valkey-url")
275-
url_args = parse_url(url)
270+
url_args = parse_url(url, True)
276271

277272
pool = None
278273

tests/test_connection_pool.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99
import valkey
10-
from valkey.connection import to_bool
10+
from valkey._parsers.url_parser import to_bool
1111
from valkey.utils import SSL_AVAILABLE
1212

1313
from .conftest import _get_client, skip_if_server_version_lt
@@ -337,15 +337,15 @@ def test_invalid_scheme_raises_error(self):
337337
valkey.ConnectionPool.from_url("localhost")
338338
assert str(cm.value) == (
339339
"Valkey URL must specify one of the following schemes "
340-
"(valkey://, valkeys://, unix://)"
340+
"['valkey', 'valkeys', 'redis', 'rediss', 'unix']"
341341
)
342342

343343
def test_invalid_scheme_raises_error_when_double_slash_missing(self):
344344
with pytest.raises(ValueError) as cm:
345345
valkey.ConnectionPool.from_url("valkey:foo.bar.com:12345")
346346
assert str(cm.value) == (
347347
"Valkey URL must specify one of the following schemes "
348-
"(valkey://, valkeys://, unix://)"
348+
"['valkey', 'valkeys', 'redis', 'rediss', 'unix']"
349349
)
350350

351351

valkey/_parsers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .hiredis import _AsyncHiredisParser, _HiredisParser
55
from .resp2 import _AsyncRESP2Parser, _RESP2Parser
66
from .resp3 import _AsyncRESP3Parser, _RESP3Parser
7+
from .url_parser import parse_url
78

89
__all__ = [
910
"AsyncCommandsParser",
@@ -17,4 +18,5 @@
1718
"_HiredisParser",
1819
"_RESP2Parser",
1920
"_RESP3Parser",
21+
"parse_url",
2022
]

valkey/_parsers/url_parser.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import re
2+
from types import MappingProxyType
3+
from typing import Callable, Mapping, Optional
4+
from urllib.parse import ParseResult, parse_qs, unquote, urlparse
5+
6+
from valkey.asyncio.connection import ConnectKwargs
7+
from valkey.asyncio.connection import SSLConnection as SSLConnectionAsync
8+
from valkey.asyncio.connection import (
9+
UnixDomainSocketConnection as UnixDomainSocketConnectionAsync,
10+
)
11+
from valkey.connection import SSLConnection, UnixDomainSocketConnection
12+
13+
14+
def to_bool(value) -> Optional[bool]:
15+
if value is None or value == "":
16+
return None
17+
if isinstance(value, str) and value.upper() in FALSE_STRINGS:
18+
return False
19+
return bool(value)
20+
21+
22+
FALSE_STRINGS = ("0", "F", "FALSE", "N", "NO")
23+
24+
URL_QUERY_ARGUMENT_PARSERS: Mapping[str, Callable[..., object]] = MappingProxyType(
25+
{
26+
"db": int,
27+
"socket_timeout": float,
28+
"socket_connect_timeout": float,
29+
"socket_keepalive": to_bool,
30+
"retry_on_timeout": to_bool,
31+
"max_connections": int,
32+
"health_check_interval": int,
33+
"ssl_check_hostname": to_bool,
34+
"timeout": float,
35+
}
36+
)
37+
38+
39+
def parse_url(url: str, async_connection: bool):
40+
supported_schemes = ["valkey", "valkeys", "redis", "rediss", "unix"]
41+
parsed: ParseResult = urlparse(url)
42+
kwargs: ConnectKwargs = {}
43+
pattern = re.compile(
44+
r"^(?:" + "|".join(map(re.escape, supported_schemes)) + r")://", re.IGNORECASE
45+
)
46+
if not pattern.match(url):
47+
raise ValueError(
48+
f"Valkey URL must specify one of the following schemes {supported_schemes}"
49+
)
50+
51+
for name, value_list in parse_qs(parsed.query).items():
52+
if value_list and len(value_list) > 0:
53+
value = unquote(value_list[0])
54+
parser = URL_QUERY_ARGUMENT_PARSERS.get(name)
55+
if parser:
56+
try:
57+
kwargs[name] = parser(value)
58+
except (TypeError, ValueError):
59+
raise ValueError(f"Invalid value for `{name}` in connection URL.")
60+
else:
61+
kwargs[name] = value
62+
63+
if parsed.username:
64+
kwargs["username"] = unquote(parsed.username)
65+
if parsed.password:
66+
kwargs["password"] = unquote(parsed.password)
67+
68+
# We only support valkey://, valkeys://, redis://, rediss://, and unix:// schemes.
69+
if parsed.scheme == "unix":
70+
if parsed.path:
71+
kwargs["path"] = unquote(parsed.path)
72+
kwargs["connection_class"] = (
73+
UnixDomainSocketConnectionAsync
74+
if async_connection
75+
else UnixDomainSocketConnection
76+
)
77+
78+
elif parsed.scheme in supported_schemes:
79+
if parsed.hostname:
80+
kwargs["host"] = unquote(parsed.hostname)
81+
if parsed.port:
82+
kwargs["port"] = int(parsed.port)
83+
84+
# If there's a path argument, use it as the db argument if a
85+
# querystring value wasn't specified
86+
if parsed.path and "db" not in kwargs:
87+
try:
88+
kwargs["db"] = int(unquote(parsed.path).replace("/", ""))
89+
except (AttributeError, ValueError):
90+
pass
91+
92+
if parsed.scheme in ("valkeys", "rediss"):
93+
kwargs["connection_class"] = (
94+
SSLConnectionAsync if async_connection else SSLConnection
95+
)
96+
else:
97+
raise ValueError(
98+
f"Valkey URL must specify one of the following schemes {supported_schemes}"
99+
)
100+
101+
return kwargs

valkey/asyncio/cluster.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,14 @@
2525
DEFAULT_EVICTION_POLICY,
2626
AbstractCache,
2727
)
28-
from valkey._parsers import AsyncCommandsParser, Encoder
28+
from valkey._parsers import AsyncCommandsParser, Encoder, parse_url
2929
from valkey._parsers.helpers import (
3030
_ValkeyCallbacks,
3131
_ValkeyCallbacksRESP2,
3232
_ValkeyCallbacksRESP3,
3333
)
3434
from valkey.asyncio.client import ResponseCallbackT
35-
from valkey.asyncio.connection import (
36-
Connection,
37-
DefaultParser,
38-
SSLConnection,
39-
parse_url,
40-
)
35+
from valkey.asyncio.connection import Connection, DefaultParser, SSLConnection
4136
from valkey.asyncio.lock import Lock
4237
from valkey.asyncio.retry import Retry
4338
from valkey.backoff import default_backoff
@@ -211,7 +206,7 @@ def from_url(cls, url: str, **kwargs: Any) -> "ValkeyCluster":
211206
:class:`~valkey.asyncio.connection.Connection` when created.
212207
In the case of conflicting arguments, querystring arguments are used.
213208
"""
214-
kwargs.update(parse_url(url))
209+
kwargs.update(parse_url(url, True))
215210
if kwargs.pop("connection_class", None) is SSLConnection:
216211
kwargs["ssl"] = True
217212
return cls(**kwargs)

0 commit comments

Comments
 (0)