databricks-sdk 0.156.0__tar.gz → 0.158.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.
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/CHANGELOG.md +28 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/PKG-INFO +2 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/credentials_provider.py +15 -6
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/platform.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files.py +59 -84
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentkit.py +520 -2
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/billing.py +218 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundle.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/catalog.py +6 -6
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/compute.py +4 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/database.py +54 -4
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataquality.py +58 -3
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iamv2.py +11 -7
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/jobs.py +178 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/ml.py +673 -5
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/pipelines.py +26 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sandbox.py +36 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/serving.py +2 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sharing.py +3 -2
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sql.py +2 -2
- databricks_sdk-0.158.0/databricks/sdk/version.py +1 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/PKG-INFO +2 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/requires.txt +1 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/pyproject.toml +1 -0
- databricks_sdk-0.156.0/databricks/sdk/version.py +0 -1
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/CONTRIBUTING.md +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/DCO +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/LICENSE +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/MANIFEST.in +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/Makefile +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/NOTICE +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/README.md +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/SECURITY.md +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_base_client.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_property.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2_grpc.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/default_widgets_utils.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/ipywidgets_utils.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/azure.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/casing.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/client_types.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/clock.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/lro.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/fieldmask.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/config.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/core.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/data_plane.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/dbutils.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/environments.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/base.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/customizer.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/deserializer.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/details.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/mapper.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/overrides.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/parser.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/private_link.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/sdk.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/round_trip_logger.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/_grpc_transport.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/compute.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files_utils.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/jobs.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/open_ai_client.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sandbox.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sharing.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/workspace.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oauth.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc_token_supplier.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/py.typed +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/retries.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/__init__.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/dbutils_stub.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/_internal.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentbricks.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aifunctions.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aisearch.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/apps.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundledeployments.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/cleanrooms.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dashboards.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataclassification.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/disasterrecovery.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/domains.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/environments.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/files.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iam.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/knowledgeassistants.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/marketplace.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/networking.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/oauth2.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/postgres.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/provisioning.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/qualitymonitorv2.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settings.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settingsv2.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/supervisoragents.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/tags.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/vectorsearch.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/workspace.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/useragent.py +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/SOURCES.txt +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/dependency_links.txt +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/top_level.txt +0 -0
- {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/setup.cfg +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Version changelog
|
|
2
2
|
|
|
3
|
+
## Release v0.158.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### API Changes
|
|
6
|
+
* Add `get_budget_spend_status()` and `list_budget_spend_statuses()` methods for [a.budgets](https://databricks-sdk-py.readthedocs.io/en/latest/account/billing/budgets.html) account-level service.
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
* Fix `WorkspaceClient.files.download_to` with `use_parallel=True` always falling back to the Files API for files above the parallel download threshold, instead of downloading byte ranges from presigned URLs (or from the storage proxy when `experimental_files_ext_enable_storage_proxy` is set). A byte range that fails after another range was received now raises, and expired GCP presigned URLs are refreshed like AWS and Azure ones.
|
|
10
|
+
* Fix `WorkspaceClient.files.download_to` with `use_parallel=True` requesting the wrong path from the Files API when the file path contains characters such as `#`, `?` or `%`.
|
|
11
|
+
* Fix Databricks CLI authentication rejecting small wrappers around the CLI (e.g. launcher scripts or version manager shims) as the legacy Python CLI. Small files found in `PATH` are now only rejected if they are the entry point pip installs for the legacy Python CLI.
|
|
12
|
+
|
|
13
|
+
## Release v0.157.0 (2026-10-24)
|
|
14
|
+
|
|
15
|
+
### API Changes
|
|
16
|
+
* Add `get_memory_store_permission()`, `get_session_store_permission()`, `grant_memory_store_permission()`, `grant_session_store_permission()`, `list_memory_store_permissions()`, `list_session_store_permissions()`, `revoke_memory_store_permission()` and `revoke_session_store_permission()` methods for [w.agent_kit](https://databricks-sdk-py.readthedocs.io/en/latest/workspace/agentkit/agent_kit.html) workspace-level service.
|
|
17
|
+
* Add `create_materialization_set()`, `delete_materialization_set()`, `get_materialization_set()` and `update_materialization_set()` methods for [w.feature_engineering](https://databricks-sdk-py.readthedocs.io/en/latest/workspace/ml/feature_engineering.html) workspace-level service.
|
|
18
|
+
* Add `notification_settings` field for `databricks.sdk.service.dataquality.AnomalyDetectionConfig`.
|
|
19
|
+
* Add `destination_ids` and `destination_user_ids` fields for `databricks.sdk.service.dataquality.NotificationDestination`.
|
|
20
|
+
* Add `on_unhealthy_table` field for `databricks.sdk.service.dataquality.NotificationSettings`.
|
|
21
|
+
* Add `file_output` field for `databricks.sdk.service.jobs.SqlOutput`.
|
|
22
|
+
* Add `materialization_set_id` field for `databricks.sdk.service.ml.MaterializedFeature`.
|
|
23
|
+
* Add `delete_rows` field for `databricks.sdk.service.ml.PurgeFeatureEntitiesRequest`.
|
|
24
|
+
* Add `full_refresh_selection_by_path` and `refresh_selection_by_path` fields for `databricks.sdk.service.pipelines.StartUpdate`.
|
|
25
|
+
* Add `full_refresh_selection_by_path` and `refresh_selection_by_path` fields for `databricks.sdk.service.pipelines.UpdateInfo`.
|
|
26
|
+
* Add `metadata` field for `databricks.sdk.service.sandbox.Sandbox`.
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
* Require certifi 2026.10.19 or newer to use an updated CA trust bundle.
|
|
30
|
+
|
|
3
31
|
## Release v0.156.0 (2026-10-22)
|
|
4
32
|
|
|
5
33
|
### API Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: databricks-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.158.0
|
|
4
4
|
Summary: Databricks SDK for Python (Beta)
|
|
5
5
|
Project-URL: Documentation, https://databricks-sdk-py.readthedocs.io
|
|
6
6
|
Keywords: databricks,sdk
|
|
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
|
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: NOTICE
|
|
21
|
+
Requires-Dist: certifi>=2026.10.19
|
|
21
22
|
Requires-Dist: requests<3,>=2.28.1
|
|
22
23
|
Requires-Dist: google-auth~=2.0
|
|
23
24
|
Requires-Dist: protobuf!=5.26.*,!=5.27.*,!=5.28.*,!=5.29.0,!=5.29.1,!=5.29.2,!=5.29.3,!=5.29.4,!=6.30.0,!=6.30.1,!=6.31.0,<8.0,>=4.25.8
|
|
@@ -1226,6 +1226,13 @@ class DatabricksCliTokenSource(CliTokenSource):
|
|
|
1226
1226
|
cmd += ["--account-id", cfg.account_id]
|
|
1227
1227
|
return cmd
|
|
1228
1228
|
|
|
1229
|
+
# Files at least this large are assumed to be the Go CLI without reading them.
|
|
1230
|
+
_CLI_MIN_SIZE = 1024 * 1024
|
|
1231
|
+
|
|
1232
|
+
# Identifies the legacy CLI's pip entry point script. The imported name is omitted
|
|
1233
|
+
# because it changed from `cli` to `main` in 0.17.8.
|
|
1234
|
+
_LEGACY_CLI_MARKER = b"from databricks_cli.cli import "
|
|
1235
|
+
|
|
1229
1236
|
@staticmethod
|
|
1230
1237
|
def _find_executable(name) -> str:
|
|
1231
1238
|
err = FileNotFoundError("Most likely the Databricks CLI is not installed")
|
|
@@ -1234,12 +1241,14 @@ class DatabricksCliTokenSource(CliTokenSource):
|
|
|
1234
1241
|
if not path.is_file():
|
|
1235
1242
|
continue
|
|
1236
1243
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1244
|
+
if path.stat().st_size < DatabricksCliTokenSource._CLI_MIN_SIZE:
|
|
1245
|
+
try:
|
|
1246
|
+
content = path.read_bytes()
|
|
1247
|
+
except OSError:
|
|
1248
|
+
continue
|
|
1249
|
+
if DatabricksCliTokenSource._LEGACY_CLI_MARKER in content:
|
|
1250
|
+
err = FileNotFoundError("Databricks CLI version <0.100.0 detected")
|
|
1251
|
+
continue
|
|
1243
1252
|
|
|
1244
1253
|
return str(path)
|
|
1245
1254
|
|
|
File without changes
|
|
@@ -850,25 +850,6 @@ class _PresignedUrlRequestBuilder:
|
|
|
850
850
|
)
|
|
851
851
|
return CreateDownloadUrlResponse.from_dict(response)
|
|
852
852
|
|
|
853
|
-
def build_abort_url(self, path: str, session_token: str, expire_time: str) -> _PresignedUrl:
|
|
854
|
-
"""Fetches a presigned URL for aborting a multipart upload."""
|
|
855
|
-
body = {
|
|
856
|
-
"path": path,
|
|
857
|
-
"session_token": session_token,
|
|
858
|
-
"expire_time": expire_time,
|
|
859
|
-
}
|
|
860
|
-
response = self._api.do(
|
|
861
|
-
"POST",
|
|
862
|
-
url=f"{self._hostname}/api/2.0/fs/create-abort-upload-url",
|
|
863
|
-
headers={"Content-Type": "application/json"},
|
|
864
|
-
body=body,
|
|
865
|
-
)
|
|
866
|
-
abort_node = response["abort_upload_url"]
|
|
867
|
-
headers: dict[str, str] = {"Content-Type": "application/octet-stream"}
|
|
868
|
-
for h in abort_node.get("headers", []):
|
|
869
|
-
headers[h["name"]] = h["value"]
|
|
870
|
-
return _PresignedUrl(url=abort_node["url"], headers=headers)
|
|
871
|
-
|
|
872
853
|
|
|
873
854
|
class _StorageProxyRequestBuilder:
|
|
874
855
|
"""Builds direct upload requests to the storage proxy.
|
|
@@ -930,21 +911,6 @@ class _StorageProxyRequestBuilder:
|
|
|
930
911
|
headers={},
|
|
931
912
|
)
|
|
932
913
|
|
|
933
|
-
def build_abort_url(self, path: str, session_token: str, expire_time: str) -> _PresignedUrl:
|
|
934
|
-
"""Builds a URL for aborting an upload directly on the storage proxy."""
|
|
935
|
-
escaped = _escape_multi_segment_path_parameter(path)
|
|
936
|
-
base = f"{self._hostname}/api/2.0/fs/files{escaped}"
|
|
937
|
-
query = parse.urlencode(
|
|
938
|
-
{
|
|
939
|
-
"action": "abort-upload",
|
|
940
|
-
"session_token": session_token,
|
|
941
|
-
}
|
|
942
|
-
)
|
|
943
|
-
return _PresignedUrl(
|
|
944
|
-
url=f"{base}?{query}",
|
|
945
|
-
headers={"Content-Type": "application/json"},
|
|
946
|
-
)
|
|
947
|
-
|
|
948
914
|
|
|
949
915
|
class FilesExt(files.FilesAPI):
|
|
950
916
|
__doc__ = files.FilesAPI.__doc__
|
|
@@ -990,8 +956,8 @@ class FilesExt(files.FilesAPI):
|
|
|
990
956
|
self._cached_cloud_provider_session: Optional[requests.Session] = None
|
|
991
957
|
self._cloud_provider_session_lock = Lock()
|
|
992
958
|
self._dp_hostname_available: Optional[bool] = None
|
|
993
|
-
self.
|
|
994
|
-
self.
|
|
959
|
+
self._cached_authenticated_session: Optional[requests.Session] = None
|
|
960
|
+
self._authenticated_session_lock = Lock()
|
|
995
961
|
self._cached_endpoints: Optional[list[_Endpoint]] = None
|
|
996
962
|
self._endpoints_lock = Lock()
|
|
997
963
|
|
|
@@ -1185,17 +1151,16 @@ class FilesExt(files.FilesAPI):
|
|
|
1185
1151
|
except Exception:
|
|
1186
1152
|
return False
|
|
1187
1153
|
|
|
1188
|
-
def
|
|
1189
|
-
"""Returns an HTTP session with SDK auth for
|
|
1154
|
+
def _authenticated_session(self) -> requests.Session:
|
|
1155
|
+
"""Returns an HTTP session with SDK auth for direct Files API data requests.
|
|
1190
1156
|
|
|
1191
|
-
Unlike _cloud_provider_session (which has no auth), this session
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
first call and cached for reuse.
|
|
1157
|
+
Unlike _cloud_provider_session (which has no auth), this session includes the SDK authentication
|
|
1158
|
+
callback so that every request carries valid credentials. The session is created on first call and
|
|
1159
|
+
cached for reuse.
|
|
1195
1160
|
"""
|
|
1196
|
-
with self.
|
|
1197
|
-
if self.
|
|
1198
|
-
return self.
|
|
1161
|
+
with self._authenticated_session_lock:
|
|
1162
|
+
if self._cached_authenticated_session is not None:
|
|
1163
|
+
return self._cached_authenticated_session
|
|
1199
1164
|
session = requests.Session()
|
|
1200
1165
|
config = self._config
|
|
1201
1166
|
|
|
@@ -1212,7 +1177,7 @@ class FilesExt(files.FilesAPI):
|
|
|
1212
1177
|
)
|
|
1213
1178
|
session.mount("https://", http_adapter)
|
|
1214
1179
|
session.mount("http://", http_adapter)
|
|
1215
|
-
self.
|
|
1180
|
+
self._cached_authenticated_session = session
|
|
1216
1181
|
return session
|
|
1217
1182
|
|
|
1218
1183
|
def download(
|
|
@@ -1339,8 +1304,12 @@ class FilesExt(files.FilesAPI):
|
|
|
1339
1304
|
file_info = self.get_metadata(remote_path)
|
|
1340
1305
|
file_size = file_info.content_length
|
|
1341
1306
|
last_modified = file_info.last_modified
|
|
1342
|
-
# If the file is smaller than the threshold, do not use parallel download.
|
|
1343
|
-
|
|
1307
|
+
# If the file is smaller than the threshold, or fits in one range, do not use parallel download. A range
|
|
1308
|
+
# covering the whole file may also be answered with 200 instead of 206.
|
|
1309
|
+
if file_size <= max(
|
|
1310
|
+
self._config.files_ext_parallel_download_min_file_size,
|
|
1311
|
+
self._config.files_ext_parallel_download_default_part_size,
|
|
1312
|
+
):
|
|
1344
1313
|
self._sequential_download_to_file(destination, remote_path, last_modified)
|
|
1345
1314
|
return
|
|
1346
1315
|
part_size = self._config.files_ext_parallel_download_default_part_size
|
|
@@ -1422,13 +1391,16 @@ class FilesExt(files.FilesAPI):
|
|
|
1422
1391
|
url_distributor.invalidate_url(version)
|
|
1423
1392
|
retry_count += 1
|
|
1424
1393
|
continue
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1394
|
+
# Only a 206 carries the requested range; a 200 carries the whole file, which must not be written
|
|
1395
|
+
# at this range's offset.
|
|
1396
|
+
if raw_resp.status_code != 206:
|
|
1397
|
+
message = f"Received {raw_resp.status_code} from presigned URL for {additional_headers['Range']}"
|
|
1398
|
+
if not any_success.is_set():
|
|
1399
|
+
raise FallbackToDownloadUsingFilesApi(message)
|
|
1400
|
+
if raw_resp.ok:
|
|
1401
|
+
raise ValueError(message)
|
|
1402
|
+
raise _error_mapper(raw_resp, {"message": message})
|
|
1430
1403
|
|
|
1431
|
-
raw_resp.raise_for_status()
|
|
1432
1404
|
any_success.set()
|
|
1433
1405
|
return BytesIO(raw_resp.content)
|
|
1434
1406
|
raise ValueError("Exceeded maximum retries for downloading with presigned URL: URL expired too many times")
|
|
@@ -1451,11 +1423,17 @@ class FilesExt(files.FilesAPI):
|
|
|
1451
1423
|
def download_chunk(additional_headers: dict[str, str]) -> BinaryIO:
|
|
1452
1424
|
raw_response: dict = self._api.do(
|
|
1453
1425
|
method="GET",
|
|
1454
|
-
path=f"/api/2.0/fs/files{remote_path}",
|
|
1426
|
+
path=f"/api/2.0/fs/files{_escape_multi_segment_path_parameter(remote_path)}",
|
|
1455
1427
|
headers=additional_headers,
|
|
1456
1428
|
raw=True,
|
|
1457
1429
|
)
|
|
1458
|
-
|
|
1430
|
+
contents = raw_response["contents"]
|
|
1431
|
+
status = contents._response.status_code
|
|
1432
|
+
if status != 206:
|
|
1433
|
+
# Only a 206 carries the requested range; anything else must not be written at its offset.
|
|
1434
|
+
contents.close()
|
|
1435
|
+
raise ValueError(f"Received {status} from the Files API for {additional_headers['Range']}")
|
|
1436
|
+
return contents
|
|
1459
1437
|
|
|
1460
1438
|
self._do_parallel_download(remote_path, destination, parallelism, download_chunk)
|
|
1461
1439
|
|
|
@@ -2314,7 +2292,7 @@ class FilesExt(files.FilesAPI):
|
|
|
2314
2292
|
"""
|
|
2315
2293
|
Checks if response matches one of the known "URL expired" responses from the cloud storage providers.
|
|
2316
2294
|
"""
|
|
2317
|
-
if response.status_code
|
|
2295
|
+
if response.status_code not in (400, 403):
|
|
2318
2296
|
return False
|
|
2319
2297
|
|
|
2320
2298
|
try:
|
|
@@ -2326,6 +2304,10 @@ class FilesExt(files.FilesAPI):
|
|
|
2326
2304
|
if code is None:
|
|
2327
2305
|
return False
|
|
2328
2306
|
|
|
2307
|
+
if code.text == "ExpiredToken":
|
|
2308
|
+
# GCP, and AWS when the URL was signed with temporary credentials that have since expired.
|
|
2309
|
+
return True
|
|
2310
|
+
|
|
2329
2311
|
if code.text == "AuthenticationFailed":
|
|
2330
2312
|
# Azure
|
|
2331
2313
|
details = xml_root.find("AuthenticationErrorDetail")
|
|
@@ -2587,37 +2569,30 @@ class FilesExt(files.FilesAPI):
|
|
|
2587
2569
|
)
|
|
2588
2570
|
|
|
2589
2571
|
def _abort_multipart_upload(self, ctx: _UploadContext, session_token: str) -> None:
|
|
2590
|
-
"""Aborts ongoing multipart upload session to clean up incomplete file.
|
|
2591
|
-
builder = self._create_request_builder()
|
|
2592
|
-
abort_info = builder.build_abort_url(ctx.target_path, session_token, self._get_upload_url_expire_time())
|
|
2593
|
-
|
|
2594
|
-
def perform() -> requests.Response:
|
|
2595
|
-
return self._cloud_provider_session().request(
|
|
2596
|
-
"DELETE",
|
|
2597
|
-
abort_info.url,
|
|
2598
|
-
headers=abort_info.headers,
|
|
2599
|
-
data=b"",
|
|
2600
|
-
timeout=self._config.files_ext_network_transfer_inactivity_timeout_seconds,
|
|
2601
|
-
)
|
|
2572
|
+
"""Aborts ongoing multipart upload session to clean up incomplete file.
|
|
2602
2573
|
|
|
2603
|
-
|
|
2574
|
+
Best-effort, so sent once, without the API client's retries.
|
|
2575
|
+
"""
|
|
2576
|
+
escaped = _escape_multi_segment_path_parameter(ctx.target_path)
|
|
2577
|
+
abort_response = self._authenticated_session().request(
|
|
2578
|
+
"POST",
|
|
2579
|
+
f"{self._get_hostname()}/api/2.0/fs/files{escaped}",
|
|
2580
|
+
params={"action": "abort-upload", "upload_type": "multipart", "session_token": session_token},
|
|
2581
|
+
timeout=self._config.files_ext_network_transfer_inactivity_timeout_seconds,
|
|
2582
|
+
)
|
|
2604
2583
|
|
|
2605
|
-
if abort_response.
|
|
2584
|
+
if not abort_response.ok:
|
|
2606
2585
|
raise ValueError(abort_response)
|
|
2607
2586
|
|
|
2608
2587
|
def _abort_resumable_upload(self, resumable_upload_url: str, headers: dict[str, str]) -> None:
|
|
2609
|
-
"""Aborts ongoing resumable upload session to clean up incomplete file."""
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
timeout=self._config.files_ext_network_transfer_inactivity_timeout_seconds,
|
|
2618
|
-
)
|
|
2619
|
-
|
|
2620
|
-
abort_response = self._retry_cloud_idempotent_operation(perform)
|
|
2588
|
+
"""Aborts ongoing resumable upload session to clean up incomplete file. Best-effort, so sent once."""
|
|
2589
|
+
abort_response = self._cloud_provider_session().request(
|
|
2590
|
+
"DELETE",
|
|
2591
|
+
resumable_upload_url,
|
|
2592
|
+
headers=headers,
|
|
2593
|
+
data=b"",
|
|
2594
|
+
timeout=self._config.files_ext_network_transfer_inactivity_timeout_seconds,
|
|
2595
|
+
)
|
|
2621
2596
|
|
|
2622
2597
|
if abort_response.status_code not in (200, 201):
|
|
2623
2598
|
raise ValueError(abort_response)
|
|
@@ -2631,7 +2606,7 @@ class FilesExt(files.FilesAPI):
|
|
|
2631
2606
|
The session is created on first call and cached for reuse.
|
|
2632
2607
|
"""
|
|
2633
2608
|
if self._config.experimental_files_ext_enable_storage_proxy and self._dp_hostname_available:
|
|
2634
|
-
return self.
|
|
2609
|
+
return self._authenticated_session()
|
|
2635
2610
|
with self._cloud_provider_session_lock:
|
|
2636
2611
|
if self._cached_cloud_provider_session is None:
|
|
2637
2612
|
session = requests.Session()
|
|
File without changes
|