From a5f46eb32a29faa885643c8226d290f4ae38642a Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Wed, 30 Jul 2025 18:47:06 +0530 Subject: [PATCH 1/6] telemetry circuit breaker Signed-off-by: Sai Shree Pradhan --- poetry.lock | 16 +++++- pyproject.toml | 1 + src/databricks/sql/common/http.py | 17 +++++- tests/unit/test_telemetry.py | 95 ++++++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index b68d1a3fb..0b4390019 100644 --- a/poetry.lock +++ b/poetry.lock @@ -870,6 +870,20 @@ files = [ [package.extras] test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] +[[package]] +name = "pybreaker" +version = "0.6.0" +description = "Python implementation of the Circuit Breaker pattern" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pybreaker-0.6.0.tar.gz", hash = "sha256:038c2808d4f845bf7e17819ad278aa5701c6512a5e389760bc4657be0a5364d3"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "pyjwt" version = "2.9.0" @@ -1176,4 +1190,4 @@ pyarrow = ["pyarrow", "pyarrow"] [metadata] lock-version = "2.1" python-versions = "^3.8.0" -content-hash = "0305d9a30397e4baa3d02d0a920989a901ba08749b93bd1c433886f151ed2cdc" +content-hash = "4f5fde10f5fba65edbbe9285fba8f83fb033667e98a1fee5f61f83f0102daacc" diff --git a/pyproject.toml b/pyproject.toml index 9b862d7ac..eff8b3546 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ oauthlib = "^3.1.0" openpyxl = "^3.0.10" urllib3 = ">=1.26" python-dateutil = "^2.8.0" +pybreaker = "^0.6.0" pyarrow = [ { version = ">=14.0.1", python = ">=3.8,<3.13", optional=true }, { version = ">=18.0.0", python = ">=3.13", optional=true } diff --git a/src/databricks/sql/common/http.py b/src/databricks/sql/common/http.py index 0cd2919c0..7cb9129a5 100644 --- a/src/databricks/sql/common/http.py +++ b/src/databricks/sql/common/http.py @@ -9,6 +9,7 @@ import logging from requests.adapters import HTTPAdapter from databricks.sql.auth.retry import DatabricksRetryPolicy, CommandType +from pybreaker import CircuitBreaker, CircuitBreakerError logger = logging.getLogger(__name__) @@ -109,6 +110,9 @@ class TelemetryHttpClient: # TODO: Unify all the http clients in the PySQL Conn TELEMETRY_RETRY_DELAY_MAX = 10.0 TELEMETRY_RETRY_STOP_AFTER_ATTEMPTS_DURATION = 30.0 + CIRCUIT_BREAKER_FAIL_MAX = 5 + CIRCUIT_BREAKER_RESET_TIMEOUT = 60 + def __init__(self): """Initializes the session and mounts the custom retry adapter.""" retry_policy = DatabricksRetryPolicy( @@ -123,6 +127,10 @@ def __init__(self): self.session = requests.Session() self.session.mount("https://", adapter) self.session.mount("http://", adapter) + self.breaker = CircuitBreaker( + fail_max=self.CIRCUIT_BREAKER_FAIL_MAX, + reset_timeout=self.CIRCUIT_BREAKER_RESET_TIMEOUT, + ) @classmethod def get_instance(cls) -> "TelemetryHttpClient": @@ -141,7 +149,14 @@ def post(self, url: str, **kwargs) -> requests.Response: This is a blocking call intended to be run in a background thread. """ logger.debug("Executing telemetry POST request to: %s", url) - return self.session.post(url, **kwargs) + try: + return self.breaker.call(self.session.post, url, **kwargs) + except CircuitBreakerError as e: + logger.error("Circuit breaker error: %s", e) + raise e + except Exception as e: + logger.error("Error executing telemetry POST request: %s", e) + raise e def close(self): """Closes the underlying requests.Session.""" diff --git a/tests/unit/test_telemetry.py b/tests/unit/test_telemetry.py index 398387540..3d150cf99 100644 --- a/tests/unit/test_telemetry.py +++ b/tests/unit/test_telemetry.py @@ -1,13 +1,16 @@ import uuid import pytest from unittest.mock import patch, MagicMock +import time +import requests +from pybreaker import CircuitBreakerError +from databricks.sql.common.http import TelemetryHttpClient from databricks.sql.telemetry.telemetry_client import ( TelemetryClient, NoopTelemetryClient, TelemetryClientFactory, TelemetryHelper, - BaseTelemetryClient, ) from databricks.sql.telemetry.models.enums import AuthMech, AuthFlow from databricks.sql.auth.authenticators import ( @@ -316,3 +319,93 @@ def test_connection_failure_sends_correct_telemetry_payload( call_arguments = mock_export_failure_log.call_args assert call_arguments[0][0] == "Exception" assert call_arguments[0][1] == error_message + + +class TestTelemetryHttpClient: + """Tests for the TelemetryHttpClient, including retry and circuit breaker logic.""" + + @pytest.fixture + def http_client(self): + """ + Provides a fresh TelemetryHttpClient instance for each test, + ensuring the singleton state is reset. + """ + if TelemetryHttpClient._instance: + TelemetryHttpClient.get_instance().close() + + client = TelemetryHttpClient.get_instance() + yield client + + client.close() + + def test_circuit_breaker_opens_after_failures(self, http_client): + """Verify the circuit opens after N consecutive failures and rejects new calls.""" + fail_max = 3 + http_client.breaker.fail_max = fail_max + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = requests.exceptions.RequestException("Connection failed") + + for _ in range(fail_max - 1): + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com/telemetry") + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com/telemetry") + + assert http_client.breaker.current_state == "open" + assert mock_post.call_count == fail_max + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com/telemetry") + assert mock_post.call_count == fail_max + + def test_circuit_breaker_closes_after_timeout_and_success(self, http_client): + """Verify the circuit moves to half-open and then closes after a successful probe.""" + fail_max = 2 + reset_timeout = 0.1 + http_client.breaker.fail_max = fail_max + http_client.breaker.reset_timeout = reset_timeout + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = [ + requests.exceptions.RequestException("Fail 1"), + requests.exceptions.RequestException("Fail 2"), + MagicMock(ok=True) + ] + + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com") + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.breaker.current_state == "open" + time.sleep(reset_timeout) + + http_client.post("https://test.com") + assert http_client.breaker.current_state == "closed" + assert mock_post.call_count == 3 + + def test_circuit_breaker_reopens_if_probe_fails(self, http_client): + """Verify the circuit moves to half-open and then back to open if the probe fails.""" + fail_max = 2 + reset_timeout = 0.1 + http_client.breaker.fail_max = fail_max + http_client.breaker.reset_timeout = reset_timeout + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = requests.exceptions.RequestException("Always fails") + + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com") + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.breaker.current_state == "open" + time.sleep(reset_timeout) + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.breaker.current_state == "open" + assert mock_post.call_count == 3 \ No newline at end of file From 1b713bdc38a3fe39f7114b78a37fd0a91f9727ef Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Wed, 30 Jul 2025 18:49:18 +0530 Subject: [PATCH 2/6] Revert "telemetry circuit breaker" This reverts commit a5f46eb32a29faa885643c8226d290f4ae38642a. Signed-off-by: Sai Shree Pradhan --- poetry.lock | 16 +----- pyproject.toml | 1 - src/databricks/sql/common/http.py | 17 +----- tests/unit/test_telemetry.py | 95 +------------------------------ 4 files changed, 3 insertions(+), 126 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0b4390019..b68d1a3fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -870,20 +870,6 @@ files = [ [package.extras] test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] -[[package]] -name = "pybreaker" -version = "0.6.0" -description = "Python implementation of the Circuit Breaker pattern" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pybreaker-0.6.0.tar.gz", hash = "sha256:038c2808d4f845bf7e17819ad278aa5701c6512a5e389760bc4657be0a5364d3"}, -] - -[package.dependencies] -six = "*" - [[package]] name = "pyjwt" version = "2.9.0" @@ -1190,4 +1176,4 @@ pyarrow = ["pyarrow", "pyarrow"] [metadata] lock-version = "2.1" python-versions = "^3.8.0" -content-hash = "4f5fde10f5fba65edbbe9285fba8f83fb033667e98a1fee5f61f83f0102daacc" +content-hash = "0305d9a30397e4baa3d02d0a920989a901ba08749b93bd1c433886f151ed2cdc" diff --git a/pyproject.toml b/pyproject.toml index eff8b3546..9b862d7ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ oauthlib = "^3.1.0" openpyxl = "^3.0.10" urllib3 = ">=1.26" python-dateutil = "^2.8.0" -pybreaker = "^0.6.0" pyarrow = [ { version = ">=14.0.1", python = ">=3.8,<3.13", optional=true }, { version = ">=18.0.0", python = ">=3.13", optional=true } diff --git a/src/databricks/sql/common/http.py b/src/databricks/sql/common/http.py index 7cb9129a5..0cd2919c0 100644 --- a/src/databricks/sql/common/http.py +++ b/src/databricks/sql/common/http.py @@ -9,7 +9,6 @@ import logging from requests.adapters import HTTPAdapter from databricks.sql.auth.retry import DatabricksRetryPolicy, CommandType -from pybreaker import CircuitBreaker, CircuitBreakerError logger = logging.getLogger(__name__) @@ -110,9 +109,6 @@ class TelemetryHttpClient: # TODO: Unify all the http clients in the PySQL Conn TELEMETRY_RETRY_DELAY_MAX = 10.0 TELEMETRY_RETRY_STOP_AFTER_ATTEMPTS_DURATION = 30.0 - CIRCUIT_BREAKER_FAIL_MAX = 5 - CIRCUIT_BREAKER_RESET_TIMEOUT = 60 - def __init__(self): """Initializes the session and mounts the custom retry adapter.""" retry_policy = DatabricksRetryPolicy( @@ -127,10 +123,6 @@ def __init__(self): self.session = requests.Session() self.session.mount("https://", adapter) self.session.mount("http://", adapter) - self.breaker = CircuitBreaker( - fail_max=self.CIRCUIT_BREAKER_FAIL_MAX, - reset_timeout=self.CIRCUIT_BREAKER_RESET_TIMEOUT, - ) @classmethod def get_instance(cls) -> "TelemetryHttpClient": @@ -149,14 +141,7 @@ def post(self, url: str, **kwargs) -> requests.Response: This is a blocking call intended to be run in a background thread. """ logger.debug("Executing telemetry POST request to: %s", url) - try: - return self.breaker.call(self.session.post, url, **kwargs) - except CircuitBreakerError as e: - logger.error("Circuit breaker error: %s", e) - raise e - except Exception as e: - logger.error("Error executing telemetry POST request: %s", e) - raise e + return self.session.post(url, **kwargs) def close(self): """Closes the underlying requests.Session.""" diff --git a/tests/unit/test_telemetry.py b/tests/unit/test_telemetry.py index 3d150cf99..398387540 100644 --- a/tests/unit/test_telemetry.py +++ b/tests/unit/test_telemetry.py @@ -1,16 +1,13 @@ import uuid import pytest from unittest.mock import patch, MagicMock -import time -import requests -from pybreaker import CircuitBreakerError -from databricks.sql.common.http import TelemetryHttpClient from databricks.sql.telemetry.telemetry_client import ( TelemetryClient, NoopTelemetryClient, TelemetryClientFactory, TelemetryHelper, + BaseTelemetryClient, ) from databricks.sql.telemetry.models.enums import AuthMech, AuthFlow from databricks.sql.auth.authenticators import ( @@ -319,93 +316,3 @@ def test_connection_failure_sends_correct_telemetry_payload( call_arguments = mock_export_failure_log.call_args assert call_arguments[0][0] == "Exception" assert call_arguments[0][1] == error_message - - -class TestTelemetryHttpClient: - """Tests for the TelemetryHttpClient, including retry and circuit breaker logic.""" - - @pytest.fixture - def http_client(self): - """ - Provides a fresh TelemetryHttpClient instance for each test, - ensuring the singleton state is reset. - """ - if TelemetryHttpClient._instance: - TelemetryHttpClient.get_instance().close() - - client = TelemetryHttpClient.get_instance() - yield client - - client.close() - - def test_circuit_breaker_opens_after_failures(self, http_client): - """Verify the circuit opens after N consecutive failures and rejects new calls.""" - fail_max = 3 - http_client.breaker.fail_max = fail_max - - with patch.object(http_client.session, "post") as mock_post: - mock_post.side_effect = requests.exceptions.RequestException("Connection failed") - - for _ in range(fail_max - 1): - with pytest.raises(requests.exceptions.RequestException): - http_client.post("https://test.com/telemetry") - - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com/telemetry") - - assert http_client.breaker.current_state == "open" - assert mock_post.call_count == fail_max - - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com/telemetry") - assert mock_post.call_count == fail_max - - def test_circuit_breaker_closes_after_timeout_and_success(self, http_client): - """Verify the circuit moves to half-open and then closes after a successful probe.""" - fail_max = 2 - reset_timeout = 0.1 - http_client.breaker.fail_max = fail_max - http_client.breaker.reset_timeout = reset_timeout - - with patch.object(http_client.session, "post") as mock_post: - mock_post.side_effect = [ - requests.exceptions.RequestException("Fail 1"), - requests.exceptions.RequestException("Fail 2"), - MagicMock(ok=True) - ] - - with pytest.raises(requests.exceptions.RequestException): - http_client.post("https://test.com") - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com") - - assert http_client.breaker.current_state == "open" - time.sleep(reset_timeout) - - http_client.post("https://test.com") - assert http_client.breaker.current_state == "closed" - assert mock_post.call_count == 3 - - def test_circuit_breaker_reopens_if_probe_fails(self, http_client): - """Verify the circuit moves to half-open and then back to open if the probe fails.""" - fail_max = 2 - reset_timeout = 0.1 - http_client.breaker.fail_max = fail_max - http_client.breaker.reset_timeout = reset_timeout - - with patch.object(http_client.session, "post") as mock_post: - mock_post.side_effect = requests.exceptions.RequestException("Always fails") - - with pytest.raises(requests.exceptions.RequestException): - http_client.post("https://test.com") - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com") - - assert http_client.breaker.current_state == "open" - time.sleep(reset_timeout) - - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com") - - assert http_client.breaker.current_state == "open" - assert mock_post.call_count == 3 \ No newline at end of file From ec0f5a75303c3728ae5bf8de2c1ac5dee03e7e26 Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Wed, 30 Jul 2025 19:42:07 +0530 Subject: [PATCH 3/6] pybreaker Signed-off-by: Sai Shree Pradhan --- src/databricks/sql/common/http.py | 17 +++++- tests/unit/test_telemetry.py | 95 ++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/databricks/sql/common/http.py b/src/databricks/sql/common/http.py index 0cd2919c0..e1a235e85 100644 --- a/src/databricks/sql/common/http.py +++ b/src/databricks/sql/common/http.py @@ -9,6 +9,7 @@ import logging from requests.adapters import HTTPAdapter from databricks.sql.auth.retry import DatabricksRetryPolicy, CommandType +from pybreaker import CircuitBreaker, CircuitBreakerError logger = logging.getLogger(__name__) @@ -109,6 +110,9 @@ class TelemetryHttpClient: # TODO: Unify all the http clients in the PySQL Conn TELEMETRY_RETRY_DELAY_MAX = 10.0 TELEMETRY_RETRY_STOP_AFTER_ATTEMPTS_DURATION = 30.0 + CIRCUIT_BREAKER_FAIL_MAX = 5 + CIRCUIT_BREAKER_RESET_TIMEOUT = 60 + def __init__(self): """Initializes the session and mounts the custom retry adapter.""" retry_policy = DatabricksRetryPolicy( @@ -123,6 +127,10 @@ def __init__(self): self.session = requests.Session() self.session.mount("https://", adapter) self.session.mount("http://", adapter) + self.circuit_breaker = CircuitBreaker( + fail_max=self.CIRCUIT_BREAKER_FAIL_MAX, + reset_timeout=self.CIRCUIT_BREAKER_RESET_TIMEOUT, + ) @classmethod def get_instance(cls) -> "TelemetryHttpClient": @@ -141,7 +149,14 @@ def post(self, url: str, **kwargs) -> requests.Response: This is a blocking call intended to be run in a background thread. """ logger.debug("Executing telemetry POST request to: %s", url) - return self.session.post(url, **kwargs) + try: + return self.circuit_breaker.call(self.session.post, url, **kwargs) + except CircuitBreakerError as e: + logger.error("Circuit breaker error: %s", e) + raise e + except Exception as e: + logger.error("Error executing telemetry POST request: %s", e) + raise e def close(self): """Closes the underlying requests.Session.""" diff --git a/tests/unit/test_telemetry.py b/tests/unit/test_telemetry.py index 398387540..8c724a248 100644 --- a/tests/unit/test_telemetry.py +++ b/tests/unit/test_telemetry.py @@ -1,13 +1,16 @@ import uuid import pytest from unittest.mock import patch, MagicMock +import time +import requests +from pybreaker import CircuitBreakerError +from databricks.sql.common.http import TelemetryHttpClient from databricks.sql.telemetry.telemetry_client import ( TelemetryClient, NoopTelemetryClient, TelemetryClientFactory, TelemetryHelper, - BaseTelemetryClient, ) from databricks.sql.telemetry.models.enums import AuthMech, AuthFlow from databricks.sql.auth.authenticators import ( @@ -316,3 +319,93 @@ def test_connection_failure_sends_correct_telemetry_payload( call_arguments = mock_export_failure_log.call_args assert call_arguments[0][0] == "Exception" assert call_arguments[0][1] == error_message + + +class TestTelemetryHttpClient: + """Tests for the TelemetryHttpClient, including retry and circuit breaker logic.""" + + @pytest.fixture + def http_client(self): + """ + Provides a fresh TelemetryHttpClient instance for each test, + ensuring the singleton state is reset. + """ + if TelemetryHttpClient._instance: + TelemetryHttpClient.get_instance().close() + + client = TelemetryHttpClient.get_instance() + yield client + + client.close() + + def test_circuit_breaker_opens_after_failures(self, http_client): + """Verify the circuit opens after N consecutive failures and rejects new calls.""" + fail_max = 3 + http_client.circuit_breaker.fail_max = fail_max + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = requests.exceptions.RequestException("Connection failed") + + for _ in range(fail_max - 1): + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com/telemetry") + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com/telemetry") + + assert http_client.circuit_breaker.current_state == "open" + assert mock_post.call_count == fail_max + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com/telemetry") + assert mock_post.call_count == fail_max + + def test_circuit_breaker_closes_after_timeout_and_success(self, http_client): + """Verify the circuit moves to half-open and then closes after a successful probe.""" + fail_max = 2 + reset_timeout = 0.1 + http_client.circuit_breaker.fail_max = fail_max + http_client.circuit_breaker.reset_timeout = reset_timeout + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = [ + requests.exceptions.RequestException("Fail 1"), + requests.exceptions.RequestException("Fail 2"), + MagicMock(ok=True) + ] + + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com") + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.circuit_breaker.current_state == "open" + time.sleep(reset_timeout) + + http_client.post("https://test.com") + assert http_client.circuit_breaker.current_state == "closed" + assert mock_post.call_count == 3 + + def test_circuit_breaker_reopens_if_probe_fails(self, http_client): + """Verify the circuit moves to half-open and then back to open if the probe fails.""" + fail_max = 2 + reset_timeout = 0.1 + http_client.circuit_breaker.fail_max = fail_max + http_client.circuit_breaker.reset_timeout = reset_timeout + + with patch.object(http_client.session, "post") as mock_post: + mock_post.side_effect = requests.exceptions.RequestException("Always fails") + + with pytest.raises(requests.exceptions.RequestException): + http_client.post("https://test.com") + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.circuit_breaker.current_state == "open" + time.sleep(reset_timeout) + + with pytest.raises(CircuitBreakerError): + http_client.post("https://test.com") + + assert http_client.circuit_breaker.current_state == "open" + assert mock_post.call_count == 3 \ No newline at end of file From 9a87af37cb3b794230c2d799512094141c2a3c87 Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Wed, 30 Jul 2025 19:53:11 +0530 Subject: [PATCH 4/6] dependency Signed-off-by: Sai Shree Pradhan --- README.md | 2 +- poetry.lock | 152 +++++-------------------------------------------- pyproject.toml | 7 ++- 3 files changed, 19 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index a4c5a1307..a1ea3bd89 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ You are welcome to file an issue here for general use cases. You can also contac ## Requirements -Python 3.8 or above is required. +Python 3.9 or above is required. ## Documentation diff --git a/poetry.lock b/poetry.lock index b68d1a3fb..0da9e6872 100644 --- a/poetry.lock +++ b/poetry.lock @@ -228,7 +228,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -421,7 +421,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "python_version < \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, @@ -460,7 +460,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["main", "dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.11\"" files = [ {file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"}, {file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"}, @@ -563,71 +563,6 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -[[package]] -name = "pandas" -version = "2.0.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, -] - -[package.dependencies] -numpy = {version = ">=1.20.3", markers = "python_version < \"3.10\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] - [[package]] name = "pandas" version = "2.2.3" @@ -635,7 +570,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -761,59 +695,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pyarrow" -version = "17.0.0" -description = "Python library for Apache Arrow" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.10\" and extra == \"pyarrow\"" -files = [ - {file = "pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07"}, - {file = "pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047"}, - {file = "pyarrow-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087"}, - {file = "pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977"}, - {file = "pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4"}, - {file = "pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03"}, - {file = "pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22"}, - {file = "pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b"}, - {file = "pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"}, - {file = "pyarrow-17.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204"}, - {file = "pyarrow-17.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c"}, - {file = "pyarrow-17.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca"}, - {file = "pyarrow-17.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb"}, - {file = "pyarrow-17.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda"}, - {file = "pyarrow-17.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204"}, - {file = "pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28"}, -] - -[package.dependencies] -numpy = ">=1.16.6" - -[package.extras] -test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] - [[package]] name = "pyarrow" version = "19.0.1" @@ -821,7 +702,7 @@ description = "Python library for Apache Arrow" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\" and extra == \"pyarrow\"" +markers = "extra == \"pyarrow\"" files = [ {file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69"}, {file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec"}, @@ -871,23 +752,19 @@ files = [ test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] [[package]] -name = "pyjwt" -version = "2.9.0" -description = "JSON Web Token implementation in Python" +name = "pybreaker" +version = "1.4.0" +description = "Python implementation of the Circuit Breaker pattern" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, + {file = "pybreaker-1.4.0-py3-none-any.whl", hash = "sha256:98b3324575293ba5e62be0098041481eda603a934c4d59857bb724ecfbea4a4c"}, + {file = "pybreaker-1.4.0.tar.gz", hash = "sha256:82910927d504aca596b5266964eaeabf41361aff30feb31d568d3e530fcc6f2b"}, ] [package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +test = ["fakeredis", "mock", "pytest", "redis", "tornado", "types-mock", "types-redis"] [[package]] name = "pyjwt" @@ -896,7 +773,6 @@ description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -1080,7 +956,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1175,5 +1051,5 @@ pyarrow = ["pyarrow", "pyarrow"] [metadata] lock-version = "2.1" -python-versions = "^3.8.0" -content-hash = "0305d9a30397e4baa3d02d0a920989a901ba08749b93bd1c433886f151ed2cdc" +python-versions = "^3.9.0" +content-hash = "a5781c244e175814aec22a46cee6444caa280af43010cd6a7e43539a91002710" diff --git a/pyproject.toml b/pyproject.toml index 9b862d7ac..0a01651fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ packages = [{ include = "databricks", from = "src" }] include = ["CHANGELOG.md"] [tool.poetry.dependencies] -python = "^3.8.0" +python = "^3.9.0" thrift = ">=0.16.0,<0.21.0" pandas = [ { version = ">=1.2.5,<2.3.0", python = ">=3.8,<3.13" }, @@ -22,10 +22,11 @@ openpyxl = "^3.0.10" urllib3 = ">=1.26" python-dateutil = "^2.8.0" pyarrow = [ - { version = ">=14.0.1", python = ">=3.8,<3.13", optional=true }, + { version = ">=14.0.1", python = ">=3.9,<3.13", optional=true }, { version = ">=18.0.0", python = ">=3.13", optional=true } ] pyjwt = "^2.0.0" +pybreaker = "^1.4.0" [tool.poetry.extras] @@ -38,7 +39,7 @@ pylint = ">=2.12.0" black = "^22.3.0" pytest-dotenv = "^0.5.2" numpy = [ - { version = ">=1.16.6", python = ">=3.8,<3.11" }, + { version = ">=1.16.6", python = ">=3.9,<3.11" }, { version = ">=1.23.4", python = ">=3.11" }, ] From aebb46a0bf0695dd60affcf94760e0ed992808e6 Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Fri, 1 Aug 2025 10:30:09 +0530 Subject: [PATCH 5/6] test Signed-off-by: Sai Shree Pradhan --- tests/unit/test_telemetry.py | 69 ++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_telemetry.py b/tests/unit/test_telemetry.py index 8c724a248..d2bc5879d 100644 --- a/tests/unit/test_telemetry.py +++ b/tests/unit/test_telemetry.py @@ -338,53 +338,68 @@ def http_client(self): client.close() - def test_circuit_breaker_opens_after_failures(self, http_client): - """Verify the circuit opens after N consecutive failures and rejects new calls.""" + def test_circuit_breaker_full_lifecycle(self, http_client): + """ + Verifies the full lifecycle of the circuit breaker: + 1. Starts closed. + 2. Opens on the Nth consecutive failure. + 3. Rejects new calls immediately while open. + 4. Transitions to half-open after the reset timeout. + 5. Closes after a single successful call in the half-open state. + """ + fail_max = 3 + reset_timeout = 1 http_client.circuit_breaker.fail_max = fail_max + http_client.circuit_breaker.reset_timeout = reset_timeout with patch.object(http_client.session, "post") as mock_post: - mock_post.side_effect = requests.exceptions.RequestException("Connection failed") + # Define the sequence of mock behaviors: 3 failures, then 1 success + mock_post.side_effect = [ + requests.exceptions.RequestException("Connection failed 1"), + requests.exceptions.RequestException("Connection failed 2"), + requests.exceptions.RequestException("Connection failed 3"), + MagicMock(ok=True, status_code=200) # The successful probe call + ] - for _ in range(fail_max - 1): - with pytest.raises(requests.exceptions.RequestException): + # Cause N-1 Failures (Circuit should remain closed) + # These first two calls should fail normally without opening the circuit. + for i in range(fail_max - 1): + with pytest.raises(requests.exceptions.RequestException, match=f"Connection failed {i+1}"): http_client.post("https://test.com/telemetry") + + # Verify state: circuit is still closed, but the counter has increased + assert http_client.circuit_breaker.current_state == "closed" + assert mock_post.call_count == fail_max - 1 + # Cause the Nth Failure (This will open the circuit) + # This is the call that trips the breaker. We expect a CircuitBreakerError. with pytest.raises(CircuitBreakerError): http_client.post("https://test.com/telemetry") + # Verify state: circuit is now open and the network call was still made assert http_client.circuit_breaker.current_state == "open" assert mock_post.call_count == fail_max + # Verify the Circuit is Open + # Any subsequent call should be rejected immediately without a network request. with pytest.raises(CircuitBreakerError): http_client.post("https://test.com/telemetry") - assert mock_post.call_count == fail_max - - def test_circuit_breaker_closes_after_timeout_and_success(self, http_client): - """Verify the circuit moves to half-open and then closes after a successful probe.""" - fail_max = 2 - reset_timeout = 0.1 - http_client.circuit_breaker.fail_max = fail_max - http_client.circuit_breaker.reset_timeout = reset_timeout - - with patch.object(http_client.session, "post") as mock_post: - mock_post.side_effect = [ - requests.exceptions.RequestException("Fail 1"), - requests.exceptions.RequestException("Fail 2"), - MagicMock(ok=True) - ] - with pytest.raises(requests.exceptions.RequestException): - http_client.post("https://test.com") - with pytest.raises(CircuitBreakerError): - http_client.post("https://test.com") + assert mock_post.call_count == fail_max - assert http_client.circuit_breaker.current_state == "open" + # Wait for the reset timeout to elapse. time.sleep(reset_timeout) - http_client.post("https://test.com") + # Make one more call. Since the circuit is half-open, this will be let through. + # Our mock is configured for this call to succeed. + http_client.post("https://test.com/telemetry") + + # After the successful probe, the circuit should immediately close. assert http_client.circuit_breaker.current_state == "closed" - assert mock_post.call_count == 3 + + # Verify that the successful probe call was actually made + assert mock_post.call_count == fail_max + 1 def test_circuit_breaker_reopens_if_probe_fails(self, http_client): """Verify the circuit moves to half-open and then back to open if the probe fails.""" From bec62fd2b63b90c81276c1a4d4e608763744552d Mon Sep 17 00:00:00 2001 From: Sai Shree Pradhan Date: Fri, 1 Aug 2025 14:12:59 +0530 Subject: [PATCH 6/6] logging Signed-off-by: Sai Shree Pradhan --- src/databricks/sql/common/http.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/databricks/sql/common/http.py b/src/databricks/sql/common/http.py index e1a235e85..88c493c43 100644 --- a/src/databricks/sql/common/http.py +++ b/src/databricks/sql/common/http.py @@ -149,14 +149,7 @@ def post(self, url: str, **kwargs) -> requests.Response: This is a blocking call intended to be run in a background thread. """ logger.debug("Executing telemetry POST request to: %s", url) - try: - return self.circuit_breaker.call(self.session.post, url, **kwargs) - except CircuitBreakerError as e: - logger.error("Circuit breaker error: %s", e) - raise e - except Exception as e: - logger.error("Error executing telemetry POST request: %s", e) - raise e + return self.circuit_breaker.call(self.session.post, url, **kwargs) def close(self): """Closes the underlying requests.Session."""