From 8db4d1f3971cd72558b4d913866fb3d67f765cf8 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Tue, 16 Jun 2026 01:05:14 +0300 Subject: [PATCH 01/15] handle connection params/attrs and secure connection case + update root tests --- asynch/connection.py | 41 ++++++++++++++++++++---------------- asynch/proto/connection.py | 14 ++++++------ asynch/proto/utils/dsn.py | 7 +++--- asynch/proto/utils/escape.py | 8 ------- tests/test_connection.py | 15 +++++++------ tests/test_cursors.py | 6 +++--- 6 files changed, 43 insertions(+), 48 deletions(-) diff --git a/asynch/connection.py b/asynch/connection.py index 27383dcc..e8487951 100644 --- a/asynch/connection.py +++ b/asynch/connection.py @@ -15,21 +15,26 @@ def __init__( user: str = constants.DEFAULT_USER, password: str = constants.DEFAULT_PASSWORD, host: str = constants.DEFAULT_HOST, - port: int = constants.DEFAULT_PORT, + port: Optional[int] = None, database: str = constants.DEFAULT_DATABASE, cursor_cls=Cursor, echo: bool = False, stack_track: bool = False, + secure: bool = False, **kwargs, ): + self._dsn = dsn + if port is None: + port = constants.DEFAULT_SECURE_PORT if secure else constants.DEFAULT_PORT if dsn: config = parse_dsn(dsn) self._connection = ProtoConnection(**config, stack_track=stack_track, **kwargs) - user = config.get("user", None) or user - password = config.get("password", None) or password - host = config.get("host", None) or host - port = config.get("port", None) or port - database = config.get("database", None) or database + self._user = config.get("user") + self._password = config.get("password") + self._host = config.get("host") + self._port = config.get("port") + self._database = config.get("database") + self._secure = config.get("secure") else: self._connection = ProtoConnection( host=host, @@ -38,15 +43,15 @@ def __init__( user=user, password=password, stack_track=stack_track, + secure=secure, **kwargs, ) - self._dsn = dsn - # dsn parts - self._user = user - self._password = password - self._host = host - self._port = port - self._database = database + self._user = user + self._password = password + self._host = host + self._port = port + self._database = database + self._secure = secure # connection additional settings self._opened: bool = False self._closed: bool = False @@ -104,23 +109,23 @@ def status(self) -> str: raise ConnectionError(f"{self} is in an unknown state") @property - def host(self) -> str: + def host(self) -> Optional[str]: return self._host @property - def port(self) -> int: + def port(self) -> Optional[int]: return self._port @property - def user(self) -> str: + def user(self) -> Optional[str]: return self._user @property - def password(self) -> str: + def password(self) -> Optional[str]: return self._password @property - def database(self) -> str: + def database(self) -> Optional[str]: return self._database @property diff --git a/asynch/proto/connection.py b/asynch/proto/connection.py index 1c2f3070..fccdfd99 100644 --- a/asynch/proto/connection.py +++ b/asynch/proto/connection.py @@ -77,7 +77,7 @@ def __init__( # nosec:B107 user: str = constants.DEFAULT_USER, password: str = constants.DEFAULT_PASSWORD, host: str = constants.DEFAULT_HOST, - port: int = constants.DEFAULT_PORT, + port: Optional[int] = None, database: str = constants.DEFAULT_DATABASE, client_name: str = constants.CLIENT_NAME, connect_timeout: int = constants.DBMS_DEFAULT_CONNECT_TIMEOUT_SEC, @@ -91,21 +91,19 @@ def __init__( # nosec:B107 ssl_version=None, ca_certs=None, ciphers=None, - alt_hosts: str = None, + alt_hosts: str = "", stack_track=False, settings_is_important=False, **kwargs, ): + if port is None: + port = constants.DEFAULT_SECURE_PORT if secure else constants.DEFAULT_PORT self.stack_track = stack_track - if secure: - default_port = constants.DEFAULT_SECURE_PORT - else: - default_port = constants.DEFAULT_PORT - self.hosts = [(host, port or default_port)] + self.hosts = [(host, port)] if alt_hosts: for host in alt_hosts.split(","): url = urlparse(f"{ClickhouseScheme.clickhouse}://" + host) - self.hosts.append((url.hostname, url.port or default_port)) + self.hosts.append((url.hostname or host, url.port or port)) self.database = database self.host = None self.port = None diff --git a/asynch/proto/utils/dsn.py b/asynch/proto/utils/dsn.py index 3087cf2f..a6946066 100644 --- a/asynch/proto/utils/dsn.py +++ b/asynch/proto/utils/dsn.py @@ -16,8 +16,7 @@ _TIMEOUTS: set[str] = {"connect_timeout", "send_receive_timeout", "sync_request_timeout"} -class DSNError(Exception): - pass +class DSNError(Exception): ... def parse_dsn(dsn: str) -> dict[str, Any]: @@ -33,9 +32,9 @@ def parse_dsn(dsn: str) -> dict[str, Any]: :param dsn str: the DSN string - :raises DSNError: when parsing fails under the strict mode + :raises DSNError: DNS parsing failure(s) - :return: the dictionary of DSN string components + :return: DSN configuration dictionary :rtype: dict[str, Any] """ diff --git a/asynch/proto/utils/escape.py b/asynch/proto/utils/escape.py index c7dfe4fe..1afe60a6 100644 --- a/asynch/proto/utils/escape.py +++ b/asynch/proto/utils/escape.py @@ -22,28 +22,20 @@ def escape_param(item: Any) -> str: if item is None: return "NULL" - elif isinstance(item, datetime): return "'%s'" % item.strftime("%Y-%m-%d %H:%M:%S") - elif isinstance(item, date): return "'%s'" % item.strftime("%Y-%m-%d") - elif isinstance(item, string_types): return "'%s'" % "".join(escape_chars_map.get(c, c) for c in item) - elif isinstance(item, list): return "[%s]" % ", ".join(text_type(escape_param(x)) for x in item) - elif isinstance(item, tuple): return "(%s)" % ", ".join(text_type(escape_param(x)) for x in item) - elif isinstance(item, Enum): return escape_param(item.value) - elif isinstance(item, UUID): return "'%s'" % str(item) - else: return str(item) diff --git a/tests/test_connection.py b/tests/test_connection.py index 6810670f..0d519157 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3,6 +3,7 @@ import pytest from asynch.connection import Connection +from tests.conftest import CONNECTION_DSN HOST = "192.168.15.103" PORT = 10000 @@ -134,7 +135,7 @@ def test_connection_status_offline(): @pytest.mark.asyncio async def test_connection_status_online(): - conn = Connection() + conn = Connection(dsn=CONNECTION_DSN) conn_id = id(conn) repstr = f"<{conn.__class__.__name__} object at 0x{conn_id:x}" @@ -158,7 +159,7 @@ async def test_connection_status_online(): @pytest.mark.asyncio async def test_async_context_manager_interface(): - conn = Connection() + conn = Connection(dsn=CONNECTION_DSN) _test_connectivity_invariant(conn=conn) async with conn: @@ -178,7 +179,7 @@ async def test_async_context_manager_interface(): @pytest.mark.asyncio async def test_connection_ping(): - conn = Connection() # default + conn = Connection(dsn=CONNECTION_DSN) with pytest.raises(ConnectionError): await conn.ping() @@ -210,13 +211,13 @@ async def test_connection_cleanup(get_tcp_connections): # get the number of total TCP connections to the ClickHouse init_tcps = 0 - conn = Connection() + conn = Connection(dsn=CONNECTION_DSN) async with conn as cn: init_tcps = await get_tcp_connections(cn) # open-execute-close connections for _ in range(100): - async with Connection() as cn: + async with Connection(dsn=CONNECTION_DSN) as cn: async with cn.cursor() as cur: await cur.execute("SELECT 1") ret = await cur.fetchone() @@ -231,7 +232,7 @@ async def test_connection_cleanup(get_tcp_connections): @pytest.mark.asyncio async def test_connection_close(): - conn = Connection() + conn = Connection(dsn=CONNECTION_DSN) # it does not break await conn.close() @@ -239,7 +240,7 @@ async def test_connection_close(): assert not conn.opened assert conn.closed - async with Connection() as conn: + async with Connection(dsn=CONNECTION_DSN) as conn: assert conn.opened await conn.close() diff --git a/tests/test_cursors.py b/tests/test_cursors.py index c5288452..642e4754 100644 --- a/tests/test_cursors.py +++ b/tests/test_cursors.py @@ -58,7 +58,7 @@ async def test_fetchone(conn: Connection): ret = await cursor.fetchone() assert ret == (1,) - await cursor.execute("SELECT {val}", args={"val": 2}) + await cursor.execute("SELECT %(val)s", args={"val": 2}) ret = await cursor.fetchone() assert ret == (2,) @@ -74,7 +74,7 @@ async def test_fetchall(conn: Connection): ret = await cursor.fetchall() assert ret == [(1,)] - await cursor.execute("SELECT {val}", args={"val": 2}) + await cursor.execute("SELECT %(val)s", args={"val": 2}) ret = await cursor.fetchall() assert ret == [(2,)] @@ -86,7 +86,7 @@ async def test_dict_cursor(conn: Connection): ret = await cursor.fetchall() assert ret == [{"1": 1}] - await cursor.execute("SELECT {val}", args={"val": 2}) + await cursor.execute("SELECT %(val)s", args={"val": 2}) ret = await cursor.fetchall() assert ret == [{"2": 2}] From fb43371dc1f72b2721babab04175133bf7e34b89 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Tue, 16 Jun 2026 01:06:34 +0300 Subject: [PATCH 02/15] change .env credentials --- example.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example.env b/example.env index 99cf60ce..54cb02d3 100644 --- a/example.env +++ b/example.env @@ -1,5 +1,5 @@ CLICKHOUSE_HOST=localhost CLICKHOUSE_PORT=9000 -CLICKHOUSE_USER=default -CLICKHOUSE_PASSWORD=default -CLICKHOUSE_DB=default \ No newline at end of file +CLICKHOUSE_USER=ch +CLICKHOUSE_PASSWORD=P@s5W0rD +CLICKHOUSE_DB=test From c7bb6a27f8e545d8b8ce20aee56927caa9592a2e Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Tue, 16 Jun 2026 01:07:17 +0300 Subject: [PATCH 03/15] add project notes (NOTES.md) with 'How to test' section --- NOTES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 NOTES.md diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 00000000..cd48bfa9 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,15 @@ +# Project notes + +## Table of contents + +- [How to test](#how-to-test) + +### How to test + +A way (but not the sole one) to do it: + +1. make sure the virtual environment is activated (use `poetry shell` to activate it if necessary); +2. create a `.env` file (you can copypaste the contents of the [example.env](./example.env) file); +3. inject the environment variable, on Ubuntu you can do it like `export $(cat .env | xargs)`; +4. run `docker run --name clickhouse-server -p 8123:8123 -p 9000:9000 --ulimit nofile=262144:262144 -v clickhouse_data:/var/lib/clickhouse -v clickhouse_logs:/var/log/clickhouse-server -e CLICKHOUSE_USER=ch -e CLICKHOUSE_PASSWORD=P@s5W0rD -e CLICKHOUSE_DB=test clickhouse/clickhouse-server` in a terminal (assuming the contents of the [example.env](./example.env) file are copypasted, otherwise adjust the environment variables accordingly); +5. run `make test` in another terminal. From 0328a96ba9688f9b20dab82623093ac4e834f99a Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Tue, 16 Jun 2026 01:24:03 +0300 Subject: [PATCH 04/15] update pool tests --- asynch/pool.py | 8 +++----- tests/test_pool.py | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/asynch/pool.py b/asynch/pool.py index 7aa1f381..31d4560b 100644 --- a/asynch/pool.py +++ b/asynch/pool.py @@ -190,7 +190,7 @@ async def _init_connections(self, n: int, *, strict: bool = False) -> None: raise ValueError(msg) if (self._pool_size + n) > self.maxsize: msg = ( - f"{self} has the {self._pool_size} connections, " + f"{self} has {self._pool_size} connections, " f"adding {n} will exceed its maxsize ({self.maxsize})" ) raise AsynchPoolError(msg) @@ -207,7 +207,7 @@ async def _init_connections(self, n: int, *, strict: bool = False) -> None: for i in await asyncio.gather(*tasks, return_exceptions=True) if isinstance(i, Exception) ): - msg = f"failed to create the {n} connection(s) for the {self}" + msg = f"failed to create {n} connection(s) for {self}" raise AsynchPoolError(msg) async def _ensure_minsize_connections(self, *, strict: bool = False) -> None: @@ -243,9 +243,7 @@ async def connection(self) -> AsyncIterator[Connection]: async def startup(self) -> "Pool": """Initialise the pool. - When entering the context, - the pool get filled with connections - up to the pool `minsize` value. + The pool is filled with `minsize` connections. :return: a pool object with `minsize` opened connections :rtype: Pool diff --git a/tests/test_pool.py b/tests/test_pool.py index b392ea1c..98e842b2 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -8,6 +8,7 @@ from asynch.pool import Pool from asynch.proto import constants from asynch.proto.models.enums import PoolStatus +from tests.conftest import CONNECTION_DSN def _get_pool_size(pool: Pool) -> int: @@ -41,7 +42,7 @@ async def test_pool_repr(): assert repr(pool) == repstr min_size, max_size = 2, 3 - pool = Pool(minsize=min_size, maxsize=max_size) + pool = Pool(minsize=min_size, maxsize=max_size, dsn=CONNECTION_DSN) async with pool: repstr = ( f" Date: Tue, 16 Jun 2026 01:55:36 +0300 Subject: [PATCH 05/15] add PR #162 (mention #138) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1125a354..65bb29f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 0.3.2 - Restore %(param)s style param substition support. By @baconfield in #147 +- Fix connection secure parameter management. By @stankudrow in #162 (inspired by PR #138 by @barakor) ### 0.3.1 From 29744559e08d4d702489b074f7606a7b6e7f4d7a Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Wed, 17 Jun 2026 23:50:28 +0300 Subject: [PATCH 06/15] update root files --- NOTES.md | 10 +++++----- README.md | 2 +- example.env | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NOTES.md b/NOTES.md index cd48bfa9..76312d22 100644 --- a/NOTES.md +++ b/NOTES.md @@ -8,8 +8,8 @@ A way (but not the sole one) to do it: -1. make sure the virtual environment is activated (use `poetry shell` to activate it if necessary); -2. create a `.env` file (you can copypaste the contents of the [example.env](./example.env) file); -3. inject the environment variable, on Ubuntu you can do it like `export $(cat .env | xargs)`; -4. run `docker run --name clickhouse-server -p 8123:8123 -p 9000:9000 --ulimit nofile=262144:262144 -v clickhouse_data:/var/lib/clickhouse -v clickhouse_logs:/var/log/clickhouse-server -e CLICKHOUSE_USER=ch -e CLICKHOUSE_PASSWORD=P@s5W0rD -e CLICKHOUSE_DB=test clickhouse/clickhouse-server` in a terminal (assuming the contents of the [example.env](./example.env) file are copypasted, otherwise adjust the environment variables accordingly); -5. run `make test` in another terminal. +1. Make sure the virtual environment (venv) is activated (use `poetry shell` to activate it if necessary); +2. Create a `.env` file if necessary (you can copypaste the contents of the [example.env](./example.env) file); +3. Inject the environment variable from the `.env` file into the virtual environment -> on Ubuntu you can do it with `export $(cat .env | xargs)`; +4. Run a "clickhouse-server" container in a terminal. For example, `docker run --rm --name clickhouse-server -p 8123:8123 -p 9000:9000 --ulimit nofile=262144:262144 -v clickhouse_data:/var/lib/clickhouse -v clickhouse_logs:/var/log/clickhouse-server -e CLICKHOUSE_USER=ch -e CLICKHOUSE_PASSWORD=asynch -e CLICKHOUSE_DB=test clickhouse/clickhouse-server`; +5. Launch `make test` in another terminal. diff --git a/README.md b/README.md index a46021d1..330743fd 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ async def use_pool(): assert ret == (1,) ``` -Or, you may opne/close the pool manually: +Or, you may open/close the pool manually: ```python async def use_pool(): diff --git a/example.env b/example.env index 54cb02d3..5259339e 100644 --- a/example.env +++ b/example.env @@ -1,5 +1,5 @@ CLICKHOUSE_HOST=localhost CLICKHOUSE_PORT=9000 CLICKHOUSE_USER=ch -CLICKHOUSE_PASSWORD=P@s5W0rD +CLICKHOUSE_PASSWORD=asynch CLICKHOUSE_DB=test From a8ca1629f1d5ebfc25c960805eb01b79e48303ca Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Wed, 17 Jun 2026 23:51:01 +0300 Subject: [PATCH 07/15] add test_escape module (from #138m thanx to @barakor) --- tests/test_proto/utils/test_escape.py | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_proto/utils/test_escape.py diff --git a/tests/test_proto/utils/test_escape.py b/tests/test_proto/utils/test_escape.py new file mode 100644 index 00000000..6a208e3d --- /dev/null +++ b/tests/test_proto/utils/test_escape.py @@ -0,0 +1,61 @@ +from datetime import date, datetime +from enum import Enum +from uuid import UUID + +import pytest +from pytest import param + +from asynch.proto.utils.escape import escape_param, escape_params + + +@pytest.mark.parametrize( + ("item", "expected"), + [ + param(None, "NULL", id="None"), + param(date(2025, 5, 21), "'2025-05-21'", id="date"), + param(datetime(2025, 5, 21, 12, 0, 0), "'2025-05-21 12:00:00'", id="datetime"), + param("test", "'test'", id="str"), + param([1, 2, 3], "[1, 2, 3]", id="list"), + param((1, 2, 3), "(1, 2, 3)", id="tuple"), + param( + UUID("123e4567-e89b-12d3-a456-426614174000"), + "'123e4567-e89b-12d3-a456-426614174000'", + id="uuid", + ), + param(Enum("testEnum", {"test1": "test1", "test2": "test2"}).test1, "'test1'", id="enum"), + ], +) +def test_escape_param(item, expected): + assert escape_param(item) == expected + + +@pytest.mark.parametrize( + ("params", "expected"), + [ + param({"test": "test"}, {"test": "'test'"}, id="strings"), + param( + {"test": 1, "test2": 2, "test3": 0, "test4": -1}, + {"test": "1", "test2": "2", "test3": "0", "test4": "-1"}, + id="ints", + ), + param( + {"test": 1.0, "test2": 0.1, "test3": -0.1, "test4": 0.0}, + {"test": "1.0", "test2": "0.1", "test3": "-0.1", "test4": "0.0"}, + id="floats", + ), + param({"test": None}, {"test": "NULL"}, id="none"), + param({"test": date(2025, 5, 21)}, {"test": "'2025-05-21'"}, id="dates"), + param( + {"test": datetime(2025, 5, 21, 12, 0, 0)}, + {"test": "'2025-05-21 12:00:00'"}, + id="datetimes", + ), + param( + {"test": UUID("123e4567-e89b-12d3-a456-426614174000")}, + {"test": "'123e4567-e89b-12d3-a456-426614174000'"}, + id="uuids", + ), + ], +) +def test_escape_params(params, expected): + assert escape_params(params) == expected From 02952d67deb29a3cf44e751a07361de4dd833e77 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Wed, 17 Jun 2026 23:58:13 +0300 Subject: [PATCH 08/15] add get_default_port helper --- asynch/connection.py | 3 ++- asynch/proto/connection.py | 4 ++-- asynch/proto/utils/helpers.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/asynch/connection.py b/asynch/connection.py index e8487951..7730bea5 100644 --- a/asynch/connection.py +++ b/asynch/connection.py @@ -6,6 +6,7 @@ from asynch.proto.connection import Connection as ProtoConnection from asynch.proto.models.enums import ConnectionStatus from asynch.proto.utils.dsn import parse_dsn +from asynch.proto.utils.helpers import get_default_port class Connection: @@ -25,7 +26,7 @@ def __init__( ): self._dsn = dsn if port is None: - port = constants.DEFAULT_SECURE_PORT if secure else constants.DEFAULT_PORT + port = get_default_port(secure=secure) if dsn: config = parse_dsn(dsn) self._connection = ProtoConnection(**config, stack_track=stack_track, **kwargs) diff --git a/asynch/proto/connection.py b/asynch/proto/connection.py index fccdfd99..07566f73 100644 --- a/asynch/proto/connection.py +++ b/asynch/proto/connection.py @@ -36,7 +36,7 @@ from asynch.proto.streams.block import BlockReader, BlockWriter from asynch.proto.streams.buffered import BufferedReader, BufferedWriter from asynch.proto.utils.escape import escape_params -from asynch.proto.utils.helpers import chunks, column_chunks +from asynch.proto.utils.helpers import chunks, column_chunks, get_default_port logger = logging.getLogger(__name__) @@ -97,7 +97,7 @@ def __init__( # nosec:B107 **kwargs, ): if port is None: - port = constants.DEFAULT_SECURE_PORT if secure else constants.DEFAULT_PORT + port = get_default_port(secure=secure) self.stack_track = stack_track self.hosts = [(host, port)] if alt_hosts: diff --git a/asynch/proto/utils/helpers.py b/asynch/proto/utils/helpers.py index d8c0904d..e14b9c65 100644 --- a/asynch/proto/utils/helpers.py +++ b/asynch/proto/utils/helpers.py @@ -1,5 +1,21 @@ from itertools import islice, tee +from asynch.proto import constants + + +def get_default_port(*, secure: bool = False) -> int: + """Returns the default port. + + Args: + secure (bool): return the default secure port + + Returns: + int + """ + if secure: + return constants.DEFAULT_SECURE_PORT + return constants.DEFAULT_PORT + def chunks(seq, n): it = iter(seq) From 53f87a36b1b2826c539f95a15827a142ac938b41 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 00:01:56 +0300 Subject: [PATCH 09/15] fix get_default_port helper docstring --- asynch/proto/utils/helpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/asynch/proto/utils/helpers.py b/asynch/proto/utils/helpers.py index e14b9c65..6b638cb7 100644 --- a/asynch/proto/utils/helpers.py +++ b/asynch/proto/utils/helpers.py @@ -6,11 +6,10 @@ def get_default_port(*, secure: bool = False) -> int: """Returns the default port. - Args: - secure (bool): return the default secure port + :param secure bool: return the default secure port - Returns: - int + :returns: the default port + :rtype: int """ if secure: return constants.DEFAULT_SECURE_PORT From e45f3d7854ad0e1c248d25722a404c11b39f2204 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 02:37:17 +0300 Subject: [PATCH 10/15] Update asynch/proto/connection.py Co-authored-by: Barak <26878518+barakor@users.noreply.github.com> --- asynch/proto/connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/asynch/proto/connection.py b/asynch/proto/connection.py index 07566f73..11a9fbfa 100644 --- a/asynch/proto/connection.py +++ b/asynch/proto/connection.py @@ -96,8 +96,7 @@ def __init__( # nosec:B107 settings_is_important=False, **kwargs, ): - if port is None: - port = get_default_port(secure=secure) + port = get_default_port(secure=secure) if port is None else port self.stack_track = stack_track self.hosts = [(host, port)] if alt_hosts: From 5d6e43c8cf893b5c3aeff579e40b6492d6f1e6f9 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 02:37:29 +0300 Subject: [PATCH 11/15] Update asynch/connection.py Co-authored-by: Barak <26878518+barakor@users.noreply.github.com> --- asynch/connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/asynch/connection.py b/asynch/connection.py index 7730bea5..2d13c686 100644 --- a/asynch/connection.py +++ b/asynch/connection.py @@ -25,8 +25,7 @@ def __init__( **kwargs, ): self._dsn = dsn - if port is None: - port = get_default_port(secure=secure) + port = get_default_port(secure=secure) if port is None else port if dsn: config = parse_dsn(dsn) self._connection = ProtoConnection(**config, stack_track=stack_track, **kwargs) From 72950f509d992ec433f6fdcfbf08fc9e19e00226 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 11:09:00 +0300 Subject: [PATCH 12/15] swap CHANGELOG records for v0.3.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65bb29f3..d6a674b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ### 0.3.2 -- Restore %(param)s style param substition support. By @baconfield in #147 - Fix connection secure parameter management. By @stankudrow in #162 (inspired by PR #138 by @barakor) +- Restore %(param)s style param substition support. By @baconfield in #147 ### 0.3.1 From 13a51482bff37577591bf05a6de4c8c37748984d Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 22:49:37 +0300 Subject: [PATCH 13/15] fix connection configuration when DSN and connection params are specified --- asynch/connection.py | 105 ++++++++++++++++++++-------------- asynch/proto/connection.py | 15 ++++- asynch/proto/utils/helpers.py | 15 ----- 3 files changed, 77 insertions(+), 58 deletions(-) diff --git a/asynch/connection.py b/asynch/connection.py index 2d13c686..a1002aa0 100644 --- a/asynch/connection.py +++ b/asynch/connection.py @@ -1,63 +1,84 @@ -from typing import Optional +from typing import Any, Dict, Optional from asynch.cursors import Cursor from asynch.errors import NotSupportedError from asynch.proto import constants from asynch.proto.connection import Connection as ProtoConnection +from asynch.proto.connection import get_default_port from asynch.proto.models.enums import ConnectionStatus from asynch.proto.utils.dsn import parse_dsn -from asynch.proto.utils.helpers import get_default_port class Connection: def __init__( self, dsn: Optional[str] = None, - user: str = constants.DEFAULT_USER, - password: str = constants.DEFAULT_PASSWORD, - host: str = constants.DEFAULT_HOST, + user: Optional[str] = None, + password: Optional[str] = None, + host: Optional[str] = None, port: Optional[int] = None, - database: str = constants.DEFAULT_DATABASE, + database: Optional[str] = None, cursor_cls=Cursor, echo: bool = False, stack_track: bool = False, secure: bool = False, **kwargs, ): - self._dsn = dsn - port = get_default_port(secure=secure) if port is None else port - if dsn: - config = parse_dsn(dsn) - self._connection = ProtoConnection(**config, stack_track=stack_track, **kwargs) - self._user = config.get("user") - self._password = config.get("password") - self._host = config.get("host") - self._port = config.get("port") - self._database = config.get("database") - self._secure = config.get("secure") - else: - self._connection = ProtoConnection( - host=host, - port=port, - database=database, - user=user, - password=password, - stack_track=stack_track, - secure=secure, - **kwargs, - ) - self._user = user - self._password = password - self._host = host - self._port = port - self._database = database - self._secure = secure + self._proto_kwargs = self._get_proto_connection_kwargs( + dsn, + user, + password, + host, + port, + database, + secure=secure, + stack_track=stack_track, + **kwargs, + ) + self._connection = ProtoConnection(**self._proto_kwargs) + self._cursor_cls = cursor_cls + self._echo = echo # connection additional settings self._opened: bool = False self._closed: bool = False - self._cursor_cls = cursor_cls - self._connection_kwargs = kwargs - self._echo = echo + + @staticmethod + def _get_proto_connection_kwargs( + dsn: Optional[str], + user: Optional[str], + password: Optional[str], + host: Optional[str], + port: Optional[int], + database: Optional[str], + *, + secure: bool, + stack_track: bool, + **kwargs, + ) -> Dict[str, Any]: + conninfo: Dict[str, Any] = {} + if dsn: + config = parse_dsn(dsn) + conninfo = {**conninfo, **config} + # Parameters suppercede DSN parts! + # If a param is given and it is not False-evaliuated, this is it, + # otherwise the corresponding DSN part is used OR the param itself. + user = user or config.get("user", constants.DEFAULT_USER) + password = password or config.get("password", constants.DEFAULT_PASSWORD) + host = host or config.get("host", constants.DEFAULT_HOST) + port = port or config.get("port", get_default_port(secure=secure)) + database = database or config.get("database", constants.DEFAULT_DATABASE) + secure = secure or config.get("secure", secure) + return { + **conninfo, + "user": user, + "password": password, + "host": host, + "port": port, + "database": database, + "secure": secure, + "stack_track": stack_track, + **kwargs, + } async def __aenter__(self) -> "Connection": await self.connect() @@ -110,23 +131,23 @@ def status(self) -> str: @property def host(self) -> Optional[str]: - return self._host + return self._proto_kwargs.get("host") @property def port(self) -> Optional[int]: - return self._port + return self._proto_kwargs.get("port") @property def user(self) -> Optional[str]: - return self._user + return self._proto_kwargs.get("user") @property def password(self) -> Optional[str]: - return self._password + return self._proto_kwargs.get("password") @property def database(self) -> Optional[str]: - return self._database + return self._proto_kwargs.get("database") @property def echo(self) -> bool: diff --git a/asynch/proto/connection.py b/asynch/proto/connection.py index 11a9fbfa..4fa7fa69 100644 --- a/asynch/proto/connection.py +++ b/asynch/proto/connection.py @@ -36,11 +36,24 @@ from asynch.proto.streams.block import BlockReader, BlockWriter from asynch.proto.streams.buffered import BufferedReader, BufferedWriter from asynch.proto.utils.escape import escape_params -from asynch.proto.utils.helpers import chunks, column_chunks, get_default_port +from asynch.proto.utils.helpers import chunks, column_chunks logger = logging.getLogger(__name__) +def get_default_port(*, secure: bool = False) -> int: + """Returns the default port. + + :param secure bool: return the default secure port + + :returns: the default port + :rtype: int + """ + if secure: + return constants.DEFAULT_SECURE_PORT + return constants.DEFAULT_PORT + + class QueryProcessingStage: """Determines till which state SELECT query should be executed.""" diff --git a/asynch/proto/utils/helpers.py b/asynch/proto/utils/helpers.py index 6b638cb7..d8c0904d 100644 --- a/asynch/proto/utils/helpers.py +++ b/asynch/proto/utils/helpers.py @@ -1,20 +1,5 @@ from itertools import islice, tee -from asynch.proto import constants - - -def get_default_port(*, secure: bool = False) -> int: - """Returns the default port. - - :param secure bool: return the default secure port - - :returns: the default port - :rtype: int - """ - if secure: - return constants.DEFAULT_SECURE_PORT - return constants.DEFAULT_PORT - def chunks(seq, n): it = iter(seq) From 35e6f4cbafb8c3d3cebd6883b0be92c795e4109e Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 23:02:58 +0300 Subject: [PATCH 14/15] add test_connection_params --- tests/test_connection.py | 56 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 0d519157..4f7668aa 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -42,7 +42,7 @@ def _test_connectivity_invariant( assert conn.closed is is_closed -def test_dsn(): +def test_dsn() -> None: dsn = f"clickhouse://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" conn = Connection(dsn=dsn) @@ -52,6 +52,60 @@ def test_dsn(): _test_connectivity_invariant(conn=conn) +@pytest.mark.parametrize( + ("dsn", "params", "answer"), + [ + ( + "", + { + "host": HOST, + "port": PORT, + "user": USER, + "password": PASSWORD, + "database": DATABASE, + }, + { + "host": HOST, + "port": PORT, + "user": USER, + "password": PASSWORD, + "database": DATABASE, + }, + ), + ( + f"clickhouses://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}", + { + "host": "243.164.23.54", + "port": None, + "user": "test_user", + "database": "db", + }, + { + "host": "243.164.23.54", + "port": PORT, + "user": "test_user", + "password": PASSWORD, + "database": "db", + }, + ), + ( + f"clickhouses://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}", + {}, + { + "host": HOST, + "port": PORT, + "user": USER, + "password": PASSWORD, + "database": DATABASE, + }, + ), + ], +) +def test_connection_params(dsn: str, params: dict, answer: dict) -> None: + conn = Connection(dsn=dsn, **params) + _test_connection_credentials(conn, **answer) + + def test_secure_dsn(): dsn = ( f"clickhouses://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" From d2b1c2977c34b3315433f41502497d6b24fea426 Mon Sep 17 00:00:00 2001 From: Stanley Kudrow Date: Thu, 18 Jun 2026 23:05:38 +0300 Subject: [PATCH 15/15] mention @barakor as coauthor in #162 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a674b0..498b19d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ### 0.3.2 -- Fix connection secure parameter management. By @stankudrow in #162 (inspired by PR #138 by @barakor) -- Restore %(param)s style param substition support. By @baconfield in #147 +- Fix connection secure parameter management. By @stankudrow and @barakor in #162 (inspired by PR #138 by @barakor). +- Restore %(param)s style param substition support. By @baconfield in #147. ### 0.3.1