databricks-sdk 0.44.1__py3-none-any.whl → 0.46.0__py3-none-any.whl
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.
Potentially problematic release.
This version of databricks-sdk might be problematic. Click here for more details.
- databricks/sdk/__init__.py +135 -116
- databricks/sdk/_base_client.py +112 -88
- databricks/sdk/_property.py +12 -7
- databricks/sdk/_widgets/__init__.py +13 -2
- databricks/sdk/_widgets/default_widgets_utils.py +21 -15
- databricks/sdk/_widgets/ipywidgets_utils.py +47 -24
- databricks/sdk/azure.py +8 -6
- databricks/sdk/casing.py +5 -5
- databricks/sdk/config.py +156 -99
- databricks/sdk/core.py +57 -47
- databricks/sdk/credentials_provider.py +306 -206
- databricks/sdk/data_plane.py +75 -50
- databricks/sdk/dbutils.py +123 -87
- databricks/sdk/environments.py +52 -35
- databricks/sdk/errors/base.py +61 -35
- databricks/sdk/errors/customizer.py +3 -3
- databricks/sdk/errors/deserializer.py +38 -25
- databricks/sdk/errors/details.py +417 -0
- databricks/sdk/errors/mapper.py +1 -1
- databricks/sdk/errors/overrides.py +27 -24
- databricks/sdk/errors/parser.py +26 -14
- databricks/sdk/errors/platform.py +10 -10
- databricks/sdk/errors/private_link.py +24 -24
- databricks/sdk/logger/round_trip_logger.py +28 -20
- databricks/sdk/mixins/compute.py +90 -60
- databricks/sdk/mixins/files.py +815 -145
- databricks/sdk/mixins/jobs.py +191 -16
- databricks/sdk/mixins/open_ai_client.py +26 -20
- databricks/sdk/mixins/workspace.py +45 -34
- databricks/sdk/oauth.py +379 -198
- databricks/sdk/retries.py +14 -12
- databricks/sdk/runtime/__init__.py +34 -17
- databricks/sdk/runtime/dbutils_stub.py +52 -39
- databricks/sdk/service/_internal.py +12 -7
- databricks/sdk/service/apps.py +618 -418
- databricks/sdk/service/billing.py +827 -604
- databricks/sdk/service/catalog.py +6552 -4474
- databricks/sdk/service/cleanrooms.py +550 -388
- databricks/sdk/service/compute.py +5263 -3536
- databricks/sdk/service/dashboards.py +1331 -924
- databricks/sdk/service/files.py +446 -309
- databricks/sdk/service/iam.py +2115 -1483
- databricks/sdk/service/jobs.py +4151 -2588
- databricks/sdk/service/marketplace.py +2210 -1517
- databricks/sdk/service/ml.py +3839 -2256
- databricks/sdk/service/oauth2.py +910 -584
- databricks/sdk/service/pipelines.py +1865 -1203
- databricks/sdk/service/provisioning.py +1435 -1029
- databricks/sdk/service/serving.py +2060 -1290
- databricks/sdk/service/settings.py +2846 -1929
- databricks/sdk/service/sharing.py +2201 -877
- databricks/sdk/service/sql.py +4650 -3103
- databricks/sdk/service/vectorsearch.py +816 -550
- databricks/sdk/service/workspace.py +1330 -906
- databricks/sdk/useragent.py +36 -22
- databricks/sdk/version.py +1 -1
- {databricks_sdk-0.44.1.dist-info → databricks_sdk-0.46.0.dist-info}/METADATA +31 -31
- databricks_sdk-0.46.0.dist-info/RECORD +70 -0
- {databricks_sdk-0.44.1.dist-info → databricks_sdk-0.46.0.dist-info}/WHEEL +1 -1
- databricks_sdk-0.44.1.dist-info/RECORD +0 -69
- {databricks_sdk-0.44.1.dist-info → databricks_sdk-0.46.0.dist-info}/LICENSE +0 -0
- {databricks_sdk-0.44.1.dist-info → databricks_sdk-0.46.0.dist-info}/NOTICE +0 -0
- {databricks_sdk-0.44.1.dist-info → databricks_sdk-0.46.0.dist-info}/top_level.txt +0 -0
databricks/sdk/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import configparser
|
|
2
2
|
import copy
|
|
3
|
+
import datetime
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import pathlib
|
|
@@ -19,11 +20,11 @@ from .oauth import (OidcEndpoints, Token, get_account_endpoints,
|
|
|
19
20
|
get_azure_entra_id_workspace_endpoints,
|
|
20
21
|
get_workspace_endpoints)
|
|
21
22
|
|
|
22
|
-
logger = logging.getLogger(
|
|
23
|
+
logger = logging.getLogger("databricks.sdk")
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class ConfigAttribute:
|
|
26
|
-
"""
|
|
27
|
+
"""Configuration attribute metadata and descriptor protocols."""
|
|
27
28
|
|
|
28
29
|
# name and transform are discovered from Config.__new__
|
|
29
30
|
name: str = None
|
|
@@ -34,12 +35,12 @@ class ConfigAttribute:
|
|
|
34
35
|
self.auth = auth
|
|
35
36
|
self.sensitive = sensitive
|
|
36
37
|
|
|
37
|
-
def __get__(self, cfg:
|
|
38
|
+
def __get__(self, cfg: "Config", owner):
|
|
38
39
|
if not cfg:
|
|
39
40
|
return None
|
|
40
41
|
return cfg._inner.get(self.name, None)
|
|
41
42
|
|
|
42
|
-
def __set__(self, cfg:
|
|
43
|
+
def __set__(self, cfg: "Config", value: any):
|
|
43
44
|
cfg._inner[self.name] = self.transform(value)
|
|
44
45
|
|
|
45
46
|
def __repr__(self) -> str:
|
|
@@ -57,71 +58,124 @@ def with_user_agent_extra(key: str, value: str):
|
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
class Config:
|
|
60
|
-
host: str = ConfigAttribute(env=
|
|
61
|
-
account_id: str = ConfigAttribute(env=
|
|
62
|
-
token: str = ConfigAttribute(env=
|
|
63
|
-
username: str = ConfigAttribute(env=
|
|
64
|
-
password: str = ConfigAttribute(env=
|
|
65
|
-
client_id: str = ConfigAttribute(env=
|
|
66
|
-
client_secret: str = ConfigAttribute(env=
|
|
67
|
-
profile: str = ConfigAttribute(env=
|
|
68
|
-
config_file: str = ConfigAttribute(env=
|
|
69
|
-
google_service_account: str = ConfigAttribute(env=
|
|
70
|
-
google_credentials: str = ConfigAttribute(env=
|
|
71
|
-
azure_workspace_resource_id: str = ConfigAttribute(env=
|
|
72
|
-
azure_use_msi: bool = ConfigAttribute(env=
|
|
73
|
-
azure_client_secret: str = ConfigAttribute(env=
|
|
74
|
-
azure_client_id: str = ConfigAttribute(env=
|
|
75
|
-
azure_tenant_id: str = ConfigAttribute(env=
|
|
76
|
-
azure_environment: str = ConfigAttribute(env=
|
|
77
|
-
databricks_cli_path: str = ConfigAttribute(env=
|
|
78
|
-
auth_type: str = ConfigAttribute(env=
|
|
79
|
-
cluster_id: str = ConfigAttribute(env=
|
|
80
|
-
warehouse_id: str = ConfigAttribute(env=
|
|
81
|
-
serverless_compute_id: str = ConfigAttribute(env=
|
|
61
|
+
host: str = ConfigAttribute(env="DATABRICKS_HOST")
|
|
62
|
+
account_id: str = ConfigAttribute(env="DATABRICKS_ACCOUNT_ID")
|
|
63
|
+
token: str = ConfigAttribute(env="DATABRICKS_TOKEN", auth="pat", sensitive=True)
|
|
64
|
+
username: str = ConfigAttribute(env="DATABRICKS_USERNAME", auth="basic")
|
|
65
|
+
password: str = ConfigAttribute(env="DATABRICKS_PASSWORD", auth="basic", sensitive=True)
|
|
66
|
+
client_id: str = ConfigAttribute(env="DATABRICKS_CLIENT_ID", auth="oauth")
|
|
67
|
+
client_secret: str = ConfigAttribute(env="DATABRICKS_CLIENT_SECRET", auth="oauth", sensitive=True)
|
|
68
|
+
profile: str = ConfigAttribute(env="DATABRICKS_CONFIG_PROFILE")
|
|
69
|
+
config_file: str = ConfigAttribute(env="DATABRICKS_CONFIG_FILE")
|
|
70
|
+
google_service_account: str = ConfigAttribute(env="DATABRICKS_GOOGLE_SERVICE_ACCOUNT", auth="google")
|
|
71
|
+
google_credentials: str = ConfigAttribute(env="GOOGLE_CREDENTIALS", auth="google", sensitive=True)
|
|
72
|
+
azure_workspace_resource_id: str = ConfigAttribute(env="DATABRICKS_AZURE_RESOURCE_ID", auth="azure")
|
|
73
|
+
azure_use_msi: bool = ConfigAttribute(env="ARM_USE_MSI", auth="azure")
|
|
74
|
+
azure_client_secret: str = ConfigAttribute(env="ARM_CLIENT_SECRET", auth="azure", sensitive=True)
|
|
75
|
+
azure_client_id: str = ConfigAttribute(env="ARM_CLIENT_ID", auth="azure")
|
|
76
|
+
azure_tenant_id: str = ConfigAttribute(env="ARM_TENANT_ID", auth="azure")
|
|
77
|
+
azure_environment: str = ConfigAttribute(env="ARM_ENVIRONMENT")
|
|
78
|
+
databricks_cli_path: str = ConfigAttribute(env="DATABRICKS_CLI_PATH")
|
|
79
|
+
auth_type: str = ConfigAttribute(env="DATABRICKS_AUTH_TYPE")
|
|
80
|
+
cluster_id: str = ConfigAttribute(env="DATABRICKS_CLUSTER_ID")
|
|
81
|
+
warehouse_id: str = ConfigAttribute(env="DATABRICKS_WAREHOUSE_ID")
|
|
82
|
+
serverless_compute_id: str = ConfigAttribute(env="DATABRICKS_SERVERLESS_COMPUTE_ID")
|
|
82
83
|
skip_verify: bool = ConfigAttribute()
|
|
83
84
|
http_timeout_seconds: float = ConfigAttribute()
|
|
84
|
-
debug_truncate_bytes: int = ConfigAttribute(env=
|
|
85
|
-
debug_headers: bool = ConfigAttribute(env=
|
|
86
|
-
rate_limit: int = ConfigAttribute(env=
|
|
85
|
+
debug_truncate_bytes: int = ConfigAttribute(env="DATABRICKS_DEBUG_TRUNCATE_BYTES")
|
|
86
|
+
debug_headers: bool = ConfigAttribute(env="DATABRICKS_DEBUG_HEADERS")
|
|
87
|
+
rate_limit: int = ConfigAttribute(env="DATABRICKS_RATE_LIMIT")
|
|
87
88
|
retry_timeout_seconds: int = ConfigAttribute()
|
|
88
|
-
metadata_service_url = ConfigAttribute(
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
metadata_service_url = ConfigAttribute(
|
|
90
|
+
env="DATABRICKS_METADATA_SERVICE_URL",
|
|
91
|
+
auth="metadata-service",
|
|
92
|
+
sensitive=True,
|
|
93
|
+
)
|
|
91
94
|
max_connection_pools: int = ConfigAttribute()
|
|
92
95
|
max_connections_per_pool: int = ConfigAttribute()
|
|
93
96
|
databricks_environment: Optional[DatabricksEnvironment] = None
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
env=
|
|
98
|
+
enable_experimental_async_token_refresh: bool = ConfigAttribute(
|
|
99
|
+
env="DATABRICKS_ENABLE_EXPERIMENTAL_ASYNC_TOKEN_REFRESH"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
enable_experimental_files_api_client: bool = ConfigAttribute(env="DATABRICKS_ENABLE_EXPERIMENTAL_FILES_API_CLIENT")
|
|
97
103
|
files_api_client_download_max_total_recovers = None
|
|
98
104
|
files_api_client_download_max_total_recovers_without_progressing = 1
|
|
99
105
|
|
|
106
|
+
# File multipart upload parameters
|
|
107
|
+
# ----------------------
|
|
108
|
+
|
|
109
|
+
# Minimal input stream size (bytes) to use multipart / resumable uploads.
|
|
110
|
+
# For small files it's more efficient to make one single-shot upload request.
|
|
111
|
+
# When uploading a file, SDK will initially buffer this many bytes from input stream.
|
|
112
|
+
# This parameter can be less or bigger than multipart_upload_chunk_size.
|
|
113
|
+
multipart_upload_min_stream_size: int = 5 * 1024 * 1024
|
|
114
|
+
|
|
115
|
+
# Maximum number of presigned URLs that can be requested at a time.
|
|
116
|
+
#
|
|
117
|
+
# The more URLs we request at once, the higher chance is that some of the URLs will expire
|
|
118
|
+
# before we get to use it. We discover the presigned URL is expired *after* sending the
|
|
119
|
+
# input stream partition to the server. So to retry the upload of this partition we must rewind
|
|
120
|
+
# the stream back. In case of a non-seekable stream we cannot rewind, so we'll abort
|
|
121
|
+
# the upload. To reduce the chance of this, we're requesting presigned URLs one by one
|
|
122
|
+
# and using them immediately.
|
|
123
|
+
multipart_upload_batch_url_count: int = 1
|
|
124
|
+
|
|
125
|
+
# Size of the chunk to use for multipart uploads.
|
|
126
|
+
#
|
|
127
|
+
# The smaller chunk is, the less chance for network errors (or URL get expired),
|
|
128
|
+
# but the more requests we'll make.
|
|
129
|
+
# For AWS, minimum is 5Mb: https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
|
|
130
|
+
# For GCP, minimum is 256 KiB (and also recommended multiple is 256 KiB)
|
|
131
|
+
# boto uses 8Mb: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/s3.html#boto3.s3.transfer.TransferConfig
|
|
132
|
+
multipart_upload_chunk_size: int = 10 * 1024 * 1024
|
|
133
|
+
|
|
134
|
+
# use maximum duration of 1 hour
|
|
135
|
+
multipart_upload_url_expiration_duration: datetime.timedelta = datetime.timedelta(hours=1)
|
|
136
|
+
|
|
137
|
+
# This is not a "wall time" cutoff for the whole upload request,
|
|
138
|
+
# but a maximum time between consecutive data reception events (even 1 byte) from the server
|
|
139
|
+
multipart_upload_single_chunk_upload_timeout_seconds: float = 60
|
|
140
|
+
|
|
141
|
+
# Cap on the number of custom retries during incremental uploads:
|
|
142
|
+
# 1) multipart: upload part URL is expired, so new upload URLs must be requested to continue upload
|
|
143
|
+
# 2) resumable: chunk upload produced a retryable response (or exception), so upload status must be
|
|
144
|
+
# retrieved to continue the upload.
|
|
145
|
+
# In these two cases standard SDK retries (which are capped by the `retry_timeout_seconds` option) are not used.
|
|
146
|
+
# Note that retry counter is reset when upload is successfully resumed.
|
|
147
|
+
multipart_upload_max_retries = 3
|
|
148
|
+
|
|
100
149
|
def __init__(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
# Deprecated. Use credentials_strategy instead.
|
|
153
|
+
credentials_provider: Optional[CredentialsStrategy] = None,
|
|
154
|
+
credentials_strategy: Optional[CredentialsStrategy] = None,
|
|
155
|
+
product=None,
|
|
156
|
+
product_version=None,
|
|
157
|
+
clock: Optional[Clock] = None,
|
|
158
|
+
**kwargs,
|
|
159
|
+
):
|
|
110
160
|
self._header_factory = None
|
|
111
161
|
self._inner = {}
|
|
112
162
|
self._user_agent_other_info = []
|
|
113
163
|
if credentials_strategy and credentials_provider:
|
|
114
|
-
raise ValueError(
|
|
115
|
-
"When providing `credentials_strategy` field, `credential_provider` cannot be specified.")
|
|
164
|
+
raise ValueError("When providing `credentials_strategy` field, `credential_provider` cannot be specified.")
|
|
116
165
|
if credentials_provider:
|
|
117
|
-
logger.warning(
|
|
118
|
-
"parameter 'credentials_provider' is deprecated. Use 'credentials_strategy' instead.")
|
|
166
|
+
logger.warning("parameter 'credentials_provider' is deprecated. Use 'credentials_strategy' instead.")
|
|
119
167
|
self._credentials_strategy = next(
|
|
120
|
-
s
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
s
|
|
169
|
+
for s in [
|
|
170
|
+
credentials_strategy,
|
|
171
|
+
credentials_provider,
|
|
172
|
+
DefaultCredentials(),
|
|
173
|
+
]
|
|
174
|
+
if s is not None
|
|
175
|
+
)
|
|
176
|
+
if "databricks_environment" in kwargs:
|
|
177
|
+
self.databricks_environment = kwargs["databricks_environment"]
|
|
178
|
+
del kwargs["databricks_environment"]
|
|
125
179
|
self._clock = clock if clock is not None else RealClock()
|
|
126
180
|
try:
|
|
127
181
|
self._set_inner_config(kwargs)
|
|
@@ -145,15 +199,15 @@ class Config:
|
|
|
145
199
|
return message
|
|
146
200
|
|
|
147
201
|
@staticmethod
|
|
148
|
-
def parse_dsn(dsn: str) ->
|
|
202
|
+
def parse_dsn(dsn: str) -> "Config":
|
|
149
203
|
uri = urllib.parse.urlparse(dsn)
|
|
150
|
-
if uri.scheme !=
|
|
151
|
-
raise ValueError(f
|
|
152
|
-
kwargs = {
|
|
204
|
+
if uri.scheme != "databricks":
|
|
205
|
+
raise ValueError(f"Expected databricks:// scheme, got {uri.scheme}://")
|
|
206
|
+
kwargs = {"host": f"https://{uri.hostname}"}
|
|
153
207
|
if uri.username:
|
|
154
|
-
kwargs[
|
|
208
|
+
kwargs["username"] = uri.username
|
|
155
209
|
if uri.password:
|
|
156
|
-
kwargs[
|
|
210
|
+
kwargs["password"] = uri.password
|
|
157
211
|
query = dict(urllib.parse.parse_qsl(uri.query))
|
|
158
212
|
for attr in Config.attributes():
|
|
159
213
|
if attr.name not in query:
|
|
@@ -162,7 +216,7 @@ class Config:
|
|
|
162
216
|
return Config(**kwargs)
|
|
163
217
|
|
|
164
218
|
def authenticate(self) -> Dict[str, str]:
|
|
165
|
-
"""
|
|
219
|
+
"""Returns a list of fresh authentication headers"""
|
|
166
220
|
return self._header_factory()
|
|
167
221
|
|
|
168
222
|
def as_dict(self) -> dict:
|
|
@@ -174,9 +228,9 @@ class Config:
|
|
|
174
228
|
env = self.azure_environment.upper()
|
|
175
229
|
# Compatibility with older versions of the SDK that allowed users to specify AzurePublicCloud or AzureChinaCloud
|
|
176
230
|
if env.startswith("AZURE"):
|
|
177
|
-
env = env[len("AZURE"):]
|
|
231
|
+
env = env[len("AZURE") :]
|
|
178
232
|
if env.endswith("CLOUD"):
|
|
179
|
-
env = env[
|
|
233
|
+
env = env[: -len("CLOUD")]
|
|
180
234
|
return env
|
|
181
235
|
|
|
182
236
|
@property
|
|
@@ -241,19 +295,21 @@ class Config:
|
|
|
241
295
|
|
|
242
296
|
@property
|
|
243
297
|
def user_agent(self):
|
|
244
|
-
"""
|
|
298
|
+
"""Returns User-Agent header used by this SDK"""
|
|
245
299
|
|
|
246
300
|
# global user agent includes SDK version, product name & version, platform info,
|
|
247
301
|
# and global extra info. Config can have specific extra info associated with it,
|
|
248
302
|
# such as an override product, auth type, and other user-defined information.
|
|
249
|
-
return useragent.to_string(
|
|
250
|
-
|
|
303
|
+
return useragent.to_string(
|
|
304
|
+
self._product_info,
|
|
305
|
+
[("auth", self.auth_type)] + self._user_agent_other_info,
|
|
306
|
+
)
|
|
251
307
|
|
|
252
308
|
@property
|
|
253
309
|
def _upstream_user_agent(self) -> str:
|
|
254
310
|
return " ".join(f"{k}/{v}" for k, v in useragent._get_upstream_user_agent_info())
|
|
255
311
|
|
|
256
|
-
def with_user_agent_extra(self, key: str, value: str) ->
|
|
312
|
+
def with_user_agent_extra(self, key: str, value: str) -> "Config":
|
|
257
313
|
self._user_agent_other_info.append((key, value))
|
|
258
314
|
return self
|
|
259
315
|
|
|
@@ -269,7 +325,7 @@ class Config:
|
|
|
269
325
|
return get_workspace_endpoints(self.host)
|
|
270
326
|
|
|
271
327
|
def debug_string(self) -> str:
|
|
272
|
-
"""
|
|
328
|
+
"""Returns log-friendly representation of configured attributes"""
|
|
273
329
|
buf = []
|
|
274
330
|
attrs_used = []
|
|
275
331
|
envs_used = []
|
|
@@ -279,13 +335,13 @@ class Config:
|
|
|
279
335
|
value = getattr(self, attr.name)
|
|
280
336
|
if not value:
|
|
281
337
|
continue
|
|
282
|
-
safe =
|
|
283
|
-
attrs_used.append(f
|
|
338
|
+
safe = "***" if attr.sensitive else f"{value}"
|
|
339
|
+
attrs_used.append(f"{attr.name}={safe}")
|
|
284
340
|
if attrs_used:
|
|
285
341
|
buf.append(f'Config: {", ".join(attrs_used)}')
|
|
286
342
|
if envs_used:
|
|
287
343
|
buf.append(f'Env: {", ".join(envs_used)}')
|
|
288
|
-
return
|
|
344
|
+
return ". ".join(buf)
|
|
289
345
|
|
|
290
346
|
def to_dict(self) -> Dict[str, any]:
|
|
291
347
|
return self._inner
|
|
@@ -302,16 +358,16 @@ class Config:
|
|
|
302
358
|
if (not self.cluster_id) and (not self.warehouse_id):
|
|
303
359
|
return None
|
|
304
360
|
if self.cluster_id and self.warehouse_id:
|
|
305
|
-
raise ValueError(
|
|
361
|
+
raise ValueError("cannot have both cluster_id and warehouse_id")
|
|
306
362
|
headers = self.authenticate()
|
|
307
|
-
headers[
|
|
363
|
+
headers["User-Agent"] = f"{self.user_agent} sdk-feature/sql-http-path"
|
|
308
364
|
if self.cluster_id:
|
|
309
365
|
response = requests.get(f"{self.host}/api/2.0/preview/scim/v2/Me", headers=headers)
|
|
310
366
|
# get workspace ID from the response header
|
|
311
|
-
workspace_id = response.headers.get(
|
|
312
|
-
return f
|
|
367
|
+
workspace_id = response.headers.get("x-databricks-org-id")
|
|
368
|
+
return f"sql/protocolv1/o/{workspace_id}/{self.cluster_id}"
|
|
313
369
|
if self.warehouse_id:
|
|
314
|
-
return f
|
|
370
|
+
return f"/sql/1.0/warehouses/{self.warehouse_id}"
|
|
315
371
|
|
|
316
372
|
@property
|
|
317
373
|
def clock(self) -> Clock:
|
|
@@ -319,17 +375,18 @@ class Config:
|
|
|
319
375
|
|
|
320
376
|
@classmethod
|
|
321
377
|
def attributes(cls) -> Iterable[ConfigAttribute]:
|
|
322
|
-
"""
|
|
323
|
-
if hasattr(cls,
|
|
378
|
+
"""Returns a list of Databricks SDK configuration metadata"""
|
|
379
|
+
if hasattr(cls, "_attributes"):
|
|
324
380
|
return cls._attributes
|
|
325
381
|
if sys.version_info[1] >= 10:
|
|
326
382
|
import inspect
|
|
383
|
+
|
|
327
384
|
anno = inspect.get_annotations(cls)
|
|
328
385
|
else:
|
|
329
386
|
# Python 3.7 compatibility: getting type hints require extra hop, as described in
|
|
330
387
|
# "Accessing The Annotations Dict Of An Object In Python 3.9 And Older" section of
|
|
331
388
|
# https://docs.python.org/3/howto/annotations.html
|
|
332
|
-
anno = cls.__dict__[
|
|
389
|
+
anno = cls.__dict__["__annotations__"]
|
|
333
390
|
attrs = []
|
|
334
391
|
for name, v in cls.__dict__.items():
|
|
335
392
|
if type(v) != ConfigAttribute:
|
|
@@ -351,26 +408,25 @@ class Config:
|
|
|
351
408
|
If the tenant ID is already set, this method does nothing."""
|
|
352
409
|
if not self.is_azure or self.azure_tenant_id is not None or self.host is None:
|
|
353
410
|
return
|
|
354
|
-
login_url = f
|
|
355
|
-
logger.debug(f
|
|
411
|
+
login_url = f"{self.host}/aad/auth"
|
|
412
|
+
logger.debug(f"Loading tenant ID from {login_url}")
|
|
356
413
|
resp = requests.get(login_url, allow_redirects=False)
|
|
357
414
|
if resp.status_code // 100 != 3:
|
|
358
|
-
logger.debug(
|
|
359
|
-
f'Failed to get tenant ID from {login_url}: expected status code 3xx, got {resp.status_code}')
|
|
415
|
+
logger.debug(f"Failed to get tenant ID from {login_url}: expected status code 3xx, got {resp.status_code}")
|
|
360
416
|
return
|
|
361
|
-
entra_id_endpoint = resp.headers.get(
|
|
417
|
+
entra_id_endpoint = resp.headers.get("Location")
|
|
362
418
|
if entra_id_endpoint is None:
|
|
363
|
-
logger.debug(f
|
|
419
|
+
logger.debug(f"No Location header in response from {login_url}")
|
|
364
420
|
return
|
|
365
421
|
# The Location header has the following form: https://login.microsoftonline.com/<tenant-id>/oauth2/authorize?...
|
|
366
422
|
# The domain may change depending on the Azure cloud (e.g. login.microsoftonline.us for US Government cloud).
|
|
367
423
|
url = urllib.parse.urlparse(entra_id_endpoint)
|
|
368
|
-
path_segments = url.path.split(
|
|
424
|
+
path_segments = url.path.split("/")
|
|
369
425
|
if len(path_segments) < 2:
|
|
370
|
-
logger.debug(f
|
|
426
|
+
logger.debug(f"Invalid path in Location header: {url.path}")
|
|
371
427
|
return
|
|
372
428
|
self.azure_tenant_id = path_segments[1]
|
|
373
|
-
logger.debug(f
|
|
429
|
+
logger.debug(f"Loaded tenant ID: {self.azure_tenant_id}")
|
|
374
430
|
|
|
375
431
|
def _set_inner_config(self, keyword_args: Dict[str, any]):
|
|
376
432
|
for attr in self.attributes():
|
|
@@ -393,11 +449,10 @@ class Config:
|
|
|
393
449
|
self.__setattr__(attr.name, value)
|
|
394
450
|
found = True
|
|
395
451
|
if found:
|
|
396
|
-
logger.debug(
|
|
452
|
+
logger.debug("Loaded from environment")
|
|
397
453
|
|
|
398
454
|
def _known_file_config_loader(self):
|
|
399
|
-
if not self.profile and (self.is_any_auth_configured or self.host
|
|
400
|
-
or self.azure_workspace_resource_id):
|
|
455
|
+
if not self.profile and (self.is_any_auth_configured or self.host or self.azure_workspace_resource_id):
|
|
401
456
|
# skip loading configuration file if there's any auth configured
|
|
402
457
|
# directly as part of the Config() constructor.
|
|
403
458
|
return
|
|
@@ -417,15 +472,15 @@ class Config:
|
|
|
417
472
|
# from Unified Auth test suite at the moment. Hence, the private variable access.
|
|
418
473
|
# See: https://docs.python.org/3/library/configparser.html#mapping-protocol-access
|
|
419
474
|
if not has_explicit_profile and not ini_file.defaults():
|
|
420
|
-
logger.debug(f
|
|
475
|
+
logger.debug(f"{config_path} has no DEFAULT profile configured")
|
|
421
476
|
return
|
|
422
477
|
if not has_explicit_profile:
|
|
423
478
|
profile = "DEFAULT"
|
|
424
479
|
profiles = ini_file._sections
|
|
425
480
|
if ini_file.defaults():
|
|
426
|
-
profiles[
|
|
481
|
+
profiles["DEFAULT"] = ini_file.defaults()
|
|
427
482
|
if profile not in profiles:
|
|
428
|
-
raise ValueError(f
|
|
483
|
+
raise ValueError(f"resolve: {config_path} has no {profile} profile configured")
|
|
429
484
|
raw_config = profiles[profile]
|
|
430
485
|
logger.info(f'loading {profile} profile from {config_file}: {", ".join(raw_config.keys())}')
|
|
431
486
|
for k, v in raw_config.items():
|
|
@@ -448,26 +503,29 @@ class Config:
|
|
|
448
503
|
# client has auth preference set
|
|
449
504
|
return
|
|
450
505
|
names = " and ".join(sorted(auths_used))
|
|
451
|
-
raise ValueError(f
|
|
506
|
+
raise ValueError(f"validate: more than one authorization method configured: {names}")
|
|
452
507
|
|
|
453
508
|
def init_auth(self):
|
|
454
509
|
try:
|
|
455
510
|
self._header_factory = self._credentials_strategy(self)
|
|
456
511
|
self.auth_type = self._credentials_strategy.auth_type()
|
|
457
512
|
if not self._header_factory:
|
|
458
|
-
raise ValueError(
|
|
513
|
+
raise ValueError("not configured")
|
|
459
514
|
except ValueError as e:
|
|
460
|
-
raise ValueError(f
|
|
515
|
+
raise ValueError(f"{self._credentials_strategy.auth_type()} auth: {e}") from e
|
|
461
516
|
|
|
462
517
|
def _init_product(self, product, product_version):
|
|
463
518
|
if product is not None or product_version is not None:
|
|
464
519
|
default_product, default_version = useragent.product()
|
|
465
|
-
self._product_info = (
|
|
520
|
+
self._product_info = (
|
|
521
|
+
product or default_product,
|
|
522
|
+
product_version or default_version,
|
|
523
|
+
)
|
|
466
524
|
else:
|
|
467
525
|
self._product_info = None
|
|
468
526
|
|
|
469
527
|
def __repr__(self):
|
|
470
|
-
return f
|
|
528
|
+
return f"<{self.debug_string()}>"
|
|
471
529
|
|
|
472
530
|
def copy(self):
|
|
473
531
|
"""Creates a copy of the config object.
|
|
@@ -480,6 +538,5 @@ class Config:
|
|
|
480
538
|
return cpy
|
|
481
539
|
|
|
482
540
|
def deep_copy(self):
|
|
483
|
-
"""Creates a deep copy of the config object.
|
|
484
|
-
"""
|
|
541
|
+
"""Creates a deep copy of the config object."""
|
|
485
542
|
return copy.deepcopy(self)
|
databricks/sdk/core.py
CHANGED
|
@@ -9,9 +9,9 @@ from .credentials_provider import *
|
|
|
9
9
|
from .errors import DatabricksError, _ErrorCustomizer
|
|
10
10
|
from .oauth import retrieve_token
|
|
11
11
|
|
|
12
|
-
__all__ = [
|
|
12
|
+
__all__ = ["Config", "DatabricksError"]
|
|
13
13
|
|
|
14
|
-
logger = logging.getLogger(
|
|
14
|
+
logger = logging.getLogger("databricks.sdk")
|
|
15
15
|
|
|
16
16
|
URL_ENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
|
|
17
17
|
JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
|
@@ -22,16 +22,18 @@ class ApiClient:
|
|
|
22
22
|
|
|
23
23
|
def __init__(self, cfg: Config):
|
|
24
24
|
self._cfg = cfg
|
|
25
|
-
self._api_client = _BaseClient(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
self._api_client = _BaseClient(
|
|
26
|
+
debug_truncate_bytes=cfg.debug_truncate_bytes,
|
|
27
|
+
retry_timeout_seconds=cfg.retry_timeout_seconds,
|
|
28
|
+
user_agent_base=cfg.user_agent,
|
|
29
|
+
header_factory=cfg.authenticate,
|
|
30
|
+
max_connection_pools=cfg.max_connection_pools,
|
|
31
|
+
max_connections_per_pool=cfg.max_connections_per_pool,
|
|
32
|
+
pool_block=True,
|
|
33
|
+
http_timeout_seconds=cfg.http_timeout_seconds,
|
|
34
|
+
extra_error_customizers=[_AddDebugErrorCustomizer(cfg)],
|
|
35
|
+
clock=cfg.clock,
|
|
36
|
+
)
|
|
35
37
|
|
|
36
38
|
@property
|
|
37
39
|
def account_id(self) -> str:
|
|
@@ -46,44 +48,52 @@ class ApiClient:
|
|
|
46
48
|
self._cfg.authenticate()
|
|
47
49
|
original_token = self._cfg.oauth_token()
|
|
48
50
|
headers = {"Content-Type": URL_ENCODED_CONTENT_TYPE}
|
|
49
|
-
params = urlencode(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
params = urlencode(
|
|
52
|
+
{
|
|
53
|
+
"grant_type": JWT_BEARER_GRANT_TYPE,
|
|
54
|
+
"authorization_details": auth_details,
|
|
55
|
+
"assertion": original_token.access_token,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return retrieve_token(
|
|
59
|
+
client_id=self._cfg.client_id,
|
|
60
|
+
client_secret=self._cfg.client_secret,
|
|
61
|
+
token_url=self._cfg.host + OIDC_TOKEN_PATH,
|
|
62
|
+
params=params,
|
|
63
|
+
headers=headers,
|
|
64
|
+
)
|
|
59
65
|
|
|
60
|
-
def do(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
def do(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
path: Optional[str] = None,
|
|
70
|
+
url: Optional[str] = None,
|
|
71
|
+
query: Optional[dict] = None,
|
|
72
|
+
headers: Optional[dict] = None,
|
|
73
|
+
body: Optional[dict] = None,
|
|
74
|
+
raw: bool = False,
|
|
75
|
+
files=None,
|
|
76
|
+
data=None,
|
|
77
|
+
auth: Optional[Callable[[requests.PreparedRequest], requests.PreparedRequest]] = None,
|
|
78
|
+
response_headers: Optional[List[str]] = None,
|
|
79
|
+
) -> Union[dict, list, BinaryIO]:
|
|
72
80
|
if url is None:
|
|
73
81
|
# Remove extra `/` from path for Files API
|
|
74
82
|
# Once we've fixed the OpenAPI spec, we can remove this
|
|
75
|
-
path = re.sub(
|
|
83
|
+
path = re.sub("^/api/2.0/fs/files//", "/api/2.0/fs/files/", path)
|
|
76
84
|
url = f"{self._cfg.host}{path}"
|
|
77
|
-
return self._api_client.do(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
return self._api_client.do(
|
|
86
|
+
method=method,
|
|
87
|
+
url=url,
|
|
88
|
+
query=query,
|
|
89
|
+
headers=headers,
|
|
90
|
+
body=body,
|
|
91
|
+
raw=raw,
|
|
92
|
+
files=files,
|
|
93
|
+
data=data,
|
|
94
|
+
auth=auth,
|
|
95
|
+
response_headers=response_headers,
|
|
96
|
+
)
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
class _AddDebugErrorCustomizer(_ErrorCustomizer):
|
|
@@ -95,5 +105,5 @@ class _AddDebugErrorCustomizer(_ErrorCustomizer):
|
|
|
95
105
|
|
|
96
106
|
def customize_error(self, response: requests.Response, kwargs: dict):
|
|
97
107
|
if response.status_code in (401, 403):
|
|
98
|
-
message = kwargs.get(
|
|
99
|
-
kwargs[
|
|
108
|
+
message = kwargs.get("message", "request failed")
|
|
109
|
+
kwargs["message"] = self._cfg.wrap_debug_info(message)
|