databricks-sdk 0.157.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.157.0 → databricks_sdk-0.158.0}/CHANGELOG.md +10 -0
  2. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/PKG-INFO +1 -1
  3. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/credentials_provider.py +15 -6
  4. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/platform.py +0 -0
  5. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files.py +59 -84
  6. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/__init__.py +0 -0
  7. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/billing.py +218 -0
  8. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundle.py +0 -0
  9. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/catalog.py +13 -11
  10. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/database.py +54 -4
  11. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iamv2.py +11 -7
  12. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/ml.py +3 -2
  13. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/serving.py +2 -1
  14. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sql.py +2 -2
  15. databricks_sdk-0.158.0/databricks/sdk/version.py +1 -0
  16. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/PKG-INFO +1 -1
  17. databricks_sdk-0.157.0/databricks/sdk/version.py +0 -1
  18. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/CONTRIBUTING.md +0 -0
  19. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/DCO +0 -0
  20. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/LICENSE +0 -0
  21. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/MANIFEST.in +0 -0
  22. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/Makefile +0 -0
  23. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/NOTICE +0 -0
  24. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/README.md +0 -0
  25. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/SECURITY.md +0 -0
  26. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/__init__.py +0 -0
  27. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/__init__.py +0 -0
  28. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_base_client.py +0 -0
  29. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_property.py +0 -0
  30. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/__init__.py +0 -0
  31. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2.py +0 -0
  32. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_sandbox_grpc/sandbox_service_pb2_grpc.py +0 -0
  33. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/__init__.py +0 -0
  34. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/default_widgets_utils.py +0 -0
  35. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/_widgets/ipywidgets_utils.py +0 -0
  36. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/azure.py +0 -0
  37. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/casing.py +0 -0
  38. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/client_types.py +0 -0
  39. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/clock.py +0 -0
  40. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/common/lro.py +0 -0
  41. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/__init__.py +0 -0
  42. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/common/types/fieldmask.py +0 -0
  43. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/config.py +0 -0
  44. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/core.py +0 -0
  45. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/data_plane.py +0 -0
  46. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/dbutils.py +0 -0
  47. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/environments.py +0 -0
  48. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/__init__.py +0 -0
  49. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/base.py +0 -0
  50. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/customizer.py +0 -0
  51. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/deserializer.py +0 -0
  52. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/details.py +0 -0
  53. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/mapper.py +0 -0
  54. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/overrides.py +0 -0
  55. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/parser.py +0 -0
  56. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/private_link.py +0 -0
  57. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/errors/sdk.py +0 -0
  58. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/__init__.py +0 -0
  59. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/logger/round_trip_logger.py +0 -0
  60. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/__init__.py +0 -0
  61. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/_grpc_transport.py +0 -0
  62. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/compute.py +0 -0
  63. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/files_utils.py +0 -0
  64. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/jobs.py +0 -0
  65. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/open_ai_client.py +0 -0
  66. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sandbox.py +0 -0
  67. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/sharing.py +0 -0
  68. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/mixins/workspace.py +0 -0
  69. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/oauth.py +0 -0
  70. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc.py +0 -0
  71. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/oidc_token_supplier.py +0 -0
  72. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/py.typed +0 -0
  73. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/retries.py +0 -0
  74. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/__init__.py +0 -0
  75. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/runtime/dbutils_stub.py +0 -0
  76. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/_internal.py +0 -0
  77. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentbricks.py +0 -0
  78. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/agentkit.py +0 -0
  79. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aifunctions.py +0 -0
  80. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/aisearch.py +0 -0
  81. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/apps.py +0 -0
  82. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/bundledeployments.py +0 -0
  83. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/cleanrooms.py +0 -0
  84. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/compute.py +0 -0
  85. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dashboards.py +0 -0
  86. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataclassification.py +0 -0
  87. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/dataquality.py +0 -0
  88. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/disasterrecovery.py +0 -0
  89. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/domains.py +0 -0
  90. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/environments.py +0 -0
  91. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/files.py +0 -0
  92. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/iam.py +0 -0
  93. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/jobs.py +0 -0
  94. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/knowledgeassistants.py +0 -0
  95. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/marketplace.py +0 -0
  96. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/networking.py +0 -0
  97. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/oauth2.py +0 -0
  98. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/pipelines.py +0 -0
  99. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/postgres.py +0 -0
  100. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/provisioning.py +0 -0
  101. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/qualitymonitorv2.py +0 -0
  102. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sandbox.py +0 -0
  103. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settings.py +0 -0
  104. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/settingsv2.py +0 -0
  105. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/sharing.py +0 -0
  106. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/supervisoragents.py +0 -0
  107. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/tags.py +0 -0
  108. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/vectorsearch.py +0 -0
  109. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/service/workspace.py +0 -0
  110. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks/sdk/useragent.py +0 -0
  111. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/SOURCES.txt +0 -0
  112. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/dependency_links.txt +0 -0
  113. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/requires.txt +0 -0
  114. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/databricks_sdk.egg-info/top_level.txt +0 -0
  115. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/pyproject.toml +0 -0
  116. {databricks_sdk-0.157.0 → databricks_sdk-0.158.0}/setup.cfg +0 -0
@@ -1,5 +1,15 @@
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
+
3
13
  ## Release v0.157.0 (2026-10-24)
4
14
 
5
15
  ### API Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-sdk
3
- Version: 0.157.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
@@ -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()
@@ -187,6 +187,76 @@ class AlertConfigurationTriggerType(Enum):
187
187
  CUMULATIVE_SPENDING_EXCEEDED = "CUMULATIVE_SPENDING_EXCEEDED"
188
188
 
189
189
 
190
+ @dataclass
191
+ class AlertSpendStatus:
192
+ """The calling user's spend against a single blocking threshold. Values may be omitted if live
193
+ spend or threshold data is temporarily unavailable."""
194
+
195
+ percent_consumed: Optional[str] = None
196
+ """Percentage of the threshold consumed, as a decimal string rounded down to one decimal place (for
197
+ example, "68.4" means 68.4%). May exceed 100 when spend is over the threshold. Returns "0.0" if
198
+ there's no spend, and is omitted if the threshold is $0. For shared thresholds, this is the only
199
+ value returned; the absolute ``spend`` and ``threshold`` dollar amounts are withheld."""
200
+
201
+ scope_type: Optional[AlertConfigurationScopeType] = None
202
+ """Whether the threshold is evaluated per individual user
203
+ (``ALERT_CONFIGURATION_SCOPE_TYPE_PER_USER``) or on the shared spend across all users
204
+ (``ALERT_CONFIGURATION_SCOPE_TYPE_SHARED``)."""
205
+
206
+ spend: Optional[str] = None
207
+ """The calling user's live spend against this threshold, in USD at list price. Returns "0" if the
208
+ caller has no usage. Omitted for shared thresholds, where only ``percent_consumed`` is returned."""
209
+
210
+ threshold: Optional[str] = None
211
+ """The calling user's effective threshold, in USD at list price. The effective threshold is the
212
+ actual threshold amount that applies to the user, after adjusting for any user or group
213
+ overrides. Omitted for shared thresholds, where only ``percent_consumed`` is returned."""
214
+
215
+ time_period: Optional[AlertConfigurationTimePeriod] = None
216
+ """The time window the threshold applies to, for example ``MONTH``."""
217
+
218
+ def as_dict(self) -> dict:
219
+ """Serializes the AlertSpendStatus into a dictionary suitable for use as a JSON request body."""
220
+ body = {}
221
+ if self.percent_consumed is not None:
222
+ body["percent_consumed"] = self.percent_consumed
223
+ if self.scope_type is not None:
224
+ body["scope_type"] = self.scope_type.value
225
+ if self.spend is not None:
226
+ body["spend"] = self.spend
227
+ if self.threshold is not None:
228
+ body["threshold"] = self.threshold
229
+ if self.time_period is not None:
230
+ body["time_period"] = self.time_period.value
231
+ return body
232
+
233
+ def as_shallow_dict(self) -> dict:
234
+ """Serializes the AlertSpendStatus into a shallow dictionary of its immediate attributes."""
235
+ body = {}
236
+ if self.percent_consumed is not None:
237
+ body["percent_consumed"] = self.percent_consumed
238
+ if self.scope_type is not None:
239
+ body["scope_type"] = self.scope_type
240
+ if self.spend is not None:
241
+ body["spend"] = self.spend
242
+ if self.threshold is not None:
243
+ body["threshold"] = self.threshold
244
+ if self.time_period is not None:
245
+ body["time_period"] = self.time_period
246
+ return body
247
+
248
+ @classmethod
249
+ def from_dict(cls, d: Dict[str, Any]) -> AlertSpendStatus:
250
+ """Deserializes the AlertSpendStatus from a dictionary."""
251
+ return cls(
252
+ percent_consumed=d.get("percent_consumed", None),
253
+ scope_type=_enum(d, "scope_type", AlertConfigurationScopeType),
254
+ spend=d.get("spend", None),
255
+ threshold=d.get("threshold", None),
256
+ time_period=_enum(d, "time_period", AlertConfigurationTimePeriod),
257
+ )
258
+
259
+
190
260
  @dataclass
191
261
  class BudgetConfiguration:
192
262
  account_id: Optional[str] = None
@@ -491,6 +561,45 @@ class BudgetResourceType(Enum):
491
561
  BUDGET_RESOURCE_TYPE_UNITY_AI_GATEWAY = "BUDGET_RESOURCE_TYPE_UNITY_AI_GATEWAY"
492
562
 
493
563
 
564
+ @dataclass
565
+ class BudgetSpendStatus:
566
+ """The calling user's live spend against the blocking thresholds of a single budget."""
567
+
568
+ alert_spend_statuses: Optional[List[AlertSpendStatus]] = None
569
+ """The calling user's spend against each blocking threshold on this budget, one entry per scope
570
+ type and time period. A blocking threshold is a threshold that, once reached, blocks further
571
+ requests through Unity Gateway."""
572
+
573
+ name: Optional[str] = None
574
+ """The resource name of this budget spend status, of the form
575
+ ``accounts/{account_id}/budget-spend-statuses/{budget_id}``."""
576
+
577
+ def as_dict(self) -> dict:
578
+ """Serializes the BudgetSpendStatus into a dictionary suitable for use as a JSON request body."""
579
+ body = {}
580
+ if self.alert_spend_statuses:
581
+ body["alert_spend_statuses"] = [v.as_dict() for v in self.alert_spend_statuses]
582
+ if self.name is not None:
583
+ body["name"] = self.name
584
+ return body
585
+
586
+ def as_shallow_dict(self) -> dict:
587
+ """Serializes the BudgetSpendStatus into a shallow dictionary of its immediate attributes."""
588
+ body = {}
589
+ if self.alert_spend_statuses:
590
+ body["alert_spend_statuses"] = self.alert_spend_statuses
591
+ if self.name is not None:
592
+ body["name"] = self.name
593
+ return body
594
+
595
+ @classmethod
596
+ def from_dict(cls, d: Dict[str, Any]) -> BudgetSpendStatus:
597
+ """Deserializes the BudgetSpendStatus from a dictionary."""
598
+ return cls(
599
+ alert_spend_statuses=_repeated_dict(d, "alert_spend_statuses", AlertSpendStatus), name=d.get("name", None)
600
+ )
601
+
602
+
494
603
  @dataclass
495
604
  class CreateBillingUsageDashboardResponse:
496
605
  dashboard_id: Optional[str] = None
@@ -1156,6 +1265,45 @@ class ListBudgetPoliciesResponse:
1156
1265
  )
1157
1266
 
1158
1267
 
1268
+ @dataclass
1269
+ class ListBudgetSpendStatusesResponse:
1270
+ """Response for ListBudgetSpendStatuses."""
1271
+
1272
+ budget_spend_statuses: Optional[List[BudgetSpendStatus]] = None
1273
+ """The calling user's spend status, one entry per returned budget."""
1274
+
1275
+ next_page_token: Optional[str] = None
1276
+ """Token to pass as ``page_token`` to retrieve the next page. Absent when there are no more
1277
+ results. A page can contain fewer results than ``page_size``, or none, while this token is still
1278
+ set, so keep paging until it's absent."""
1279
+
1280
+ def as_dict(self) -> dict:
1281
+ """Serializes the ListBudgetSpendStatusesResponse into a dictionary suitable for use as a JSON request body."""
1282
+ body = {}
1283
+ if self.budget_spend_statuses:
1284
+ body["budget_spend_statuses"] = [v.as_dict() for v in self.budget_spend_statuses]
1285
+ if self.next_page_token is not None:
1286
+ body["next_page_token"] = self.next_page_token
1287
+ return body
1288
+
1289
+ def as_shallow_dict(self) -> dict:
1290
+ """Serializes the ListBudgetSpendStatusesResponse into a shallow dictionary of its immediate attributes."""
1291
+ body = {}
1292
+ if self.budget_spend_statuses:
1293
+ body["budget_spend_statuses"] = self.budget_spend_statuses
1294
+ if self.next_page_token is not None:
1295
+ body["next_page_token"] = self.next_page_token
1296
+ return body
1297
+
1298
+ @classmethod
1299
+ def from_dict(cls, d: Dict[str, Any]) -> ListBudgetSpendStatusesResponse:
1300
+ """Deserializes the ListBudgetSpendStatusesResponse from a dictionary."""
1301
+ return cls(
1302
+ budget_spend_statuses=_repeated_dict(d, "budget_spend_statuses", BudgetSpendStatus),
1303
+ next_page_token=d.get("next_page_token", None),
1304
+ )
1305
+
1306
+
1159
1307
  class LogDeliveryConfigStatus(Enum):
1160
1308
  """Log Delivery Status
1161
1309
 
@@ -1949,6 +2097,27 @@ class BudgetsAPI:
1949
2097
  res = self._api.do("GET", f"/api/2.1/accounts/{self._api.account_id}/budgets/{budget_id}", headers=headers)
1950
2098
  return GetBudgetConfigurationResponse.from_dict(res)
1951
2099
 
2100
+ def get_budget_spend_status(self, name: str) -> BudgetSpendStatus:
2101
+ """Get the calling user's live spend against the blocking thresholds of a single Unity Gateway budget.
2102
+ Any account user can call this for their own spend. Spend is reported in USD at list price and is
2103
+ near-real-time, so it may differ slightly from billing data. Returns NOT_FOUND if the budget doesn't
2104
+ exist or has no blocking thresholds.
2105
+
2106
+ :param name: str
2107
+ The resource name of the budget spend status to read, of the form
2108
+ ``accounts/{account_id}/budget-spend-statuses/{budget_id}``. A ``budget_id`` that isn't a valid UUID
2109
+ fails with INVALID_PARAMETER_VALUE.
2110
+
2111
+ :returns: :class:`BudgetSpendStatus`
2112
+ """
2113
+
2114
+ headers = {
2115
+ "Accept": "application/json",
2116
+ }
2117
+
2118
+ res = self._api.do("GET", f"/api/2.1/{name}", headers=headers)
2119
+ return BudgetSpendStatus.from_dict(res)
2120
+
1952
2121
  def list(self, *, page_token: Optional[str] = None) -> Iterator[BudgetConfiguration]:
1953
2122
  """Gets all budgets associated with this account.
1954
2123
 
@@ -1977,6 +2146,55 @@ class BudgetsAPI:
1977
2146
  return
1978
2147
  query["page_token"] = json["next_page_token"]
1979
2148
 
2149
+ def list_budget_spend_statuses(
2150
+ self,
2151
+ parent: str,
2152
+ *,
2153
+ filter: Optional[str] = None,
2154
+ page_size: Optional[int] = None,
2155
+ page_token: Optional[str] = None,
2156
+ ) -> ListBudgetSpendStatusesResponse:
2157
+ """List the calling user's live spend against the blocking thresholds across the account's Unity Gateway
2158
+ budgets. Any account user can call this for their own spend. Spend is reported in USD at list price
2159
+ and is near-real-time, so it may differ slightly from billing data. By default, returns every budget
2160
+ in the account that has a blocking threshold. Use ``filter`` to narrow results by workspace.
2161
+
2162
+ :param parent: str
2163
+ The parent account whose budget spend statuses to list, of the form ``accounts/{account_id}``.
2164
+ :param filter: str (optional)
2165
+ A filter expression that narrows the returned budgets by workspace. Only ``workspace_id`` is
2166
+ supported, in lower case with numeric workspace IDs, for example ``workspace_id = 123`` or
2167
+ ``workspace_id IN (123, 456)``. Matches budgets that apply to any of the listed workspaces,
2168
+ including budgets that apply to all workspaces. Any other expression, such as an empty ``IN ()``
2169
+ list, fails with INVALID_PARAMETER_VALUE. If empty, returns every budget in the account that has a
2170
+ blocking threshold.
2171
+ :param page_size: int (optional)
2172
+ The maximum number of spend statuses to return per page. If unspecified or 0, at most 100 are
2173
+ returned. The maximum value is 100; values above 100 are coerced to 100. Negative values fail with
2174
+ INVALID_PARAMETER_VALUE. The server may return fewer results than requested, including zero, even
2175
+ when more pages remain; page until ``next_page_token`` is absent.
2176
+ :param page_token: str (optional)
2177
+ A page token from a previous ``ListBudgetSpendStatuses`` response, used to fetch the next page.
2178
+ Requests the first page if absent. All other request parameters except ``page_size`` must match the
2179
+ request that returned the token; otherwise the request fails with INVALID_PARAMETER_VALUE.
2180
+
2181
+ :returns: :class:`ListBudgetSpendStatusesResponse`
2182
+ """
2183
+
2184
+ query = {}
2185
+ if filter is not None:
2186
+ query["filter"] = filter
2187
+ if page_size is not None:
2188
+ query["page_size"] = page_size
2189
+ if page_token is not None:
2190
+ query["page_token"] = page_token
2191
+ headers = {
2192
+ "Accept": "application/json",
2193
+ }
2194
+
2195
+ res = self._api.do("GET", f"/api/2.1/{parent}/budget-spend-statuses", query=query, headers=headers)
2196
+ return ListBudgetSpendStatusesResponse.from_dict(res)
2197
+
1980
2198
  def update(self, budget_id: str, budget: UpdateBudgetConfigurationBudget) -> UpdateBudgetConfigurationResponse:
1981
2199
  """Updates a budget configuration for an account. Both account and budget configuration are specified by
1982
2200
  ID.
@@ -7531,9 +7531,10 @@ class ModelProviderServiceConfig:
7531
7531
  cross the trust boundary to the upstream provider."""
7532
7532
 
7533
7533
  allow_all_targets: Optional[bool] = None
7534
- """When true, accepts any model exposed by the upstream provider; ``targets`` is not required and
7535
- does not restrict routability. When false, only models listed in ``targets`` are routable.
7536
- Defaults to false."""
7534
+ """When true, allows models from the provider's built-in model catalog in addition to explicit
7535
+ ``targets``. When false, only models listed in ``targets`` are routable. Defaults to false.
7536
+ CUSTOM providers have no built-in model catalog and must leave this false and list their models
7537
+ in ``targets``."""
7537
7538
 
7538
7539
  amazon_bedrock: Optional[ModelProviderServiceConfigAmazonBedrockProviderConfig] = None
7539
7540
 
@@ -7588,8 +7589,9 @@ class ModelProviderServiceConfig:
7588
7589
  """Models and provider-native API types exposed by this provider service. Each entry must include
7589
7590
  at least one ``native_api_types`` value. When ``allow_all_targets`` is false, at least one entry
7590
7591
  is required and model service destinations can reference only listed models. When
7591
- ``allow_all_targets`` is true, any upstream model is routable; entries in this list provide
7592
- API-type metadata without restricting other models."""
7592
+ ``allow_all_targets`` is true, models from the provider's built-in model catalog are also
7593
+ routable. CUSTOM providers must explicitly list their models here and cannot enable
7594
+ ``allow_all_targets``."""
7593
7595
 
7594
7596
  def as_dict(self) -> dict:
7595
7597
  """Serializes the ModelProviderServiceConfig into a dictionary suitable for use as a JSON request body."""
@@ -14828,8 +14830,8 @@ class AiGatewayAPI:
14828
14830
  returned.
14829
14831
 
14830
14832
  :param page_size: int (optional)
14831
- Maximum number of MCP services to return. Defaults to 200 when unset or 0; the maximum is 200. Use
14832
- ``page_token`` to retrieve additional pages.
14833
+ Maximum number of MCP services to return. Defaults to 200 when unset or 0; the maximum is 1000 for
14834
+ ``view = BASIC`` and 200 for ``view = FULL``. Use ``page_token`` to retrieve additional pages.
14833
14835
  :param page_token: str (optional)
14834
14836
  Opaque pagination token from the previous response.
14835
14837
  :param parent: str (optional)
@@ -14886,8 +14888,8 @@ class AiGatewayAPI:
14886
14888
  ``MANAGE``) are returned.
14887
14889
 
14888
14890
  :param page_size: int (optional)
14889
- Maximum number of provider services to return. Defaults to 200 when unset or 0; the maximum is 200.
14890
- Use ``page_token`` to retrieve additional pages.
14891
+ Maximum number of provider services to return. Defaults to 200 when unset or 0; the maximum is 1000
14892
+ for ``view = BASIC`` and 200 for ``view = FULL``. Use ``page_token`` to retrieve additional pages.
14891
14893
  :param page_token: str (optional)
14892
14894
  Opaque pagination token from the previous response.
14893
14895
  :param parent: str (optional)
@@ -14944,8 +14946,8 @@ class AiGatewayAPI:
14944
14946
  returned by default.
14945
14947
 
14946
14948
  :param page_size: int (optional)
14947
- Maximum number of model services to return. Defaults to 200 when unset or 0; the maximum is 200. Use
14948
- ``page_token`` to retrieve additional pages.
14949
+ Maximum number of model services to return. Defaults to 200 when unset or 0; the maximum is 1000 for
14950
+ ``view = BASIC`` and 200 for ``view = FULL``. Use ``page_token`` to retrieve additional pages.
14949
14951
  :param page_token: str (optional)
14950
14952
  Opaque pagination token from the previous response.
14951
14953
  :param parent: str (optional)