From 4a1dc4775edf8b89b6d55a29c9d779c35cd47d51 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Fri, 15 May 2026 13:06:31 +0200 Subject: [PATCH 1/4] Added gzip compression for server and client side request/response bodies --- CHANGES.rst | 8 +++ docs/connect.rst | 41 ++++++++++++++ src/crate/client/connection.py | 23 ++++++++ src/crate/client/http.py | 29 +++++++++- tests/client/test_http.py | 98 ++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1f42b690..2645544c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,14 @@ Changes for crate Unreleased ========== +- Added gzip compression for outgoing request bodies (``compress_client=True``, + default enabled). Use ``compress_threshold`` (default: ``8192`` bytes) to + skip compression on small payloads. Server-side response compression + (``compress_server``) is available but defaults to ``False`` to avoid + BREACH-class oracle attacks on TLS-compressed responses; opt in explicitly + with ``compress_server=True``. Server-side response compression requires + ``http.compression=true`` in server configuration. + - Added named parameter support (``pyformat`` paramstyle). Passing a :class:`py:dict` as ``parameters`` to ``cursor.execute()`` now accepts ``%(name)s`` placeholders and converts them to positional ``?`` markers diff --git a/docs/connect.rst b/docs/connect.rst index fca3a667..b692826c 100644 --- a/docs/connect.rst +++ b/docs/connect.rst @@ -266,6 +266,47 @@ with the rest of your arguments. However, you can query any schema you like by specifying it in the query. +.. _compression: + +Request and response compression +================================= + +By default, ``crate-python`` compresses outgoing request bodies with gzip +(``compress_client=True``). Response compression is opt-in (``compress_server`` +defaults to ``False``; see the security note below):: + + >>> connection = client.connect('localhost:4200') + # compress_client=True, compress_server=False are the defaults + +To disable client-side request compression:: + + >>> connection = client.connect('localhost:4200', compress_client=False) + +Compression is skipped for request bodies smaller than ``compress_threshold`` +bytes (default ``8192``). This avoids CPU overhead on tiny payloads where +bandwidth savings are negligible:: + + >>> connection = client.connect('localhost:4200', compress_threshold=16384) + +To enable server-side response compression, set ``compress_server=True``. The +server must also have ``http.compression=true``. The client +sends ``Accept-Encoding: gzip, deflate`` and urllib3 decompresses responses +transparently:: + + >>> connection = client.connect('localhost:4200', compress_server=True) + +.. NOTE:: + + ``compress_server`` defaults to ``False`` as a precaution against + `BREACH`_-class attacks. BREACH allows an attacker who can both observe + TLS traffic *and* inject content into requests to gradually recover secrets + from compressed HTTP responses. CrateDB SQL responses do not contain + credentials, so the practical risk is low for most deployments. Enable + ``compress_server=True`` explicitly if your deployment benefits from + response compression and you have assessed the risk. + +.. _BREACH: https://en.wikipedia.org/wiki/BREACH + Next steps ========== diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index c9fa1340..fd3fed21 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -51,6 +51,10 @@ def __init__( converter=None, time_zone=None, jwt_token=None, + compress_client=True, + compress_threshold=8192, + compress_algorithm="gzip", + compress_server=False, ): """ :param servers: @@ -131,6 +135,21 @@ def __init__( converted from UTC to use the given time zone. :param jwt_token: the JWT token to authenticate with the server. + :param compress_client: + (optional, defaults to ``True``) + Compress outgoing request bodies with gzip. Payloads smaller than + ``compress_threshold`` bytes are sent uncompressed. + :param compress_threshold: + (optional, defaults to ``8192``) + Minimum request body size in bytes to trigger client-side compression. + :param compress_algorithm: + (optional, defaults to ``"gzip"``) + Compression algorithm. Only ``"gzip"`` is supported in this version. + :param compress_server: + (optional, defaults to ``False``) + Send ``Accept-Encoding: gzip`` so the server may return compressed + responses. Disabled by default to avoid BREACH-class oracle attacks + on compressed TLS responses. """ # noqa: E501 self._converter = converter @@ -158,6 +177,10 @@ def __init__( socket_tcp_keepintvl=socket_tcp_keepintvl, socket_tcp_keepcnt=socket_tcp_keepcnt, jwt_token=jwt_token, + compress_client=compress_client, + compress_threshold=compress_threshold, + compress_algorithm=compress_algorithm, + compress_server=compress_server, ) self.lowest_server_version = self._lowest_server_version() self._closed = False diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 2026cdbb..bc3848b2 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -22,6 +22,7 @@ import calendar import datetime as dt +import gzip import heapq import io import logging @@ -463,6 +464,10 @@ def __init__( socket_tcp_keepintvl=None, socket_tcp_keepcnt=None, jwt_token=None, + compress_client=True, + compress_threshold=8192, + compress_algorithm="gzip", + compress_server=False, ): if not servers: servers = [self.default_server] @@ -516,6 +521,16 @@ def __init__( self.jwt_token = jwt_token self.schema = schema + if compress_algorithm != "gzip": + raise ValueError( + f"Unsupported compress_algorithm: {compress_algorithm!r}. " + "Only 'gzip' is supported." + ) + self.compress_client = compress_client + self.compress_threshold = compress_threshold + self.compress_algorithm = compress_algorithm + self.compress_server = compress_server + self.path = self.SQL_PATH if error_trace: self.path += "&error_trace=true" @@ -678,8 +693,20 @@ def _json_request(self, method, path, data): """ Issue request against the crate HTTP API. """ + headers = {} + + if self.compress_server: + headers["Accept-Encoding"] = "gzip, deflate" + + if ( + self.compress_client + and self.compress_algorithm == "gzip" + and len(data) >= self.compress_threshold + ): + data = gzip.compress(data, compresslevel=6) + headers["Content-Encoding"] = "gzip" - response = self._request(method, path, data=data) + response = self._request(method, path, data=data, headers=headers or None) _raise_for_status(response) if len(response.data) > 0: return _json_from_response(response) diff --git a/tests/client/test_http.py b/tests/client/test_http.py index e3c49cb1..61eb88ab 100644 --- a/tests/client/test_http.py +++ b/tests/client/test_http.py @@ -19,6 +19,7 @@ # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +import gzip import json import os import queue @@ -735,3 +736,100 @@ def test_credentials_and_token(serve_http): assert excinfo.match( "Either JWT tokens are accepted, or user credentials, but not both" ) + +def test_compress_client_disabled(): + """ + No Content-Encoding header when client compression is off. + """ + captured = {} + + def capturing(method, path, **kwargs): + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client(servers="localhost:4200", compress_client=False, compress_server=False) + client.sql("SELECT 1") + assert "Content-Encoding" not in captured["headers"] + + +def test_compress_client_enabled(): + """Request body is gzip-compressed and Content-Encoding header is set.""" + captured = {} + + def capturing(method, path, **kwargs): + captured["data"] = kwargs.get("data", b"") + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client( + servers="localhost:4200", + compress_client=True, + compress_threshold=0, + compress_server=False, + ) + client.sql("SELECT 1") + assert captured["headers"].get("Content-Encoding") == "gzip" + assert b'"stmt"' in gzip.decompress(captured["data"]) + + +def test_compress_client_below_threshold(): + """No Content-Encoding header when payload is below the threshold.""" + captured = {} + + def capturing(method, path, **kwargs): + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client( + servers="localhost:4200", + compress_client=True, + compress_threshold=999_999, + compress_server=False, + ) + client.sql("SELECT 1") + assert "Content-Encoding" not in captured["headers"] + + +def test_compress_server_sends_accept_encoding(): + """Accept-Encoding: gzip, deflate header is sent when server compression is on.""" + captured = {} + + def capturing(method, path, **kwargs): + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client(servers="localhost:4200", compress_client=False, compress_server=True) + client.sql("SELECT 1") + assert captured["headers"].get("Accept-Encoding") == "gzip, deflate" + + +def test_compress_server_disabled(): + """No Accept-Encoding header when server compression is off.""" + captured = {} + + def capturing(method, path, **kwargs): + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client(servers="localhost:4200", compress_client=False, compress_server=False) + client.sql("SELECT 1") + assert "Accept-Encoding" not in captured["headers"] + + +def test_compress_server_default_disabled(): + """No Accept-Encoding header when Client is instantiated with default args.""" + captured = {} + + def capturing(method, path, **kwargs): + captured["headers"] = kwargs.get("headers") or {} + return fake_response(200) + + with patch(REQUEST_PATH, side_effect=capturing): + client = Client(servers="localhost:4200") + client.sql("SELECT 1") + assert "Accept-Encoding" not in captured["headers"] From bd8cd53c21321495a6a6cacca029e6c8e84c7581 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Fri, 15 May 2026 13:29:23 +0200 Subject: [PATCH 2/4] Fix linter errors --- src/crate/client/http.py | 4 +++- tests/client/test_http.py | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/crate/client/http.py b/src/crate/client/http.py index bc3848b2..89f7a303 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -706,7 +706,9 @@ def _json_request(self, method, path, data): data = gzip.compress(data, compresslevel=6) headers["Content-Encoding"] = "gzip" - response = self._request(method, path, data=data, headers=headers or None) + response = self._request( + method, path, data=data, headers=headers or None + ) _raise_for_status(response) if len(response.data) > 0: return _json_from_response(response) diff --git a/tests/client/test_http.py b/tests/client/test_http.py index 61eb88ab..799e5b50 100644 --- a/tests/client/test_http.py +++ b/tests/client/test_http.py @@ -748,7 +748,11 @@ def capturing(method, path, **kwargs): return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client(servers="localhost:4200", compress_client=False, compress_server=False) + client = Client( + servers="localhost:4200", + compress_client=False, + compress_server=False, + ) client.sql("SELECT 1") assert "Content-Encoding" not in captured["headers"] @@ -794,7 +798,7 @@ def capturing(method, path, **kwargs): def test_compress_server_sends_accept_encoding(): - """Accept-Encoding: gzip, deflate header is sent when server compression is on.""" + """Accept-Encoding: gzip, deflate is sent when server compression is on.""" captured = {} def capturing(method, path, **kwargs): @@ -802,7 +806,11 @@ def capturing(method, path, **kwargs): return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client(servers="localhost:4200", compress_client=False, compress_server=True) + client = Client( + servers="localhost:4200", + compress_client=False, + compress_server=True, + ) client.sql("SELECT 1") assert captured["headers"].get("Accept-Encoding") == "gzip, deflate" @@ -816,13 +824,17 @@ def capturing(method, path, **kwargs): return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client(servers="localhost:4200", compress_client=False, compress_server=False) + client = Client( + servers="localhost:4200", + compress_client=False, + compress_server=False, + ) client.sql("SELECT 1") assert "Accept-Encoding" not in captured["headers"] def test_compress_server_default_disabled(): - """No Accept-Encoding header when Client is instantiated with default args.""" + """No Accept-Encoding header with Client instantiated by default args.""" captured = {} def capturing(method, path, **kwargs): From c4a6eaa19d960b5016e513ebd6ce3d6baa25a34a Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 19 May 2026 10:35:34 +0200 Subject: [PATCH 3/4] Merge multiple args to 'compress' argument for http compression --- CHANGES.rst | 13 +++-- docs/connect.rst | 37 ++++--------- src/crate/client/connection.py | 29 +++-------- src/crate/client/http.py | 34 +++--------- tests/client/test_http.py | 95 ++++++++++++---------------------- 5 files changed, 65 insertions(+), 143 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2645544c..84dd336b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,13 +5,12 @@ Changes for crate Unreleased ========== -- Added gzip compression for outgoing request bodies (``compress_client=True``, - default enabled). Use ``compress_threshold`` (default: ``8192`` bytes) to - skip compression on small payloads. Server-side response compression - (``compress_server``) is available but defaults to ``False`` to avoid - BREACH-class oracle attacks on TLS-compressed responses; opt in explicitly - with ``compress_server=True``. Server-side response compression requires - ``http.compression=true`` in server configuration. +- Added gzip compression for outgoing request bodies via the ``compress`` + parameter (default: ``8192`` bytes). + Pass ``True`` to always compress, ``False`` to disable, or an integer + as a byte threshold. The driver always sends ``Accept-Encoding: gzip, + deflate`` to negotiate compressed responses from the server when + compression is enabled. - Added named parameter support (``pyformat`` paramstyle). Passing a :class:`py:dict` as ``parameters`` to ``cursor.execute()`` now accepts diff --git a/docs/connect.rst b/docs/connect.rst index b692826c..afc8f59c 100644 --- a/docs/connect.rst +++ b/docs/connect.rst @@ -271,41 +271,26 @@ with the rest of your arguments. Request and response compression ================================= -By default, ``crate-python`` compresses outgoing request bodies with gzip -(``compress_client=True``). Response compression is opt-in (``compress_server`` -defaults to ``False``; see the security note below):: +The ``compress`` parameter controls gzip compression of outgoing request +bodies. The default ``8192`` compresses payloads larger than 8 KB:: >>> connection = client.connect('localhost:4200') - # compress_client=True, compress_server=False are the defaults + # compress=8192 is the default — payloads > 8 KB are gzip-compressed -To disable client-side request compression:: +To always compress, regardless of payload size:: - >>> connection = client.connect('localhost:4200', compress_client=False) + >>> connection = client.connect('localhost:4200', compress=True) -Compression is skipped for request bodies smaller than ``compress_threshold`` -bytes (default ``8192``). This avoids CPU overhead on tiny payloads where -bandwidth savings are negligible:: +To disable compression entirely:: - >>> connection = client.connect('localhost:4200', compress_threshold=16384) + >>> connection = client.connect('localhost:4200', compress=False) -To enable server-side response compression, set ``compress_server=True``. The -server must also have ``http.compression=true``. The client -sends ``Accept-Encoding: gzip, deflate`` and urllib3 decompresses responses -transparently:: +To use a custom threshold (bytes):: - >>> connection = client.connect('localhost:4200', compress_server=True) + >>> connection = client.connect('localhost:4200', compress=4096) -.. NOTE:: - - ``compress_server`` defaults to ``False`` as a precaution against - `BREACH`_-class attacks. BREACH allows an attacker who can both observe - TLS traffic *and* inject content into requests to gradually recover secrets - from compressed HTTP responses. CrateDB SQL responses do not contain - credentials, so the practical risk is low for most deployments. Enable - ``compress_server=True`` explicitly if your deployment benefits from - response compression and you have assessed the risk. - -.. _BREACH: https://en.wikipedia.org/wiki/BREACH +The driver always sends ``Accept-Encoding: gzip, deflate`` so the server +may return compressed responses if compression is enabled. Next steps ========== diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index fd3fed21..221cdc52 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -51,10 +51,7 @@ def __init__( converter=None, time_zone=None, jwt_token=None, - compress_client=True, - compress_threshold=8192, - compress_algorithm="gzip", - compress_server=False, + compress=8192, ): """ :param servers: @@ -135,21 +132,12 @@ def __init__( converted from UTC to use the given time zone. :param jwt_token: the JWT token to authenticate with the server. - :param compress_client: - (optional, defaults to ``True``) - Compress outgoing request bodies with gzip. Payloads smaller than - ``compress_threshold`` bytes are sent uncompressed. - :param compress_threshold: + :param compress: (optional, defaults to ``8192``) - Minimum request body size in bytes to trigger client-side compression. - :param compress_algorithm: - (optional, defaults to ``"gzip"``) - Compression algorithm. Only ``"gzip"`` is supported in this version. - :param compress_server: - (optional, defaults to ``False``) - Send ``Accept-Encoding: gzip`` so the server may return compressed - responses. Disabled by default to avoid BREACH-class oracle attacks - on compressed TLS responses. + Controls gzip compression of outgoing request bodies. + ``False`` disables compression entirely. + ``True`` compresses every request regardless of size. + An integer compresses only when the payload exceeds that many bytes. """ # noqa: E501 self._converter = converter @@ -177,10 +165,7 @@ def __init__( socket_tcp_keepintvl=socket_tcp_keepintvl, socket_tcp_keepcnt=socket_tcp_keepcnt, jwt_token=jwt_token, - compress_client=compress_client, - compress_threshold=compress_threshold, - compress_algorithm=compress_algorithm, - compress_server=compress_server, + compress=compress, ) self.lowest_server_version = self._lowest_server_version() self._closed = False diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 89f7a303..11ce376e 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -464,10 +464,7 @@ def __init__( socket_tcp_keepintvl=None, socket_tcp_keepcnt=None, jwt_token=None, - compress_client=True, - compress_threshold=8192, - compress_algorithm="gzip", - compress_server=False, + compress=8192, ): if not servers: servers = [self.default_server] @@ -520,16 +517,7 @@ def __init__( self.password = password self.jwt_token = jwt_token self.schema = schema - - if compress_algorithm != "gzip": - raise ValueError( - f"Unsupported compress_algorithm: {compress_algorithm!r}. " - "Only 'gzip' is supported." - ) - self.compress_client = compress_client - self.compress_threshold = compress_threshold - self.compress_algorithm = compress_algorithm - self.compress_server = compress_server + self.compress = compress self.path = self.SQL_PATH if error_trace: @@ -693,22 +681,16 @@ def _json_request(self, method, path, data): """ Issue request against the crate HTTP API. """ - headers = {} + headers = {"Accept-Encoding": "gzip, deflate"} - if self.compress_server: - headers["Accept-Encoding"] = "gzip, deflate" - - if ( - self.compress_client - and self.compress_algorithm == "gzip" - and len(data) >= self.compress_threshold - ): + compress_enabled = self.compress is True or ( + not isinstance(self.compress, bool) and len(data) >= self.compress + ) + if compress_enabled: data = gzip.compress(data, compresslevel=6) headers["Content-Encoding"] = "gzip" - response = self._request( - method, path, data=data, headers=headers or None - ) + response = self._request(method, path, data=data, headers=headers) _raise_for_status(response) if len(response.data) > 0: return _json_from_response(response) diff --git a/tests/client/test_http.py b/tests/client/test_http.py index 799e5b50..32350ab9 100644 --- a/tests/client/test_http.py +++ b/tests/client/test_http.py @@ -737,111 +737,82 @@ def test_credentials_and_token(serve_http): "Either JWT tokens are accepted, or user credentials, but not both" ) -def test_compress_client_disabled(): - """ - No Content-Encoding header when client compression is off. - """ +def test_compress_accept_encoding_always_sent(): + """Accept-Encoding is sent even when compression is disabled.""" captured = {} - def capturing(method, path, **kwargs): + def capturing(*_, **kwargs): captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client( - servers="localhost:4200", - compress_client=False, - compress_server=False, - ) - client.sql("SELECT 1") - assert "Content-Encoding" not in captured["headers"] + Client(servers="localhost:4200", compress=False).sql("SELECT 1") + assert captured["headers"].get("Accept-Encoding") == "gzip, deflate" -def test_compress_client_enabled(): - """Request body is gzip-compressed and Content-Encoding header is set.""" +def test_compress_false_no_content_encoding(): + """No Content-Encoding header when compress=False.""" captured = {} - def capturing(method, path, **kwargs): - captured["data"] = kwargs.get("data", b"") + def capturing(*_, **kwargs): captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client( - servers="localhost:4200", - compress_client=True, - compress_threshold=0, - compress_server=False, - ) - client.sql("SELECT 1") - assert captured["headers"].get("Content-Encoding") == "gzip" - assert b'"stmt"' in gzip.decompress(captured["data"]) + Client(servers="localhost:4200", compress=False).sql("SELECT 1") + assert "Content-Encoding" not in captured["headers"] -def test_compress_client_below_threshold(): - """No Content-Encoding header when payload is below the threshold.""" +def test_compress_true_always_compresses(): + """compress=True compresses regardless of payload size.""" captured = {} - def capturing(method, path, **kwargs): + def capturing(*_, **kwargs): + captured["data"] = kwargs.get("data", b"") captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client( - servers="localhost:4200", - compress_client=True, - compress_threshold=999_999, - compress_server=False, - ) - client.sql("SELECT 1") - assert "Content-Encoding" not in captured["headers"] + Client(servers="localhost:4200", compress=True).sql("SELECT 1") + assert captured["headers"].get("Content-Encoding") == "gzip" + assert b'"stmt"' in gzip.decompress(captured["data"]) -def test_compress_server_sends_accept_encoding(): - """Accept-Encoding: gzip, deflate is sent when server compression is on.""" +def test_compress_threshold_above(): + """Payload above threshold is compressed.""" captured = {} - def capturing(method, path, **kwargs): + def capturing(*_, **kwargs): captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client( - servers="localhost:4200", - compress_client=False, - compress_server=True, - ) - client.sql("SELECT 1") - assert captured["headers"].get("Accept-Encoding") == "gzip, deflate" + Client(servers="localhost:4200", compress=0).sql("SELECT 1") + assert captured["headers"].get("Content-Encoding") == "gzip" -def test_compress_server_disabled(): - """No Accept-Encoding header when server compression is off.""" +def test_compress_threshold_below(): + """Payload below threshold is not compressed.""" captured = {} - def capturing(method, path, **kwargs): + def capturing(*_, **kwargs): captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client( - servers="localhost:4200", - compress_client=False, - compress_server=False, - ) - client.sql("SELECT 1") - assert "Accept-Encoding" not in captured["headers"] + Client(servers="localhost:4200", compress=999_999).sql("SELECT 1") + assert "Content-Encoding" not in captured["headers"] -def test_compress_server_default_disabled(): - """No Accept-Encoding header with Client instantiated by default args.""" +def test_compress_default(): + """Default args: Accept-Encoding sent, small payload not compressed.""" captured = {} - def capturing(method, path, **kwargs): + def capturing(*_, **kwargs): captured["headers"] = kwargs.get("headers") or {} return fake_response(200) with patch(REQUEST_PATH, side_effect=capturing): - client = Client(servers="localhost:4200") - client.sql("SELECT 1") - assert "Accept-Encoding" not in captured["headers"] + Client(servers="localhost:4200").sql("SELECT 1") + assert captured["headers"].get("Accept-Encoding") == "gzip, deflate" + assert "Content-Encoding" not in captured["headers"] From 66e9c2ae3d941e4660fe8a7bb44945061bcf1700 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 20 May 2026 09:39:57 +0200 Subject: [PATCH 4/4] Add type check and annotations --- src/crate/client/connection.py | 4 +++- src/crate/client/http.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index 221cdc52..f722b848 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -19,6 +19,8 @@ # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +from typing import Union + from verlib2 import Version from .blob import BlobContainer @@ -51,7 +53,7 @@ def __init__( converter=None, time_zone=None, jwt_token=None, - compress=8192, + compress: Union[int, bool] = 8192, ): """ :param servers: diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 11ce376e..139330ff 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -464,7 +464,7 @@ def __init__( socket_tcp_keepintvl=None, socket_tcp_keepcnt=None, jwt_token=None, - compress=8192, + compress: t.Union[int, bool] = 8192, ): if not servers: servers = [self.default_server] @@ -489,7 +489,7 @@ def __init__( ) self._active_servers = servers - self._inactive_servers = [] + self._inactive_servers: t.List[t.Tuple[float, str, str]] = [] pool_kw = _pool_kw_args( verify_ssl_cert, ca_cert, @@ -508,7 +508,7 @@ def __init__( ) self.ssl_relax_minimum_version = ssl_relax_minimum_version self.backoff_factor = backoff_factor - self.server_pool = {} + self.server_pool: t.Dict[str, Server] = {} self._update_server_pool(servers, **pool_kw) self._pool_kw = pool_kw self._lock = threading.RLock() @@ -517,6 +517,11 @@ def __init__( self.password = password self.jwt_token = jwt_token self.schema = schema + + if not isinstance(compress, (bool, int)): + raise TypeError( + f"compress must be bool or int, got {type(compress).__name__!r}" + ) self.compress = compress self.path = self.SQL_PATH