From 7dabe3286a5c7e7a19f55d7ad5b599f47f91b67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Sun, 16 Feb 2020 11:12:16 +0100 Subject: [PATCH 1/7] Implement DB URL connection support --- rethinkdb/net.py | 53 ++++++++---- tests/test_net.py | 200 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 tests/test_net.py diff --git a/rethinkdb/net.py b/rethinkdb/net.py index 5a4c8ddc..564ca92d 100644 --- a/rethinkdb/net.py +++ b/rethinkdb/net.py @@ -25,6 +25,11 @@ import struct import time +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + from rethinkdb import ql2_pb2 from rethinkdb.ast import DB, ReQLDecoder, ReQLEncoder, Repl, expr from rethinkdb.errors import ( @@ -703,9 +708,6 @@ def __init__(self, *args, **kwargs): Connection.__init__(self, ConnectionInstance, *args, **kwargs) - - - def make_connection( connection_type, host=None, @@ -716,20 +718,41 @@ def make_connection( password=None, timeout=20, ssl=None, + url=None, _handshake_version=10, **kwargs): - if host is None: - host = 'localhost' - if port is None: - port = DEFAULT_PORT - if user is None: - user = 'admin' - if timeout is None: - timeout = 20 - if ssl is None: - ssl = dict() - if _handshake_version is None: - _handshake_version = 10 + if url: + connection_string = urlparse(url) + query_string = parse_qs(connection_string.query) + + # Reverse the tuple, this way we can ensure that the host:port/user:pass + # will be always at the same position + host_port, _, user_pass = connection_string.netloc.partition("@")[::-1] + user, password = user_pass.partition(":")[0], user_pass.partition(":")[2] + host, port = host_port.partition(":")[0], host_port.partition(":")[2] + + db = connection_string.path.replace("/", "") or None + auth_key = query_string.get("auth_key") + timeout = query_string.get("timeout") + + if auth_key: + auth_key = auth_key[0] + + if timeout: + timeout = int(timeout[0]) + + + host = host or 'localhost' + port = port or DEFAULT_PORT + user = user or 'admin' + timeout = timeout or 20 + ssl = ssl or dict() + _handshake_version = _handshake_version or 10 + + # The internal APIs will wait for none to deal with auth_key and password + # TODO: refactor when we drop python2 + if not password and not password is None: + password = None conn = connection_type(host, port, db, auth_key, user, password, timeout, ssl, _handshake_version, **kwargs) return conn.reconnect(timeout=timeout) diff --git a/tests/test_net.py b/tests/test_net.py new file mode 100644 index 00000000..cba2f5d1 --- /dev/null +++ b/tests/test_net.py @@ -0,0 +1,200 @@ +import pytest +from mock import Mock, ANY +from rethinkdb.net import make_connection, DefaultConnection, DEFAULT_PORT + + +@pytest.mark.unit +class TestMakeConnection(object): + def setup_method(self): + self.reconnect = Mock() + self.conn_type = Mock() + self.conn_type.return_value.reconnect.return_value = self.reconnect + + self.host = "myhost" + self.port = "1234" + self.db = "mydb" + self.auth_key = None + self.user = "gabor" + self.password = "strongpass" + self.timeout = 20 + + + def test_make_connection(self): + ssl = dict() + _handshake_version = 10 + + conn = make_connection( + self.conn_type, + host=self.host, + port=self.port, + db=self.db, + auth_key=self.auth_key, + user=self.user, + password=self.password, + timeout=self.timeout, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + self.port, + self.db, + self.auth_key, + self.user, + self.password, + self.timeout, + ssl, + _handshake_version + ) + + + def test_make_connection_db_url(self): + url = "rethinkdb://gabor:strongpass@myhost:1234/mydb?auth_key=mykey&timeout=30" + ssl = dict() + _handshake_version = 10 + + conn = make_connection(self.conn_type, url=url) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + self.port, + self.db, + "mykey", + self.user, + self.password, + 30, + ssl, + _handshake_version + ) + + + def test_make_connection_no_host(self): + conn = make_connection( + self.conn_type, + port=self.port, + db=self.db, + auth_key=self.auth_key, + user=self.user, + password=self.password, + timeout=self.timeout, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + "localhost", + self.port, + self.db, + self.auth_key, + self.user, + self.password, + self.timeout, + ANY, + ANY + ) + + + def test_make_connection_no_port(self): + conn = make_connection( + self.conn_type, + host=self.host, + db=self.db, + auth_key=self.auth_key, + user=self.user, + password=self.password, + timeout=self.timeout, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + DEFAULT_PORT, + self.db, + self.auth_key, + self.user, + self.password, + self.timeout, + ANY, + ANY + ) + + + def test_make_connection_no_user(self): + conn = make_connection( + self.conn_type, + host=self.host, + port=self.port, + db=self.db, + auth_key=self.auth_key, + password=self.password, + timeout=self.timeout, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + self.port, + self.db, + self.auth_key, + "admin", + self.password, + self.timeout, + ANY, + ANY + ) + + + def test_make_connection_with_ssl(self): + ssl = dict() + + conn = make_connection( + self.conn_type, + host=self.host, + port=self.port, + db=self.db, + auth_key=self.auth_key, + user=self.user, + password=self.password, + timeout=self.timeout, + ssl=ssl, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + self.port, + self.db, + self.auth_key, + self.user, + self.password, + self.timeout, + ssl, + ANY + ) + + + def test_make_connection_different_handshake_version(self): + conn = make_connection( + self.conn_type, + host=self.host, + port=self.port, + db=self.db, + auth_key=self.auth_key, + user=self.user, + password=self.password, + timeout=self.timeout, + _handshake_version=20, + ) + + assert conn == self.reconnect + self.conn_type.assert_called_once_with( + self.host, + self.port, + self.db, + self.auth_key, + self.user, + self.password, + self.timeout, + ANY, + 20 + ) From 0d7d95b48bbb9f7dad9ad808db84e588f45eda75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 06:56:26 +0100 Subject: [PATCH 2/7] Make the auth info and host parsing simpler --- rethinkdb/net.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rethinkdb/net.py b/rethinkdb/net.py index 564ca92d..e879673a 100644 --- a/rethinkdb/net.py +++ b/rethinkdb/net.py @@ -725,11 +725,10 @@ def make_connection( connection_string = urlparse(url) query_string = parse_qs(connection_string.query) - # Reverse the tuple, this way we can ensure that the host:port/user:pass - # will be always at the same position - host_port, _, user_pass = connection_string.netloc.partition("@")[::-1] - user, password = user_pass.partition(":")[0], user_pass.partition(":")[2] - host, port = host_port.partition(":")[0], host_port.partition(":")[2] + user = connection_string.username + password = connection_string.password + host = connection_string.hostname + port = connection_string.port db = connection_string.path.replace("/", "") or None auth_key = query_string.get("auth_key") From 8cfb1dc349577673c7b27534c0a4cfd593ebf153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 07:26:29 +0100 Subject: [PATCH 3/7] Fix minor issue in integration test helpers --- tests/helpers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index b666050e..91a02574 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -6,18 +6,21 @@ class IntegrationTestCaseBase(object): + def _create_database(self, conn): + if INTEGRATION_TEST_DB not in self.r.db_list().run(conn): + self.r.db_create(INTEGRATION_TEST_DB).run(conn) + + conn.use(INTEGRATION_TEST_DB) + def setup_method(self): self.r = r - self.rethinkdb_host = os.getenv('RETHINKDB_HOST') + self.rethinkdb_host = os.getenv('RETHINKDB_HOST', '127.0.0.1') self.conn = self.r.connect( host=self.rethinkdb_host ) - if INTEGRATION_TEST_DB not in self.r.db_list().run(self.conn): - self.r.db_create(INTEGRATION_TEST_DB).run(self.conn) - - self.conn.use(INTEGRATION_TEST_DB) + self._create_database(self.conn) def teardown_method(self): self.r.db_drop(INTEGRATION_TEST_DB).run(self.conn) From 7af2c794d6030c3998310d0c7f010857463af806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 07:26:49 +0100 Subject: [PATCH 4/7] Add db url connect integration tests --- tests/integration/test_connect.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/integration/test_connect.py diff --git a/tests/integration/test_connect.py b/tests/integration/test_connect.py new file mode 100644 index 00000000..77213eb7 --- /dev/null +++ b/tests/integration/test_connect.py @@ -0,0 +1,29 @@ +import os +import pytest + +from rethinkdb import r +from tests.helpers import IntegrationTestCaseBase, INTEGRATION_TEST_DB + + +@pytest.mark.integration +class TestConnect(IntegrationTestCaseBase): + def setup_method(self): + super(TestConnect, self).setup_method() + + def test_connect(self): + db_url = "rethinkdb://{host}".format(host=self.rethinkdb_host) + + assert self.r.connect(url=db_url) is not None + + def test_connect_with_username(self): + db_url = "rethinkdb://admin@{host}".format(host=self.rethinkdb_host) + + assert self.r.connect(url=db_url) is not None + + def test_connect_to_db(self): + db_url = "rethinkdb://{host}/{database}".format( + host=self.rethinkdb_host, + database=INTEGRATION_TEST_DB + ) + + assert self.r.connect(url=db_url) is not None From ccb476bee00a190ef9c2e7b26fb5cf1b0a579a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 07:33:06 +0100 Subject: [PATCH 5/7] Fix failing unit tests --- tests/test_net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_net.py b/tests/test_net.py index cba2f5d1..76b3027a 100644 --- a/tests/test_net.py +++ b/tests/test_net.py @@ -11,7 +11,7 @@ def setup_method(self): self.conn_type.return_value.reconnect.return_value = self.reconnect self.host = "myhost" - self.port = "1234" + self.port = 1234 self.db = "mydb" self.auth_key = None self.user = "gabor" From 01227802fea6fb788e16d23eb219d380c6c3910f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 07:36:27 +0100 Subject: [PATCH 6/7] Remove trailing whitespace --- rethinkdb/net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rethinkdb/net.py b/rethinkdb/net.py index e879673a..7b3c774d 100644 --- a/rethinkdb/net.py +++ b/rethinkdb/net.py @@ -724,7 +724,7 @@ def make_connection( if url: connection_string = urlparse(url) query_string = parse_qs(connection_string.query) - + user = connection_string.username password = connection_string.password host = connection_string.hostname From 7317a80d6f49bdf6b68c00f52deca33e309d2fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Mon, 17 Feb 2020 07:39:00 +0100 Subject: [PATCH 7/7] Fix unit test issue only exists on py3.5 --- tests/integration/test_write_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_write_hooks.py b/tests/integration/test_write_hooks.py index 2ef0128c..cf40cd8d 100644 --- a/tests/integration/test_write_hooks.py +++ b/tests/integration/test_write_hooks.py @@ -48,4 +48,4 @@ def test_get_write_hook(self): hook = self.r.table(self.table_name).get_write_hook().run(self.conn) - assert list(hook.keys()) == ['function', 'query'] \ No newline at end of file + assert list(sorted(hook.keys())) == ['function', 'query']