databricks-sql-connector 3.0.3__tar.gz → 3.1.0__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 (58) hide show
  1. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/CHANGELOG.md +27 -6
  2. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/PKG-INFO +1 -1
  3. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/pyproject.toml +2 -1
  4. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/__init__.py +1 -1
  5. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/auth.py +10 -7
  6. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/authenticators.py +4 -6
  7. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/endpoint.py +30 -7
  8. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/retry.py +4 -2
  9. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/client.py +11 -7
  10. databricks_sql_connector-3.1.0/src/databricks/sqlalchemy/test_local/conftest.py +44 -0
  11. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/e2e/test_basic.py +26 -27
  12. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/test_parsing.py +4 -4
  13. databricks_sql_connector-3.0.3/src/databricks/sqlalchemy/pytest.ini +0 -3
  14. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/LICENSE +0 -0
  15. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/README.md +0 -0
  16. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/__init__.py +0 -0
  17. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/__init__.py +0 -0
  18. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/oauth.py +0 -0
  19. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/oauth_http_handler.py +0 -0
  20. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/auth/thrift_http_client.py +0 -0
  21. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/cloudfetch/download_manager.py +0 -0
  22. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/cloudfetch/downloader.py +0 -0
  23. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/exc.py +0 -0
  24. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/experimental/__init__.py +0 -0
  25. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/experimental/oauth_persistence.py +0 -0
  26. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/parameters/__init__.py +0 -0
  27. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/parameters/native.py +0 -0
  28. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/parameters/py.typed +0 -0
  29. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/TCLIService/TCLIService-remote +0 -0
  30. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/TCLIService/TCLIService.py +0 -0
  31. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/TCLIService/__init__.py +0 -0
  32. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/TCLIService/constants.py +0 -0
  33. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/TCLIService/ttypes.py +0 -0
  34. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_api/__init__.py +0 -0
  35. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/thrift_backend.py +0 -0
  36. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/types.py +0 -0
  37. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sql/utils.py +0 -0
  38. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/README.sqlalchemy.md +0 -0
  39. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/README.tests.md +0 -0
  40. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/__init__.py +0 -0
  41. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/_ddl.py +0 -0
  42. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/_parse.py +0 -0
  43. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/_types.py +0 -0
  44. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/base.py +0 -0
  45. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/requirements.py +0 -0
  46. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/setup.cfg +0 -0
  47. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/_extra.py +0 -0
  48. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/_future.py +0 -0
  49. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/_regression.py +0 -0
  50. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/_unsupported.py +0 -0
  51. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/conftest.py +0 -0
  52. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/overrides/_componentreflectiontest.py +0 -0
  53. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/overrides/_ctetest.py +0 -0
  54. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test/test_suite.py +0 -0
  55. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/__init__.py +0 -0
  56. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/e2e/MOCK_DATA.xlsx +0 -0
  57. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/test_ddl.py +0 -0
  58. {databricks_sql_connector-3.0.3 → databricks_sql_connector-3.1.0}/src/databricks/sqlalchemy/test_local/test_types.py +0 -0
@@ -1,5 +1,11 @@
1
1
  # Release History
2
2
 
3
+ # 3.1.0 (2024-02-16)
4
+
5
+ - Revert retry-after behavior to be exponential backoff (#349)
6
+ - Support Databricks OAuth on Azure (#351)
7
+ - Support Databricks OAuth on GCP (#338)
8
+
3
9
  # 3.0.3 (2024-02-02)
4
10
 
5
11
  - Revised docstrings and examples for OAuth (#339)
@@ -49,7 +55,7 @@
49
55
 
50
56
  ## 2.9.2 (2023-08-17)
51
57
 
52
- __Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed. The log changes are incorporated into version 2.9.3 and greater.__
58
+ **Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed. The log changes are incorporated into version 2.9.3 and greater.**
53
59
 
54
60
  - Other: Add `examples/v3_retries_query_execute.py` (#199)
55
61
  - Other: suppress log message when `_enable_v3_retries` is not `True` (#199)
@@ -57,7 +63,7 @@ __Note: this release was yanked from Pypi on 13 September 2023 due to compatibil
57
63
 
58
64
  ## 2.9.1 (2023-08-11)
59
65
 
60
- __Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed.__
66
+ **Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed.**
61
67
 
62
68
  - Other: Explicitly pin urllib3 to ^2.0.0 (#191)
63
69
 
@@ -110,6 +116,7 @@ __Note: this release was yanked from Pypi on 13 September 2023 due to compatibil
110
116
  - Other: Relax sqlalchemy required version as it was unecessarily strict.
111
117
 
112
118
  ## 2.5.0 (2023-04-14)
119
+
113
120
  - Add support for External Auth providers
114
121
  - Fix: Python HTTP proxies were broken
115
122
  - Other: All Thrift requests that timeout during connection will be automatically retried
@@ -131,8 +138,8 @@ __Note: this release was yanked from Pypi on 13 September 2023 due to compatibil
131
138
 
132
139
  ## 2.2.2 (2023-01-03)
133
140
 
134
- - Support custom oauth client id and redirect port
135
- - Fix: Add none check on _oauth_persistence in DatabricksOAuthProvider
141
+ - Support custom oauth client id and redirect port
142
+ - Fix: Add none check on \_oauth_persistence in DatabricksOAuthProvider
136
143
 
137
144
  ## 2.2.1 (2022-11-29)
138
145
 
@@ -164,57 +171,71 @@ Huge thanks to @dbaxa for contributing this change!
164
171
 
165
172
  - Add retry logic for `GetOperationStatus` requests that fail with an `OSError`
166
173
  - Reorganised code to use Poetry for dependency management.
174
+
167
175
  ## 2.0.2 (2022-05-04)
176
+
168
177
  - Better exception handling in automatic connection close
169
178
 
170
179
  ## 2.0.1 (2022-04-21)
180
+
171
181
  - Fixed Pandas dependency in setup.cfg to be >= 1.2.0
172
182
 
173
183
  ## 2.0.0 (2022-04-19)
184
+
174
185
  - Initial stable release of V2
175
- - Added better support for complex types, so that in Databricks runtime 10.3+, Arrays, Maps and Structs will get
186
+ - Added better support for complex types, so that in Databricks runtime 10.3+, Arrays, Maps and Structs will get
176
187
  deserialized as lists, lists of tuples and dicts, respectively.
177
188
  - Changed the name of the metadata arg to http_headers
178
189
 
179
190
  ## 2.0.b2 (2022-04-04)
191
+
180
192
  - Change import of collections.Iterable to collections.abc.Iterable to make the library compatible with Python 3.10
181
193
  - Fixed bug with .tables method so that .tables works as expected with Unity-Catalog enabled endpoints
182
194
 
183
195
  ## 2.0.0b1 (2022-03-04)
196
+
184
197
  - Fix packaging issue (dependencies were not being installed properly)
185
198
  - Fetching timestamp results will now return aware instead of naive timestamps
186
199
  - The client will now default to using simplified error messages
187
200
 
188
201
  ## 2.0.0b (2022-02-08)
202
+
189
203
  - Initial beta release of V2. V2 is an internal re-write of large parts of the connector to use Databricks edge features. All public APIs from V1 remain.
190
- - Added Unity Catalog support (pass catalog and / or schema key word args to the .connect method to select initial schema and catalog)
204
+ - Added Unity Catalog support (pass catalog and / or schema key word args to the .connect method to select initial schema and catalog)
191
205
 
192
206
  ---
193
207
 
194
208
  **Note**: The code for versions prior to `v2.0.0b` is not contained in this repository. The below entries are included for reference only.
195
209
 
196
210
  ---
211
+
197
212
  ## 1.0.0 (2022-01-20)
213
+
198
214
  - Add operations for retrieving metadata
199
215
  - Add the ability to access columns by name on result rows
200
216
  - Add the ability to provide configuration settings on connect
201
217
 
202
218
  ## 0.9.4 (2022-01-10)
219
+
203
220
  - Improved logging and error messages.
204
221
 
205
222
  ## 0.9.3 (2021-12-08)
223
+
206
224
  - Add retries for 429 and 503 HTTP responses.
207
225
 
208
226
  ## 0.9.2 (2021-12-02)
227
+
209
228
  - (Bug fix) Increased Thrift requirement from 0.10.0 to 0.13.0 as 0.10.0 was in fact incompatible
210
229
  - (Bug fix) Fixed error message after query execution failed -SQLSTATE and Error message were misplaced
211
230
 
212
231
  ## 0.9.1 (2021-09-01)
232
+
213
233
  - Public Preview release, Experimental tag removed
214
234
  - minor updates in internal build/packaging
215
235
  - no functional changes
216
236
 
217
237
  ## 0.9.0 (2021-08-04)
238
+
218
239
  - initial (Experimental) release of pyhive-forked connector
219
240
  - Python DBAPI 2.0 (PEP-0249), thrift based
220
241
  - see docs for more info: https://docs.databricks.com/dev-tools/python-sql-connector.html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: databricks-sql-connector
3
- Version: 3.0.3
3
+ Version: 3.1.0
4
4
  Summary: Databricks SQL Connector for Python
5
5
  License: Apache-2.0
6
6
  Author: Databricks
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "databricks-sql-connector"
3
- version = "3.0.3"
3
+ version = "3.1.0"
4
4
  description = "Databricks SQL Connector for Python"
5
5
  authors = ["Databricks <databricks-sql-connector-maintainers@databricks.com>"]
6
6
  license = "Apache-2.0"
@@ -58,6 +58,7 @@ exclude = ['ttypes\.py$', 'TCLIService\.py$']
58
58
  exclude = '/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist|thrift_api)/'
59
59
 
60
60
  [tool.pytest.ini_options]
61
+ markers = {"reviewed" = "Test case has been reviewed by Databricks"}
61
62
  minversion = "6.0"
62
63
  log_cli = "false"
63
64
  log_cli_level = "INFO"
@@ -62,7 +62,7 @@ DATETIME = DBAPITypeObject("timestamp")
62
62
  DATE = DBAPITypeObject("date")
63
63
  ROWID = DBAPITypeObject()
64
64
 
65
- __version__ = "3.0.3"
65
+ __version__ = "3.1.0"
66
66
  USER_AGENT_NAME = "PyDatabricksSqlConnector"
67
67
 
68
68
  # These two functions are pyhive legacy
@@ -8,12 +8,11 @@ from databricks.sql.auth.authenticators import (
8
8
  ExternalAuthProvider,
9
9
  DatabricksOAuthProvider,
10
10
  )
11
- from databricks.sql.auth.endpoint import infer_cloud_from_host, CloudType
12
- from databricks.sql.experimental.oauth_persistence import OAuthPersistence
13
11
 
14
12
 
15
13
  class AuthType(Enum):
16
14
  DATABRICKS_OAUTH = "databricks-oauth"
15
+ AZURE_OAUTH = "azure-oauth"
17
16
  # other supported types (access_token, user/pass) can be inferred
18
17
  # we can add more types as needed later
19
18
 
@@ -51,7 +50,7 @@ class ClientContext:
51
50
  def get_auth_provider(cfg: ClientContext):
52
51
  if cfg.credentials_provider:
53
52
  return ExternalAuthProvider(cfg.credentials_provider)
54
- if cfg.auth_type == AuthType.DATABRICKS_OAUTH.value:
53
+ if cfg.auth_type in [AuthType.DATABRICKS_OAUTH.value, AuthType.AZURE_OAUTH.value]:
55
54
  assert cfg.oauth_redirect_port_range is not None
56
55
  assert cfg.oauth_client_id is not None
57
56
  assert cfg.oauth_scopes is not None
@@ -62,6 +61,7 @@ def get_auth_provider(cfg: ClientContext):
62
61
  cfg.oauth_redirect_port_range,
63
62
  cfg.oauth_client_id,
64
63
  cfg.oauth_scopes,
64
+ cfg.auth_type,
65
65
  )
66
66
  elif cfg.access_token is not None:
67
67
  return AccessTokenAuthProvider(cfg.access_token)
@@ -87,19 +87,22 @@ def normalize_host_name(hostname: str):
87
87
  return f"{maybe_scheme}{hostname}{maybe_trailing_slash}"
88
88
 
89
89
 
90
- def get_client_id_and_redirect_port(hostname: str):
90
+ def get_client_id_and_redirect_port(use_azure_auth: bool):
91
91
  return (
92
92
  (PYSQL_OAUTH_CLIENT_ID, PYSQL_OAUTH_REDIRECT_PORT_RANGE)
93
- if infer_cloud_from_host(hostname) == CloudType.AWS
93
+ if not use_azure_auth
94
94
  else (PYSQL_OAUTH_AZURE_CLIENT_ID, PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE)
95
95
  )
96
96
 
97
97
 
98
98
  def get_python_sql_connector_auth_provider(hostname: str, **kwargs):
99
- (client_id, redirect_port_range) = get_client_id_and_redirect_port(hostname)
99
+ auth_type = kwargs.get("auth_type")
100
+ (client_id, redirect_port_range) = get_client_id_and_redirect_port(
101
+ auth_type == AuthType.AZURE_OAUTH.value
102
+ )
100
103
  cfg = ClientContext(
101
104
  hostname=normalize_host_name(hostname),
102
- auth_type=kwargs.get("auth_type"),
105
+ auth_type=auth_type,
103
106
  access_token=kwargs.get("access_token"),
104
107
  username=kwargs.get("_username"),
105
108
  password=kwargs.get("_password"),
@@ -18,6 +18,7 @@ class AuthProvider:
18
18
 
19
19
  HeaderFactory = Callable[[], Dict[str, str]]
20
20
 
21
+
21
22
  # In order to keep compatibility with SDK
22
23
  class CredentialsProvider(abc.ABC):
23
24
  """CredentialsProvider is the protocol (call-side interface)
@@ -69,16 +70,13 @@ class DatabricksOAuthProvider(AuthProvider):
69
70
  redirect_port_range: List[int],
70
71
  client_id: str,
71
72
  scopes: List[str],
73
+ auth_type: str = "databricks-oauth",
72
74
  ):
73
75
  try:
74
- cloud_type = infer_cloud_from_host(hostname)
75
- if not cloud_type:
76
- raise NotImplementedError("Cannot infer the cloud type from hostname")
77
-
78
- idp_endpoint = get_oauth_endpoints(cloud_type)
76
+ idp_endpoint = get_oauth_endpoints(hostname, auth_type == "azure-oauth")
79
77
  if not idp_endpoint:
80
78
  raise NotImplementedError(
81
- f"OAuth is not supported for cloud ${cloud_type.value}"
79
+ f"OAuth is not supported for host ${hostname}"
82
80
  )
83
81
 
84
82
  # Convert to the corresponding scopes in the corresponding IdP
@@ -1,9 +1,9 @@
1
1
  #
2
2
  # It implements all the cloud specific OAuth configuration/metadata
3
3
  #
4
- # Azure: It uses AAD
4
+ # Azure: It uses Databricks internal IdP or Azure AD
5
5
  # AWS: It uses Databricks internal IdP
6
- # GCP: Not support yet
6
+ # GCP: It uses Databricks internal IdP
7
7
  #
8
8
  from abc import ABC, abstractmethod
9
9
  from enum import Enum
@@ -21,6 +21,7 @@ class OAuthScope:
21
21
  class CloudType(Enum):
22
22
  AWS = "aws"
23
23
  AZURE = "azure"
24
+ GCP = "gcp"
24
25
 
25
26
 
26
27
  DATABRICKS_AWS_DOMAINS = [
@@ -34,6 +35,10 @@ DATABRICKS_AZURE_DOMAINS = [
34
35
  ".databricks.azure.cn",
35
36
  ".databricks.azure.us",
36
37
  ]
38
+ DATABRICKS_GCP_DOMAINS = [".gcp.databricks.com"]
39
+
40
+ # Domain supported by Databricks InHouse OAuth
41
+ DATABRICKS_OAUTH_AZURE_DOMAINS = [".azuredatabricks.net"]
37
42
 
38
43
 
39
44
  # Infer cloud type from Databricks SQL instance hostname
@@ -45,10 +50,20 @@ def infer_cloud_from_host(hostname: str) -> Optional[CloudType]:
45
50
  return CloudType.AZURE
46
51
  elif any(e for e in DATABRICKS_AWS_DOMAINS if host.endswith(e)):
47
52
  return CloudType.AWS
53
+ elif any(e for e in DATABRICKS_GCP_DOMAINS if host.endswith(e)):
54
+ return CloudType.GCP
48
55
  else:
49
56
  return None
50
57
 
51
58
 
59
+ def is_supported_databricks_oauth_host(hostname: str) -> bool:
60
+ host = hostname.lower().replace("https://", "").split("/")[0]
61
+ domains = (
62
+ DATABRICKS_AWS_DOMAINS + DATABRICKS_GCP_DOMAINS + DATABRICKS_OAUTH_AZURE_DOMAINS
63
+ )
64
+ return any(e for e in domains if host.endswith(e))
65
+
66
+
52
67
  def get_databricks_oidc_url(hostname: str):
53
68
  maybe_scheme = "https://" if not hostname.startswith("https://") else ""
54
69
  maybe_trailing_slash = "/" if not hostname.endswith("/") else ""
@@ -94,7 +109,7 @@ class AzureOAuthEndpointCollection(OAuthEndpointCollection):
94
109
  return "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration"
95
110
 
96
111
 
97
- class AwsOAuthEndpointCollection(OAuthEndpointCollection):
112
+ class InHouseOAuthEndpointCollection(OAuthEndpointCollection):
98
113
  def get_scopes_mapping(self, scopes: List[str]) -> List[str]:
99
114
  # No scope mapping in AWS
100
115
  return scopes.copy()
@@ -108,10 +123,18 @@ class AwsOAuthEndpointCollection(OAuthEndpointCollection):
108
123
  return f"{idp_url}/.well-known/oauth-authorization-server"
109
124
 
110
125
 
111
- def get_oauth_endpoints(cloud: CloudType) -> Optional[OAuthEndpointCollection]:
112
- if cloud == CloudType.AWS:
113
- return AwsOAuthEndpointCollection()
126
+ def get_oauth_endpoints(
127
+ hostname: str, use_azure_auth: bool
128
+ ) -> Optional[OAuthEndpointCollection]:
129
+ cloud = infer_cloud_from_host(hostname)
130
+
131
+ if cloud in [CloudType.AWS, CloudType.GCP]:
132
+ return InHouseOAuthEndpointCollection()
114
133
  elif cloud == CloudType.AZURE:
115
- return AzureOAuthEndpointCollection()
134
+ return (
135
+ InHouseOAuthEndpointCollection()
136
+ if is_supported_databricks_oauth_host(hostname) and not use_azure_auth
137
+ else AzureOAuthEndpointCollection()
138
+ )
116
139
  else:
117
140
  return None
@@ -283,8 +283,10 @@ class DatabricksRetryPolicy(Retry):
283
283
  """
284
284
  retry_after = self.get_retry_after(response)
285
285
  if retry_after:
286
- self.check_proposed_wait(retry_after)
287
- time.sleep(retry_after)
286
+ backoff = self.get_backoff_time()
287
+ proposed_wait = max(backoff, retry_after)
288
+ self.check_proposed_wait(proposed_wait)
289
+ time.sleep(proposed_wait)
288
290
  return True
289
291
 
290
292
  return False
@@ -96,7 +96,8 @@ class Connection:
96
96
  legacy purposes and will be deprecated in a future release. When this parameter is `True` you will see
97
97
  a warning log message. To suppress this log message, set `use_inline_params="silent"`.
98
98
  auth_type: `str`, optional
99
- `databricks-oauth` : to use oauth with fine-grained permission scopes, set to `databricks-oauth`.
99
+ `databricks-oauth` : to use Databricks OAuth with fine-grained permission scopes, set to `databricks-oauth`.
100
+ `azure-oauth` : to use Microsoft Entra ID OAuth flow, set to `azure-oauth`.
100
101
 
101
102
  oauth_client_id: `str`, optional
102
103
  custom oauth client_id. If not specified, it will use the built-in client_id of databricks-sql-python.
@@ -107,9 +108,9 @@ class Connection:
107
108
 
108
109
  experimental_oauth_persistence: configures preferred storage for persisting oauth tokens.
109
110
  This has to be a class implementing `OAuthPersistence`.
110
- When `auth_type` is set to `databricks-oauth` without persisting the oauth token in a persistence storage
111
- the oauth tokens will only be maintained in memory and if the python process restarts the end user
112
- will have to login again.
111
+ When `auth_type` is set to `databricks-oauth` or `azure-oauth` without persisting the oauth token in a
112
+ persistence storage the oauth tokens will only be maintained in memory and if the python process
113
+ restarts the end user will have to login again.
113
114
  Note this is beta (private preview)
114
115
 
115
116
  For persisting the oauth token in a prod environment you should subclass and implement OAuthPersistence
@@ -605,12 +606,15 @@ class Cursor:
605
606
  "Local file operations are restricted to paths within the configured staging_allowed_local_path"
606
607
  )
607
608
 
608
- # TODO: Experiment with DBR sending real headers.
609
- # The specification says headers will be in JSON format but the current null value is actually an empty list []
609
+ # May be real headers, or could be json string
610
+ headers = (
611
+ json.loads(row.headers) if isinstance(row.headers, str) else row.headers
612
+ )
613
+
610
614
  handler_args = {
611
615
  "presigned_url": row.presignedUrl,
612
616
  "local_file": abs_localFile,
613
- "headers": json.loads(row.headers or "{}"),
617
+ "headers": dict(headers) or {},
614
618
  }
615
619
 
616
620
  logger.debug(
@@ -0,0 +1,44 @@
1
+ import os
2
+ import pytest
3
+
4
+
5
+ @pytest.fixture(scope="session")
6
+ def host():
7
+ return os.getenv("DATABRICKS_SERVER_HOSTNAME")
8
+
9
+
10
+ @pytest.fixture(scope="session")
11
+ def http_path():
12
+ return os.getenv("DATABRICKS_HTTP_PATH")
13
+
14
+
15
+ @pytest.fixture(scope="session")
16
+ def access_token():
17
+ return os.getenv("DATABRICKS_TOKEN")
18
+
19
+
20
+ @pytest.fixture(scope="session")
21
+ def ingestion_user():
22
+ return os.getenv("DATABRICKS_USER")
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def catalog():
27
+ return os.getenv("DATABRICKS_CATALOG")
28
+
29
+
30
+ @pytest.fixture(scope="session")
31
+ def schema():
32
+ return os.getenv("DATABRICKS_SCHEMA", "default")
33
+
34
+
35
+ @pytest.fixture(scope="session", autouse=True)
36
+ def connection_details(host, http_path, access_token, ingestion_user, catalog, schema):
37
+ return {
38
+ "host": host,
39
+ "http_path": http_path,
40
+ "access_token": access_token,
41
+ "ingestion_user": ingestion_user,
42
+ "catalog": catalog,
43
+ "schema": schema,
44
+ }
@@ -1,6 +1,5 @@
1
1
  import datetime
2
2
  import decimal
3
- import os
4
3
  from typing import Tuple, Union, List
5
4
  from unittest import skipIf
6
5
 
@@ -19,7 +18,7 @@ from sqlalchemy.engine import Engine
19
18
  from sqlalchemy.engine.reflection import Inspector
20
19
  from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
21
20
  from sqlalchemy.schema import DropColumnComment, SetColumnComment
22
- from sqlalchemy.types import BOOLEAN, DECIMAL, Date, DateTime, Integer, String
21
+ from sqlalchemy.types import BOOLEAN, DECIMAL, Date, Integer, String
23
22
 
24
23
  try:
25
24
  from sqlalchemy.orm import declarative_base
@@ -49,12 +48,12 @@ def version_agnostic_select(object_to_select, *args, **kwargs):
49
48
  return select(object_to_select, *args, **kwargs)
50
49
 
51
50
 
52
- def version_agnostic_connect_arguments(catalog=None, schema=None) -> Tuple[str, dict]:
53
- HOST = os.environ.get("host")
54
- HTTP_PATH = os.environ.get("http_path")
55
- ACCESS_TOKEN = os.environ.get("access_token")
56
- CATALOG = catalog or os.environ.get("catalog")
57
- SCHEMA = schema or os.environ.get("schema")
51
+ def version_agnostic_connect_arguments(connection_details) -> Tuple[str, dict]:
52
+ HOST = connection_details["host"]
53
+ HTTP_PATH = connection_details["http_path"]
54
+ ACCESS_TOKEN = connection_details["access_token"]
55
+ CATALOG = connection_details["catalog"]
56
+ SCHEMA = connection_details["schema"]
58
57
 
59
58
  ua_connect_args = {"_user_agent_entry": USER_AGENT_TOKEN}
60
59
 
@@ -77,8 +76,8 @@ def version_agnostic_connect_arguments(catalog=None, schema=None) -> Tuple[str,
77
76
 
78
77
 
79
78
  @pytest.fixture
80
- def db_engine() -> Engine:
81
- conn_string, connect_args = version_agnostic_connect_arguments()
79
+ def db_engine(connection_details) -> Engine:
80
+ conn_string, connect_args = version_agnostic_connect_arguments(connection_details)
82
81
  return create_engine(conn_string, connect_args=connect_args)
83
82
 
84
83
 
@@ -92,10 +91,11 @@ def run_query(db_engine: Engine, query: Union[str, Text]):
92
91
 
93
92
 
94
93
  @pytest.fixture
95
- def samples_engine() -> Engine:
96
- conn_string, connect_args = version_agnostic_connect_arguments(
97
- catalog="samples", schema="nyctaxi"
98
- )
94
+ def samples_engine(connection_details) -> Engine:
95
+ details = connection_details.copy()
96
+ details["catalog"] = "samples"
97
+ details["schema"] = "nyctaxi"
98
+ conn_string, connect_args = version_agnostic_connect_arguments(details)
99
99
  return create_engine(conn_string, connect_args=connect_args)
100
100
 
101
101
 
@@ -141,7 +141,7 @@ def test_connect_args(db_engine):
141
141
  def test_pandas_upload(db_engine, metadata_obj):
142
142
  import pandas as pd
143
143
 
144
- SCHEMA = os.environ.get("schema")
144
+ SCHEMA = "default"
145
145
  try:
146
146
  df = pd.read_excel(
147
147
  "src/databricks/sqlalchemy/test_local/e2e/demo_data/MOCK_DATA.xlsx"
@@ -409,7 +409,9 @@ def test_get_table_names_smoke_test(samples_engine: Engine):
409
409
  _names is not None, "get_table_names did not succeed"
410
410
 
411
411
 
412
- def test_has_table_across_schemas(db_engine: Engine, samples_engine: Engine):
412
+ def test_has_table_across_schemas(
413
+ db_engine: Engine, samples_engine: Engine, catalog: str, schema: str
414
+ ):
413
415
  """For this test to pass these conditions must be met:
414
416
  - Table samples.nyctaxi.trips must exist
415
417
  - Table samples.tpch.customer must exist
@@ -426,9 +428,6 @@ def test_has_table_across_schemas(db_engine: Engine, samples_engine: Engine):
426
428
  )
427
429
 
428
430
  # 3) Check for a table within a different catalog
429
- other_catalog = os.environ.get("catalog")
430
- other_schema = os.environ.get("schema")
431
-
432
431
  # Create a table in a different catalog
433
432
  with db_engine.connect() as conn:
434
433
  conn.execute(text("CREATE TABLE test_has_table (numbers_are_cool INT);"))
@@ -442,8 +441,8 @@ def test_has_table_across_schemas(db_engine: Engine, samples_engine: Engine):
442
441
  assert samples_engine.dialect.has_table(
443
442
  connection=conn,
444
443
  table_name="test_has_table",
445
- schema=other_schema,
446
- catalog=other_catalog,
444
+ schema=schema,
445
+ catalog=catalog,
447
446
  )
448
447
  finally:
449
448
  conn.execute(text("DROP TABLE test_has_table;"))
@@ -503,12 +502,12 @@ def test_get_columns(db_engine, sample_table: str):
503
502
 
504
503
  class TestCommentReflection:
505
504
  @pytest.fixture(scope="class")
506
- def engine(self):
507
- HOST = os.environ.get("host")
508
- HTTP_PATH = os.environ.get("http_path")
509
- ACCESS_TOKEN = os.environ.get("access_token")
510
- CATALOG = os.environ.get("catalog")
511
- SCHEMA = os.environ.get("schema")
505
+ def engine(self, connection_details: dict):
506
+ HOST = connection_details["host"]
507
+ HTTP_PATH = connection_details["http_path"]
508
+ ACCESS_TOKEN = connection_details["access_token"]
509
+ CATALOG = connection_details["catalog"]
510
+ SCHEMA = connection_details["schema"]
512
511
 
513
512
  connection_string = f"databricks://token:{ACCESS_TOKEN}@{HOST}?http_path={HTTP_PATH}&catalog={CATALOG}&schema={SCHEMA}"
514
513
  connect_args = {"_user_agent_entry": USER_AGENT_TOKEN}
@@ -64,16 +64,16 @@ def test_extract_3l_namespace_from_bad_constraint_string():
64
64
  extract_three_level_identifier_from_constraint_string(input)
65
65
 
66
66
 
67
- @pytest.mark.parametrize("schema", [None, "some_schema"])
68
- def test_build_fk_dict(schema):
67
+ @pytest.mark.parametrize("tschema", [None, "some_schema"])
68
+ def test_build_fk_dict(tschema):
69
69
  fk_constraint_string = "FOREIGN KEY (`parent_user_id`) REFERENCES `main`.`some_schema`.`users` (`user_id`)"
70
70
 
71
- result = build_fk_dict("some_fk_name", fk_constraint_string, schema_name=schema)
71
+ result = build_fk_dict("some_fk_name", fk_constraint_string, schema_name=tschema)
72
72
 
73
73
  assert result == {
74
74
  "name": "some_fk_name",
75
75
  "constrained_columns": ["parent_user_id"],
76
- "referred_schema": schema,
76
+ "referred_schema": tschema,
77
77
  "referred_table": "users",
78
78
  "referred_columns": ["user_id"],
79
79
  }
@@ -1,3 +0,0 @@
1
- [pytest]
2
- markers =
3
- reviewed: Test case has been reviewed by databricks