databricks-sql-connector 4.1.3__tar.gz → 4.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/CHANGELOG.md +5 -0
  2. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/PKG-INFO +1 -1
  3. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/pyproject.toml +1 -1
  4. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/__init__.py +1 -1
  5. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/auth.py +22 -6
  6. databricks_sql_connector-4.1.4/src/databricks/sql/auth/auth_utils.py +64 -0
  7. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/common.py +2 -0
  8. databricks_sql_connector-4.1.4/src/databricks/sql/auth/token_federation.py +206 -0
  9. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/client.py +14 -0
  10. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/LICENSE +0 -0
  11. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/README.md +0 -0
  12. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/__init__.py +0 -0
  13. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/__init__.py +0 -0
  14. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/authenticators.py +0 -0
  15. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/endpoint.py +0 -0
  16. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/oauth.py +0 -0
  17. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/oauth_http_handler.py +0 -0
  18. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/retry.py +0 -0
  19. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/auth/thrift_http_client.py +0 -0
  20. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/databricks_client.py +0 -0
  21. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/backend.py +0 -0
  22. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/models/__init__.py +0 -0
  23. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/models/base.py +0 -0
  24. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/models/requests.py +0 -0
  25. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/models/responses.py +0 -0
  26. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/queue.py +0 -0
  27. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/result_set.py +0 -0
  28. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/utils/constants.py +0 -0
  29. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/utils/conversion.py +0 -0
  30. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/utils/filters.py +0 -0
  31. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/utils/http_client.py +0 -0
  32. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/sea/utils/normalize.py +0 -0
  33. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/thrift_backend.py +0 -0
  34. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/types.py +0 -0
  35. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/utils/__init__.py +0 -0
  36. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/backend/utils/guid_utils.py +0 -0
  37. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/cloudfetch/download_manager.py +0 -0
  38. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/cloudfetch/downloader.py +0 -0
  39. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/common/feature_flag.py +0 -0
  40. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/common/http.py +0 -0
  41. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/common/http_utils.py +0 -0
  42. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/common/unified_http_client.py +0 -0
  43. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/exc.py +0 -0
  44. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/experimental/__init__.py +0 -0
  45. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/experimental/oauth_persistence.py +0 -0
  46. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/parameters/__init__.py +0 -0
  47. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/parameters/native.py +0 -0
  48. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/parameters/py.typed +0 -0
  49. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/py.typed +0 -0
  50. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/result_set.py +0 -0
  51. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/session.py +0 -0
  52. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/latency_logger.py +0 -0
  53. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/models/endpoint_models.py +0 -0
  54. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/models/enums.py +0 -0
  55. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/models/event.py +0 -0
  56. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/models/frontend_logs.py +0 -0
  57. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/telemetry_client.py +0 -0
  58. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/telemetry/utils.py +0 -0
  59. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/TCLIService/TCLIService-remote +0 -0
  60. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/TCLIService/TCLIService.py +0 -0
  61. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/TCLIService/__init__.py +0 -0
  62. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/TCLIService/constants.py +0 -0
  63. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/TCLIService/ttypes.py +0 -0
  64. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/thrift_api/__init__.py +0 -0
  65. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/types.py +0 -0
  66. {databricks_sql_connector-4.1.3 → databricks_sql_connector-4.1.4}/src/databricks/sql/utils.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Release History
2
2
 
3
+ # 4.1.4 (2025-10-15)
4
+ - Add support for Token Federation (databricks/databricks-sql-python#691 by @madhav-db)
5
+ - Add metric view support (databricks/databricks-sql-python#688 by @shivam2680)
6
+ - Increased time limit for long running queries (databricks/databricks-sql-python#686 by @jprakash-db)
7
+
3
8
  # 4.1.3 (2025-09-17)
4
9
  - Query tags integration (databricks/databricks-sql-python#663 by @sreekanth-db)
5
10
  - Add variant support (databricks/databricks-sql-python#560 by @shivam2680)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-sql-connector
3
- Version: 4.1.3
3
+ Version: 4.1.4
4
4
  Summary: Databricks SQL Connector for Python
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "databricks-sql-connector"
3
- version = "4.1.3"
3
+ version = "4.1.4"
4
4
  description = "Databricks SQL Connector for Python"
5
5
  authors = ["Databricks <databricks-sql-connector-maintainers@databricks.com>"]
6
6
  license = "Apache-2.0"
@@ -68,7 +68,7 @@ DATETIME = DBAPITypeObject("timestamp")
68
68
  DATE = DBAPITypeObject("date")
69
69
  ROWID = DBAPITypeObject()
70
70
 
71
- __version__ = "4.1.3"
71
+ __version__ = "4.1.4"
72
72
  USER_AGENT_NAME = "PyDatabricksSqlConnector"
73
73
 
74
74
  # These two functions are pyhive legacy
@@ -8,13 +8,17 @@ from databricks.sql.auth.authenticators import (
8
8
  AzureServicePrincipalCredentialProvider,
9
9
  )
10
10
  from databricks.sql.auth.common import AuthType, ClientContext
11
+ from databricks.sql.auth.token_federation import TokenFederationProvider
11
12
 
12
13
 
13
14
  def get_auth_provider(cfg: ClientContext, http_client):
15
+ # Determine the base auth provider
16
+ base_provider: Optional[AuthProvider] = None
17
+
14
18
  if cfg.credentials_provider:
15
- return ExternalAuthProvider(cfg.credentials_provider)
19
+ base_provider = ExternalAuthProvider(cfg.credentials_provider)
16
20
  elif cfg.auth_type == AuthType.AZURE_SP_M2M.value:
17
- return ExternalAuthProvider(
21
+ base_provider = ExternalAuthProvider(
18
22
  AzureServicePrincipalCredentialProvider(
19
23
  cfg.hostname,
20
24
  cfg.azure_client_id,
@@ -29,7 +33,7 @@ def get_auth_provider(cfg: ClientContext, http_client):
29
33
  assert cfg.oauth_client_id is not None
30
34
  assert cfg.oauth_scopes is not None
31
35
 
32
- return DatabricksOAuthProvider(
36
+ base_provider = DatabricksOAuthProvider(
33
37
  cfg.hostname,
34
38
  cfg.oauth_persistence,
35
39
  cfg.oauth_redirect_port_range,
@@ -39,17 +43,17 @@ def get_auth_provider(cfg: ClientContext, http_client):
39
43
  cfg.auth_type,
40
44
  )
41
45
  elif cfg.access_token is not None:
42
- return AccessTokenAuthProvider(cfg.access_token)
46
+ base_provider = AccessTokenAuthProvider(cfg.access_token)
43
47
  elif cfg.use_cert_as_auth and cfg.tls_client_cert_file:
44
48
  # no op authenticator. authentication is performed using ssl certificate outside of headers
45
- return AuthProvider()
49
+ base_provider = AuthProvider()
46
50
  else:
47
51
  if (
48
52
  cfg.oauth_redirect_port_range is not None
49
53
  and cfg.oauth_client_id is not None
50
54
  and cfg.oauth_scopes is not None
51
55
  ):
52
- return DatabricksOAuthProvider(
56
+ base_provider = DatabricksOAuthProvider(
53
57
  cfg.hostname,
54
58
  cfg.oauth_persistence,
55
59
  cfg.oauth_redirect_port_range,
@@ -61,6 +65,17 @@ def get_auth_provider(cfg: ClientContext, http_client):
61
65
  else:
62
66
  raise RuntimeError("No valid authentication settings!")
63
67
 
68
+ # Always wrap with token federation (falls back gracefully if not needed)
69
+ if base_provider:
70
+ return TokenFederationProvider(
71
+ hostname=cfg.hostname,
72
+ external_provider=base_provider,
73
+ http_client=http_client,
74
+ identity_federation_client_id=cfg.identity_federation_client_id,
75
+ )
76
+
77
+ return base_provider
78
+
64
79
 
65
80
  PYSQL_OAUTH_SCOPES = ["sql", "offline_access"]
66
81
  PYSQL_OAUTH_CLIENT_ID = "databricks-sql-python"
@@ -114,5 +129,6 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
114
129
  else redirect_port_range,
115
130
  oauth_persistence=kwargs.get("experimental_oauth_persistence"),
116
131
  credentials_provider=kwargs.get("credentials_provider"),
132
+ identity_federation_client_id=kwargs.get("identity_federation_client_id"),
117
133
  )
118
134
  return get_auth_provider(cfg, http_client)
@@ -0,0 +1,64 @@
1
+ import logging
2
+ import jwt
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional, Dict, Tuple
5
+ from urllib.parse import urlparse
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def parse_hostname(hostname: str) -> str:
11
+ """
12
+ Normalize the hostname to include scheme and trailing slash.
13
+
14
+ Args:
15
+ hostname: The hostname to normalize
16
+
17
+ Returns:
18
+ Normalized hostname with scheme and trailing slash
19
+ """
20
+ if not hostname.startswith("http://") and not hostname.startswith("https://"):
21
+ hostname = f"https://{hostname}"
22
+ if not hostname.endswith("/"):
23
+ hostname = f"{hostname}/"
24
+ return hostname
25
+
26
+
27
+ def decode_token(access_token: str) -> Optional[Dict]:
28
+ """
29
+ Decode a JWT token without verification to extract claims.
30
+
31
+ Args:
32
+ access_token: The JWT access token to decode
33
+
34
+ Returns:
35
+ Decoded token claims or None if decoding fails
36
+ """
37
+ try:
38
+ return jwt.decode(access_token, options={"verify_signature": False})
39
+ except Exception as e:
40
+ logger.debug("Failed to decode JWT token: %s", e)
41
+ return None
42
+
43
+
44
+ def is_same_host(url1: str, url2: str) -> bool:
45
+ """
46
+ Check if two URLs have the same host.
47
+
48
+ Args:
49
+ url1: First URL
50
+ url2: Second URL
51
+
52
+ Returns:
53
+ True if hosts are the same, False otherwise
54
+ """
55
+ try:
56
+ host1 = urlparse(url1).netloc
57
+ host2 = urlparse(url2).netloc
58
+ # Handle port differences (e.g., example.com vs example.com:443)
59
+ host1_without_port = host1.split(":")[0]
60
+ host2_without_port = host2.split(":")[0]
61
+ return host1_without_port == host2_without_port
62
+ except Exception as e:
63
+ logger.debug("Failed to parse URLs: %s", e)
64
+ return False
@@ -37,6 +37,7 @@ class ClientContext:
37
37
  tls_client_cert_file: Optional[str] = None,
38
38
  oauth_persistence=None,
39
39
  credentials_provider=None,
40
+ identity_federation_client_id: Optional[str] = None,
40
41
  # HTTP client configuration parameters
41
42
  ssl_options=None, # SSLOptions type
42
43
  socket_timeout: Optional[float] = None,
@@ -65,6 +66,7 @@ class ClientContext:
65
66
  self.tls_client_cert_file = tls_client_cert_file
66
67
  self.oauth_persistence = oauth_persistence
67
68
  self.credentials_provider = credentials_provider
69
+ self.identity_federation_client_id = identity_federation_client_id
68
70
 
69
71
  # HTTP client configuration
70
72
  self.ssl_options = ssl_options
@@ -0,0 +1,206 @@
1
+ import logging
2
+ import json
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional, Dict, Tuple
5
+ from urllib.parse import urlencode
6
+
7
+ from databricks.sql.auth.authenticators import AuthProvider
8
+ from databricks.sql.auth.auth_utils import (
9
+ parse_hostname,
10
+ decode_token,
11
+ is_same_host,
12
+ )
13
+ from databricks.sql.common.http import HttpMethod
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Token:
19
+ """
20
+ Represents an OAuth token with expiration management.
21
+ """
22
+
23
+ def __init__(self, access_token: str, token_type: str = "Bearer"):
24
+ """
25
+ Initialize a token.
26
+
27
+ Args:
28
+ access_token: The access token string
29
+ token_type: The token type (default: Bearer)
30
+ """
31
+ self.access_token = access_token
32
+ self.token_type = token_type
33
+ self.expiry_time = self._calculate_expiry()
34
+
35
+ def _calculate_expiry(self) -> datetime:
36
+ """
37
+ Calculate the token expiry time from JWT claims.
38
+
39
+ Returns:
40
+ The token expiry datetime
41
+ """
42
+ decoded = decode_token(self.access_token)
43
+ if decoded and "exp" in decoded:
44
+ # Use JWT exp claim with 1 minute buffer
45
+ return datetime.fromtimestamp(decoded["exp"]) - timedelta(minutes=1)
46
+ # Default to 1 hour if no expiry info
47
+ return datetime.now() + timedelta(hours=1)
48
+
49
+ def is_expired(self) -> bool:
50
+ """
51
+ Check if the token is expired.
52
+
53
+ Returns:
54
+ True if token is expired, False otherwise
55
+ """
56
+ return datetime.now() >= self.expiry_time
57
+
58
+ def to_dict(self) -> Dict[str, str]:
59
+ """
60
+ Convert token to dictionary format.
61
+
62
+ Returns:
63
+ Dictionary with access_token and token_type
64
+ """
65
+ return {
66
+ "access_token": self.access_token,
67
+ "token_type": self.token_type,
68
+ }
69
+
70
+
71
+ class TokenFederationProvider(AuthProvider):
72
+ """
73
+ Implementation of Token Federation for Databricks SQL Python driver.
74
+
75
+ This provider exchanges third-party access tokens for Databricks in-house tokens
76
+ when the token issuer is different from the Databricks host.
77
+ """
78
+
79
+ TOKEN_EXCHANGE_ENDPOINT = "/oidc/v1/token"
80
+ TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
81
+ TOKEN_EXCHANGE_SUBJECT_TYPE = "urn:ietf:params:oauth:token-type:jwt"
82
+
83
+ def __init__(
84
+ self,
85
+ hostname: str,
86
+ external_provider: AuthProvider,
87
+ http_client,
88
+ identity_federation_client_id: Optional[str] = None,
89
+ ):
90
+ """
91
+ Initialize the Token Federation Provider.
92
+
93
+ Args:
94
+ hostname: The Databricks workspace hostname
95
+ external_provider: The external authentication provider
96
+ http_client: HTTP client for making requests (required)
97
+ identity_federation_client_id: Optional client ID for token federation
98
+ """
99
+ if not http_client:
100
+ raise ValueError("http_client is required for TokenFederationProvider")
101
+
102
+ self.hostname = parse_hostname(hostname)
103
+ self.external_provider = external_provider
104
+ self.http_client = http_client
105
+ self.identity_federation_client_id = identity_federation_client_id
106
+
107
+ self._cached_token: Optional[Token] = None
108
+ self._external_headers: Dict[str, str] = {}
109
+
110
+ def add_headers(self, request_headers: Dict[str, str]):
111
+ """Add authentication headers to the request."""
112
+
113
+ if self._cached_token and not self._cached_token.is_expired():
114
+ request_headers[
115
+ "Authorization"
116
+ ] = f"{self._cached_token.token_type} {self._cached_token.access_token}"
117
+ return
118
+
119
+ # Get the external headers first to check if we need token federation
120
+ self._external_headers = {}
121
+ self.external_provider.add_headers(self._external_headers)
122
+
123
+ # If no Authorization header from external provider, pass through all headers
124
+ if "Authorization" not in self._external_headers:
125
+ request_headers.update(self._external_headers)
126
+ return
127
+
128
+ token = self._get_token()
129
+ request_headers["Authorization"] = f"{token.token_type} {token.access_token}"
130
+
131
+ def _get_token(self) -> Token:
132
+ """Get or refresh the authentication token."""
133
+ # Check if cached token is still valid
134
+ if self._cached_token and not self._cached_token.is_expired():
135
+ return self._cached_token
136
+
137
+ # Extract token from already-fetched headers
138
+ auth_header = self._external_headers.get("Authorization", "")
139
+ token_type, access_token = self._extract_token_from_header(auth_header)
140
+
141
+ # Check if token exchange is needed
142
+ if self._should_exchange_token(access_token):
143
+ try:
144
+ token = self._exchange_token(access_token)
145
+ self._cached_token = token
146
+ return token
147
+ except Exception as e:
148
+ logger.warning("Token exchange failed, using external token: %s", e)
149
+
150
+ # Use external token directly
151
+ token = Token(access_token, token_type)
152
+ self._cached_token = token
153
+ return token
154
+
155
+ def _should_exchange_token(self, access_token: str) -> bool:
156
+ """Check if the token should be exchanged based on issuer."""
157
+ decoded = decode_token(access_token)
158
+ if not decoded:
159
+ return False
160
+
161
+ issuer = decoded.get("iss", "")
162
+ # Check if issuer host is different from Databricks host
163
+ return not is_same_host(issuer, self.hostname)
164
+
165
+ def _exchange_token(self, access_token: str) -> Token:
166
+ """Exchange the external token for a Databricks token."""
167
+ token_url = f"{self.hostname.rstrip('/')}{self.TOKEN_EXCHANGE_ENDPOINT}"
168
+
169
+ data = {
170
+ "grant_type": self.TOKEN_EXCHANGE_GRANT_TYPE,
171
+ "subject_token": access_token,
172
+ "subject_token_type": self.TOKEN_EXCHANGE_SUBJECT_TYPE,
173
+ "scope": "sql",
174
+ "return_original_token_if_authenticated": "true",
175
+ }
176
+
177
+ if self.identity_federation_client_id:
178
+ data["client_id"] = self.identity_federation_client_id
179
+
180
+ headers = {
181
+ "Content-Type": "application/x-www-form-urlencoded",
182
+ "Accept": "*/*",
183
+ }
184
+
185
+ body = urlencode(data)
186
+
187
+ response = self.http_client.request(
188
+ HttpMethod.POST, url=token_url, body=body, headers=headers
189
+ )
190
+
191
+ token_response = json.loads(response.data.decode())
192
+
193
+ return Token(
194
+ token_response["access_token"], token_response.get("token_type", "Bearer")
195
+ )
196
+
197
+ def _extract_token_from_header(self, auth_header: str) -> Tuple[str, str]:
198
+ """Extract token type and access token from Authorization header."""
199
+ if not auth_header:
200
+ raise ValueError("Authorization header is missing")
201
+
202
+ parts = auth_header.split(" ", 1)
203
+ if len(parts) != 2:
204
+ raise ValueError("Invalid Authorization header format")
205
+
206
+ return parts[0], parts[1]
@@ -200,6 +200,12 @@ class Connection:
200
200
  STRUCT is returned as Dict[str, Any]
201
201
  ARRAY is returned as numpy.ndarray
202
202
  When False, complex types are returned as a strings. These are generally deserializable as JSON.
203
+ :param enable_metric_view_metadata: `bool`, optional (default is False)
204
+ When True, enables metric view metadata support by setting the
205
+ spark.sql.thriftserver.metadata.metricview.enabled session configuration.
206
+ This allows
207
+ 1. cursor.tables() to return METRIC_VIEW table type
208
+ 2. cursor.columns() to return "measure" column type
203
209
  """
204
210
 
205
211
  # Internal arguments in **kwargs:
@@ -248,6 +254,14 @@ class Connection:
248
254
  access_token_kv = {"access_token": access_token}
249
255
  kwargs = {**kwargs, **access_token_kv}
250
256
 
257
+ enable_metric_view_metadata = kwargs.get("enable_metric_view_metadata", False)
258
+ if enable_metric_view_metadata:
259
+ if session_configuration is None:
260
+ session_configuration = {}
261
+ session_configuration[
262
+ "spark.sql.thriftserver.metadata.metricview.enabled"
263
+ ] = "true"
264
+
251
265
  self.disable_pandas = kwargs.get("_disable_pandas", False)
252
266
  self.lz4_compression = kwargs.get("enable_query_result_lz4_compression", True)
253
267
  self.use_cloud_fetch = kwargs.get("use_cloud_fetch", True)