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.
Files changed (116) hide show
  1. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/CHANGELOG.md +28 -0
  2. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/PKG-INFO +2 -1
  3. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/credentials_provider.py +15 -6
  4. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/platform.py +0 -0
  5. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files.py +59 -84
  6. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/__init__.py +0 -0
  7. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentkit.py +520 -2
  8. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/billing.py +218 -0
  9. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundle.py +0 -0
  10. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/catalog.py +6 -6
  11. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/compute.py +4 -1
  12. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/database.py +54 -4
  13. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataquality.py +58 -3
  14. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iamv2.py +11 -7
  15. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/jobs.py +178 -1
  16. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/ml.py +673 -5
  17. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/pipelines.py +26 -0
  18. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sandbox.py +36 -0
  19. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/serving.py +2 -1
  20. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sharing.py +3 -2
  21. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sql.py +2 -2
  22. databricks_sdk-0.158.0/databricks/sdk/version.py +1 -0
  23. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/PKG-INFO +2 -1
  24. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/requires.txt +1 -0
  25. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/pyproject.toml +1 -0
  26. databricks_sdk-0.156.0/databricks/sdk/version.py +0 -1
  27. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/CONTRIBUTING.md +0 -0
  28. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/DCO +0 -0
  29. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/LICENSE +0 -0
  30. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/MANIFEST.in +0 -0
  31. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/Makefile +0 -0
  32. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/NOTICE +0 -0
  33. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/README.md +0 -0
  34. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/SECURITY.md +0 -0
  35. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/__init__.py +0 -0
  36. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/__init__.py +0 -0
  37. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_base_client.py +0 -0
  38. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_property.py +0 -0
  39. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/__init__.py +0 -0
  40. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2.py +0 -0
  41. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2_grpc.py +0 -0
  42. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/__init__.py +0 -0
  43. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/default_widgets_utils.py +0 -0
  44. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/ipywidgets_utils.py +0 -0
  45. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/azure.py +0 -0
  46. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/casing.py +0 -0
  47. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/client_types.py +0 -0
  48. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/clock.py +0 -0
  49. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/lro.py +0 -0
  50. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/__init__.py +0 -0
  51. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/fieldmask.py +0 -0
  52. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/config.py +0 -0
  53. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/core.py +0 -0
  54. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/data_plane.py +0 -0
  55. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/dbutils.py +0 -0
  56. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/environments.py +0 -0
  57. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/__init__.py +0 -0
  58. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/base.py +0 -0
  59. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/customizer.py +0 -0
  60. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/deserializer.py +0 -0
  61. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/details.py +0 -0
  62. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/mapper.py +0 -0
  63. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/overrides.py +0 -0
  64. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/parser.py +0 -0
  65. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/private_link.py +0 -0
  66. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/sdk.py +0 -0
  67. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/__init__.py +0 -0
  68. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/round_trip_logger.py +0 -0
  69. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/__init__.py +0 -0
  70. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/_grpc_transport.py +0 -0
  71. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/compute.py +0 -0
  72. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files_utils.py +0 -0
  73. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/jobs.py +0 -0
  74. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/open_ai_client.py +0 -0
  75. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sandbox.py +0 -0
  76. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sharing.py +0 -0
  77. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/workspace.py +0 -0
  78. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oauth.py +0 -0
  79. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc.py +0 -0
  80. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc_token_supplier.py +0 -0
  81. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/py.typed +0 -0
  82. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/retries.py +0 -0
  83. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/__init__.py +0 -0
  84. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/dbutils_stub.py +0 -0
  85. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/_internal.py +0 -0
  86. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentbricks.py +0 -0
  87. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aifunctions.py +0 -0
  88. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aisearch.py +0 -0
  89. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/apps.py +0 -0
  90. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundledeployments.py +0 -0
  91. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/cleanrooms.py +0 -0
  92. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dashboards.py +0 -0
  93. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataclassification.py +0 -0
  94. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/disasterrecovery.py +0 -0
  95. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/domains.py +0 -0
  96. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/environments.py +0 -0
  97. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/files.py +0 -0
  98. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iam.py +0 -0
  99. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/knowledgeassistants.py +0 -0
  100. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/marketplace.py +0 -0
  101. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/networking.py +0 -0
  102. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/oauth2.py +0 -0
  103. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/postgres.py +0 -0
  104. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/provisioning.py +0 -0
  105. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/qualitymonitorv2.py +0 -0
  106. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settings.py +0 -0
  107. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settingsv2.py +0 -0
  108. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/supervisoragents.py +0 -0
  109. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/tags.py +0 -0
  110. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/vectorsearch.py +0 -0
  111. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/service/workspace.py +0 -0
  112. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks/sdk/useragent.py +0 -0
  113. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/SOURCES.txt +0 -0
  114. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/dependency_links.txt +0 -0
  115. {databricks_sdk-0.156.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/top_level.txt +0 -0
  116. {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.156.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
- # The new Databricks CLI is a single binary with size > 1MB.
1238
- # We use the size as a signal to determine which Databricks CLI is installed.
1239
- stat = path.stat()
1240
- if stat.st_size < (1024 * 1024):
1241
- err = FileNotFoundError("Databricks CLI version <0.100.0 detected")
1242
- continue
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
 
@@ -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._cached_storage_proxy_session: Optional[requests.Session] = None
994
- self._storage_proxy_session_lock = Lock()
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 _create_storage_proxy_session(self) -> requests.Session:
1189
- """Returns an HTTP session with SDK auth for storage-proxy requests.
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
- includes the SDK authentication callback so that every request to the
1193
- storage proxy carries valid credentials. The session is created on
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._storage_proxy_session_lock:
1197
- if self._cached_storage_proxy_session is not None:
1198
- return self._cached_storage_proxy_session
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._cached_storage_proxy_session = session
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
- if file_size <= self._config.files_ext_parallel_download_min_file_size:
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
- elif raw_resp.status_code == 403 and not any_success.is_set():
1426
- raise FallbackToDownloadUsingFilesApi("Received 403 Forbidden from presigned URL")
1427
- elif not any_success.is_set():
1428
- # For other errors, we raise a retryable exception to trigger retry logic.
1429
- raise FallbackToDownloadUsingFilesApi(f"Received {raw_resp.status_code} from presigned URL")
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
- return raw_response["contents"]
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 != 403:
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
- abort_response = self._retry_cloud_idempotent_operation(perform)
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.status_code not in (200, 201):
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
- def perform() -> requests.Response:
2612
- return self._cloud_provider_session().request(
2613
- "DELETE",
2614
- resumable_upload_url,
2615
- headers=headers,
2616
- data=b"",
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._create_storage_proxy_session()
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()