Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/code-quality-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
labels: linux-ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
dependency-version: ["default", "min"]
exclude:
- python-version: "3.12"
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
labels: linux-ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
dependency-version: ["default", "min"]
exclude:
- python-version: "3.12"
Expand Down Expand Up @@ -246,7 +246,7 @@ jobs:
labels: linux-ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Check out repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand All @@ -273,7 +273,7 @@ jobs:
labels: linux-ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Check out repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down
378 changes: 378 additions & 0 deletions .github/workflows/securityScan.yml

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions .github/workflows/warm-deps-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
# Mirror code-quality-checks.yml exactly so every fork check has a
# matching warmed entry. extras: "" is the base unit-test / lint / type
# environment; pyarrow and kernel back the two extra unit-test tiers.
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
dependency-version: ["default", "min"]
extras: ["", "pyarrow", "kernel"]
exclude:
Expand All @@ -98,10 +98,6 @@ jobs:
dependency-version: "min"
- python-version: "3.13"
dependency-version: "min"
# The kernel wheel is cp310-abi3 (Requires-Python >=3.10); the
# [kernel] extra is a no-op on 3.9, so there's no kernel leg to warm.
- python-version: "3.9"
extras: "kernel"

name: "Warm (py ${{ matrix.python-version }}, ${{ matrix.dependency-version }} deps, extras=${{ matrix.extras || 'base' }})"

Expand Down
18 changes: 18 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# OSV-Scanner suppressions for the databricks-sql-python security gate.
#
# Each entry suppresses a CVE that is a documented ecosystem false
# positive against an artifact we ship. Every entry has a justification.
#
# Trade-off worth noting: [[IgnoredVulns]] entries are CVE-id global --
# they ignore the CVE across all packages OSV reports it against, not
# just the artifact we have in mind. The alternative
# ([[PackageOverrides]] with `vulnerability.ignore = true`) is
# per-package but blanket-ignores ALL vulnerabilities on that package,
# which is much worse. OSV-Scanner v2.3.8 does NOT support an
# intersection ("this CVE on this package only").
#
# See google.github.io/osv-scanner/configuration/ for the schema.
#
# This file starts empty -- populate iteratively as the first scan run
# surfaces real false positives. Do not pre-populate with speculative
# suppressions.
2,471 changes: 1,261 additions & 1,210 deletions poetry.lock

Large diffs are not rendered by default.

45 changes: 30 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages = [{ include = "databricks", from = "src" }]
include = ["CHANGELOG.md"]

[tool.poetry.dependencies]
python = "^3.8.0"
python = "^3.10"
# Keep the cap at <0.23.0. thrift ships sdist-only, and thrift 0.23.0's
# setup.py calls sys.exit(0) on the build-success path, which kills the PEP
# 517 build backend before pip writes output.json. On the OLD setuptools
Expand All @@ -19,13 +19,31 @@ python = "^3.8.0"
# THRIFT-6067 / apache/thrift#3584) ships. The `DBR LTS Install` CI check
# (.github/workflows/dbr-lts-install.yml) installs the built wheel on real DBR
# LTS clusters and catches this regression when dependency pins change.
#
# SECURITY NOTE: thrift 0.22.0 IS affected by open Apache Thrift advisories
# (CVE-2025-48431 and the CVE-2026-41602..41636 set), all fixed in 0.23.0.
# We accept them here for two reasons:
# (1) Apache Thrift is one monorepo shipping ~20 language libraries; the PyPI
# `thrift` package is built only from `lib/py`. Each of these CVEs is in
# a NON-Python binding -- verified against the upstream advisories:
# Node.js (41636), Go (41602), c_glib/C (48431), Java (41603), Swift
# (41604, 41605) -- so none touches the Python code paths we ship.
# (2) The only fix (0.23.0) is the version that caused ES-1960554, so we
# cannot take it until a build-safe thrift ships (see above).
# NOTE these advisories are INVISIBLE to the OSV-Scanner gate: OSV and
# Dependabot match by PyPI purl, but these CVEs are filed only against the
# upstream CPE (cpe:2.3:a:apache:thrift) with no PyPI-package coordinate, so
# neither reports them. This is a point-in-time, per-CVE determination -- a
# FUTURE thrift CVE could land in `lib/py`. The weekly Security Scan therefore
# runs a supplementary NVD-CPE check for thrift (see securityScan.yml) so a
# Python-affecting thrift CVE surfaces instead of being silently missed.
thrift = "~=0.22.0"
pandas = [
{ version = ">=1.2.5,<4.0.0", python = ">=3.8,<3.13" },
{ version = ">=1.2.5,<4.0.0", python = ">=3.10,<3.13" },
{ version = ">=2.2.3,<4.0.0", python = ">=3.13" }
]
lz4 = [
{ version = "^4.0.2", python = ">=3.8,<3.14" },
{ version = "^4.0.2", python = ">=3.10,<3.14" },
{ version = "^4.4.5", python = ">=3.14" }
]
requests = "^2.18.1"
Expand All @@ -34,15 +52,6 @@ openpyxl = "^3.0.10"
urllib3 = ">=1.26"
python-dateutil = "^2.8.0"
pyarrow = [
# The <3.10 band is capped at <23 because pyarrow>=23 dropped
# Python 3.9 (it requires >=3.10). Without the cap, poetry tries to
# unify this entry with the kernel's transitive pyarrow>=23.0.1,<24
# across the 3.8–3.10 slice and `poetry lock` fails ("pyarrow is
# forbidden"). The cap removes no installable version — the newest
# pyarrow with a 3.9 wheel is 21.x — it just makes that explicit to
# the solver so the optional [kernel] extra (python>=3.10) can
# coexist. See the kernel dep + [kernel] extra below.
{ version = ">=14.0.1,<23", python = ">=3.8,<3.10", optional=true },
{ version = ">=14.0.1", python = ">=3.10,<3.13", optional=true },
{ version = ">=18.0.0", python = ">=3.13,<3.14", optional=true },
{ version = ">=22.0.0", python = ">=3.14", optional=true }
Expand Down Expand Up @@ -86,15 +95,21 @@ pyarrow = ["pyarrow"]
kernel = ["databricks-sql-kernel"]

[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
pytest = "^9.0.3"
mypy = "^1.10.1"
pylint = ">=2.12.0"
black = "^22.3.0"
black = "^26.3.1"
pytest-dotenv = "^0.5.2"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.0"
# tests/unit/test_parameters.py imports pytz directly. It used to arrive
# transitively via pandas, but pandas 3.0 (resolved on Python >=3.11)
# dropped pytz as a required dependency -- it is now only a pandas extra.
# Declare it explicitly here so the test suite does not depend on pandas's
# transitive graph, which differs across the Python matrix.
pytz = ">=2020.1"
numpy = [
{ version = ">=1.16.6", python = ">=3.8,<3.11" },
{ version = ">=1.16.6", python = ">=3.10,<3.11" },
{ version = ">=1.23.4", python = ">=3.11" },
]

Expand Down
10 changes: 6 additions & 4 deletions src/databricks/sql/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
# TODO : unify all the auth mechanisms with the Python SDK

auth_type = kwargs.get("auth_type")
(client_id, redirect_port_range) = get_client_id_and_redirect_port(
client_id, redirect_port_range = get_client_id_and_redirect_port(
auth_type == AuthType.AZURE_OAUTH.value
)

Expand All @@ -124,9 +124,11 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
azure_client_secret=kwargs.get("azure_client_secret"),
azure_tenant_id=kwargs.get("azure_tenant_id"),
azure_workspace_resource_id=kwargs.get("azure_workspace_resource_id"),
oauth_redirect_port_range=[kwargs["oauth_redirect_port"]]
if kwargs.get("oauth_client_id") and kwargs.get("oauth_redirect_port")
else redirect_port_range,
oauth_redirect_port_range=(
[kwargs["oauth_redirect_port"]]
if kwargs.get("oauth_client_id") and kwargs.get("oauth_redirect_port")
else redirect_port_range
),
oauth_persistence=kwargs.get("experimental_oauth_persistence"),
credentials_provider=kwargs.get("credentials_provider"),
identity_federation_client_id=kwargs.get("identity_federation_client_id"),
Expand Down
14 changes: 6 additions & 8 deletions src/databricks/sql/auth/authenticators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ class CredentialsProvider(abc.ABC):
for authenticating requests to Databricks REST APIs"""

@abc.abstractmethod
def auth_type(self) -> str:
...
def auth_type(self) -> str: ...

@abc.abstractmethod
def __call__(self, *args, **kwargs) -> HeaderFactory:
...
def __call__(self, *args, **kwargs) -> HeaderFactory: ...


# Private API: this is an evolving interface and it will change in the future.
Expand Down Expand Up @@ -109,7 +107,7 @@ def _initial_get_token(self):
if self._access_token and self._refresh_token:
self._update_token_if_expired()
else:
(access_token, refresh_token) = self.oauth_manager.get_tokens(
access_token, refresh_token = self.oauth_manager.get_tokens(
hostname=self._hostname, scope=self._scopes_as_str
)
self._access_token = access_token
Expand Down Expand Up @@ -231,9 +229,9 @@ def header_factory() -> Dict[str, str]:
}

if self.azure_workspace_resource_id:
headers[
self.DATABRICKS_AZURE_WORKSPACE_RESOURCE_ID_HEADER
] = self.azure_workspace_resource_id
headers[self.DATABRICKS_AZURE_WORKSPACE_RESOURCE_ID_HEADER] = (
self.azure_workspace_resource_id
)

return headers

Expand Down
6 changes: 3 additions & 3 deletions src/databricks/sql/auth/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def is_expired(self) -> bool:
exp_time = decoded_token.get("exp")
current_time = time.time()
buffer_time = 30 # 30 seconds buffer
return exp_time and (exp_time - buffer_time) <= current_time
return exp_time is not None and (exp_time - buffer_time) <= current_time
except Exception as e:
logger.error("Failed to decode token: %s", e)
raise e
Expand Down Expand Up @@ -134,7 +134,7 @@ def __get_authorization_code(self, client, auth_url, scope, state, challenge):
try:
with HTTPServer(("", port), handler) as httpd:
redirect_url = OAuthManager.__get_redirect_url(port)
(auth_req_uri, _, _) = client.prepare_authorization_request(
auth_req_uri, _, _ = client.prepare_authorization_request(
authorization_url=auth_url,
redirect_url=redirect_url,
scope=scope,
Expand Down Expand Up @@ -269,7 +269,7 @@ def get_tokens(self, hostname: str, scope=None):
auth_url = self.idp_endpoint.get_authorization_url(hostname)

state = OAuthManager.__token_urlsafe(16)
(verifier, challenge) = OAuthManager.__get_challenge()
verifier, challenge = OAuthManager.__get_challenge()
client = oauthlib.oauth2.WebApplicationClient(self.client_id)

try:
Expand Down
2 changes: 1 addition & 1 deletion src/databricks/sql/auth/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def command_type(self) -> Optional[CommandType]:
return self._command_type

@command_type.setter
def command_type(self, value: CommandType) -> None:
def command_type(self, value: Optional[CommandType]) -> None:
self._command_type = value

@property
Expand Down
8 changes: 5 additions & 3 deletions src/databricks/sql/auth/thrift_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,11 @@ def open(self):
pool_class = HTTPSConnectionPool
_pool_kwargs.update(
{
"cert_reqs": ssl.CERT_REQUIRED
if self._ssl_options.tls_verify
else ssl.CERT_NONE,
"cert_reqs": (
ssl.CERT_REQUIRED
if self._ssl_options.tls_verify
else ssl.CERT_NONE
),
"ca_certs": self._ssl_options.tls_trusted_ca_file,
"cert_file": self._ssl_options.tls_client_cert_file,
"key_file": self._ssl_options.tls_client_cert_key_file,
Expand Down
6 changes: 3 additions & 3 deletions src/databricks/sql/auth/token_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ def add_headers(self, request_headers: Dict[str, str]):
"""Add authentication headers to the request."""

if self._cached_token and not self._cached_token.is_expired():
request_headers[
"Authorization"
] = f"{self._cached_token.token_type} {self._cached_token.access_token}"
request_headers["Authorization"] = (
f"{self._cached_token.token_type} {self._cached_token.access_token}"
)
return

# Get the external headers first to check if we need token federation
Expand Down
6 changes: 2 additions & 4 deletions src/databricks/sql/backend/sea/utils/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,8 @@ def _filter_json_result_set(
if not case_sensitive:
allowed_values = [v.upper() for v in allowed_values]
# Helper lambda to get column value based on case sensitivity
get_column_value = (
lambda row: row[column_index].upper()
if not case_sensitive
else row[column_index]
get_column_value = lambda row: (
row[column_index].upper() if not case_sensitive else row[column_index]
)

# Filter rows based on allowed values
Expand Down
8 changes: 5 additions & 3 deletions src/databricks/sql/backend/sea/utils/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,11 @@ def _open(self):
pool_class = HTTPSConnectionPool
pool_kwargs.update(
{
"cert_reqs": ssl.CERT_REQUIRED
if self.ssl_options.tls_verify
else ssl.CERT_NONE,
"cert_reqs": (
ssl.CERT_REQUIRED
if self.ssl_options.tls_verify
else ssl.CERT_NONE
),
"ca_certs": self.ssl_options.tls_trusted_ca_file,
"cert_file": self.ssl_options.tls_client_cert_file,
"key_file": self.ssl_options.tls_client_cert_key_file,
Expand Down
18 changes: 11 additions & 7 deletions src/databricks/sql/backend/thrift_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from databricks.sql.result_set import ThriftResultSet
from databricks.sql.telemetry.models.event import StatementType


if TYPE_CHECKING:
from databricks.sql.client import Cursor
from databricks.sql.result_set import ResultSet
Expand Down Expand Up @@ -680,7 +679,10 @@ def _create_arrow_table(self, t_row_set, lz4_compressed, schema_bytes, descripti
num_rows,
) = convert_column_based_set_to_arrow_table(t_row_set.columns, description)
elif t_row_set.arrowBatches is not None:
(arrow_table, num_rows,) = convert_arrow_based_set_to_arrow_table(
(
arrow_table,
num_rows,
) = convert_arrow_based_set_to_arrow_table(
t_row_set.arrowBatches, lz4_compressed, schema_bytes
)
else:
Expand Down Expand Up @@ -1053,11 +1055,13 @@ def execute_command(
statement=operation,
runAsync=True,
# For async operation we don't want the direct results
getDirectResults=None
if async_op
else ttypes.TSparkGetDirectResults(
maxRows=max_rows,
maxBytes=max_bytes,
getDirectResults=(
None
if async_op
else ttypes.TSparkGetDirectResults(
maxRows=max_rows,
maxBytes=max_bytes,
)
),
canReadArrowResult=True if pyarrow else False,
canDecompressLZ4Result=lz4_compression,
Expand Down
14 changes: 8 additions & 6 deletions src/databricks/sql/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@ def read(self) -> Optional[OAuthToken]:
http_path=http_path,
port=kwargs.get("_port", 443),
client_context=client_context,
user_agent=self.session.useragent_header
if hasattr(self, "session")
else None,
user_agent=(
self.session.useragent_header if hasattr(self, "session") else None
),
enable_telemetry=enable_telemetry,
)
raise e
Expand Down Expand Up @@ -390,9 +390,11 @@ def read(self) -> Optional[OAuthToken]:

driver_connection_params = DriverConnectionParameters(
http_path=http_path,
mode=DatabricksClientType.SEA
if self.session.use_sea
else DatabricksClientType.THRIFT,
mode=(
DatabricksClientType.SEA
if self.session.use_sea
else DatabricksClientType.THRIFT
),
host_info=HostDetails(host_url=server_hostname, port=self.session.port),
auth_mech=TelemetryHelper.get_auth_mechanism(self.session.auth_provider),
auth_flow=TelemetryHelper.get_auth_flow(self.session.auth_provider),
Expand Down
12 changes: 7 additions & 5 deletions src/databricks/sql/common/unified_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ def _setup_pool_managers(self):
"num_pools": self.config.pool_connections,
"maxsize": self.config.pool_maxsize,
"retries": self._retry_policy,
"timeout": urllib3.Timeout(
connect=self.config.socket_timeout, read=self.config.socket_timeout
)
if self.config.socket_timeout
else None,
"timeout": (
urllib3.Timeout(
connect=self.config.socket_timeout, read=self.config.socket_timeout
)
if self.config.socket_timeout
else None
),
"ssl_context": ssl_context,
}

Expand Down
Loading
Loading