Repository: databricks/databricks-sdk-py
Pull request #912: [Fix] Add support for all error details types and fix potential unmarshalling error
Author: login: renaudhartert-db
Created: 2025-03-04T14:28:01Z | Merged: 2025-03-05T10:34:28Z

## What changes are proposed in this pull request?

This PR refactors how `DatabricksError` processes error details to fully support the Databricks Error specification. In particular, it provides a clear mechanism to access known error details types while still enabling users to access unknown error details.

The core of the PR is the addition of a new `ErrorDetails` type which operates as the union of all error details type. It implements the fact that an error response should only contain at most one instance of each type.

The code is purposely structured to hide implementation details by separating the exported error details types from their unmarshalling logic. This arguably leads to code that might be more complicated that it ought to be. We could consider further simplifying it in the future (e.g. by relying on the corresponding proto unmarshallers). 

## How is this tested?

100% unit tests coverage of new code + slight refactor of the test suite.


Diff:
diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
index 9c12df319..7ab392c10 100644
--- a/NEXT_CHANGELOG.md
+++ b/NEXT_CHANGELOG.md
@@ -9,6 +9,8 @@
 ### Documentation
 
 ### Internal Changes
+
+* Refactor `DatabricksError` to expose different types of error details ([#912](https://github.com/databricks/databricks-sdk-py/pull/912)). 
 * Update Jobs ListJobs API to support paginated responses ([#896](https://github.com/databricks/databricks-sdk-py/pull/896))
 * Update Jobs ListRuns API to support paginated responses ([#890](https://github.com/databricks/databricks-sdk-py/pull/890))
 * Introduce automated tagging ([#888](https://github.com/databricks/databricks-sdk-py/pull/888))
diff --git a/databricks/sdk/errors/base.py b/databricks/sdk/errors/base.py
index be59517c4..908172576 100644
--- a/databricks/sdk/errors/base.py
+++ b/databricks/sdk/errors/base.py
@@ -5,7 +5,10 @@
 
 import requests
 
+from . import details as errdetails
 
+
+# Deprecated.
 class ErrorDetail:
     def __init__(
         self,
@@ -22,17 +25,18 @@ def __init__(
 
     @classmethod
     def from_dict(cls, d: Dict[str, any]) -> "ErrorDetail":
-        if "@type" in d:
-            d["type"] = d["@type"]
-        return cls(**d)
+        # Key "@type" is not a valid keyword argument name in Python. Rename
+        # it to "type" to avoid conflicts.
+        safe_args = {}
+        for k, v in d.items():
+            safe_args[k if k != "@type" else "type"] = v
+
+        return cls(**safe_args)
 
 
 class DatabricksError(IOError):
     """Generic error from Databricks REST API"""
 
-    # Known ErrorDetail types
-    _error_info_type = "type.googleapis.com/google.rpc.ErrorInfo"
-
     def __init__(
         self,
         message: str = None,
@@ -54,11 +58,11 @@ def __init__(
         :param status: [Deprecated]
         :param scimType: [Deprecated]
         :param error: [Deprecated]
-        :param retry_after_secs:
+        :param retry_after_secs: [Deprecated]
         :param details:
         :param kwargs:
         """
-        # SCIM-specific parameters are deprecated
+        # SCIM-specific parameters are deprecated.
         if detail:
             warnings.warn(
                 "The 'detail' parameter of DatabricksError is deprecated and will be removed in a future version."
@@ -72,12 +76,18 @@ def __init__(
                 "The 'status' parameter of DatabricksError is deprecated and will be removed in a future version."
             )
 
-        # API 1.2-specific parameters are deprecated
+        # API 1.2-specific parameters are deprecated.
         if error:
             warnings.warn(
                 "The 'error' parameter of DatabricksError is deprecated and will be removed in a future version."
             )
 
+        # Retry-after is deprecated.
+        if retry_after_secs:
+            warnings.warn(
+                "The 'retry_after_secs' parameter of DatabricksError is deprecated and will be removed in a future version."
+            )
+
         if detail:
             # Handle SCIM error message details
             # @see https://tools.ietf.org/html/rfc7644#section-3.7.3
@@ -88,20 +98,32 @@ def __init__(
             # add more context from SCIM responses
             message = f"{scimType} {message}".strip(" ")
             error_code = f"SCIM_{status}"
+
         super().__init__(message if message else error)
         self.error_code = error_code
         self.retry_after_secs = retry_after_secs
-        self.details = [ErrorDetail.from_dict(detail) for detail in details] if details else []
+        self._error_details = errdetails.parse_error_details(details)
         self.kwargs = kwargs
 
+        # Deprecated.
+        self.details = []
+        if details:
+            for d in details:
+                if not isinstance(d, dict):
+                    continue
+                self.details.append(ErrorDetail.from_dict(d))
+
     def get_error_info(self) -> List[ErrorDetail]:
-        return self._get_details_by_type(DatabricksError._error_info_type)
+        return self._get_details_by_type(errdetails._ERROR_INFO_TYPE)
 
     def _get_details_by_type(self, error_type) -> List[ErrorDetail]:
-        if self.details == None:
+        if self.details is None:
             return []
         return [detail for detail in self.details if detail.type == error_type]
 
+    def get_error_details(self) -> errdetails.ErrorDetails:
+        return self._error_details
+
 
 @dataclass
 class _ErrorOverride:
diff --git a/databricks/sdk/errors/details.py b/databricks/sdk/errors/details.py
new file mode 100644
index 000000000..c7e7ca1a2
--- /dev/null
+++ b/databricks/sdk/errors/details.py
@@ -0,0 +1,417 @@
+import re
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class ErrorInfo:
+    """Describes the cause of the error with structured details."""
+
+    reason: str
+    domain: str
+    metadata: Dict[str, str]
+
+
+@dataclass
+class RequestInfo:
+    """
+    Contains metadata about the request that clients can attach when
+    filing a bug or providing other forms of feedback.
+    """
+
+    request_id: str
+    serving_data: str
+
+
+@dataclass
+class RetryInfo:
+    """
+    Describes when the clients can retry a failed request. Clients could
+    ignore the recommendation here or retry when this information is missing
+    from error responses.
+
+    It's always recommended that clients should use exponential backoff
+    when retrying.
+
+    Clients should wait until `retry_delay` amount of time has passed since
+    receiving the error response before retrying.  If retrying requests also
+    fail, clients should use an exponential backoff scheme to gradually
+    increase the delay between retries based on `retry_delay`, until either
+    a maximum number of retries have been reached or a maximum retry delay
+    cap has been reached.
+    """
+
+    retry_delay_seconds: float
+
+
+@dataclass
+class DebugInfo:
+    """Describes additional debugging info."""
+
+    stack_entries: List[str]
+    detail: str
+
+
+@dataclass
+class QuotaFailureViolation:
+    """Describes a single quota violation."""
+
+    subject: str
+    description: str
+
+
+@dataclass
+class QuotaFailure:
+    """
+    Describes how a quota check failed.
+
+    For example if a daily limit was exceeded for the calling project, a
+    service could respond with a QuotaFailure detail containing the project
+    id and the description of the quota limit that was exceeded.  If the
+    calling project hasn't enabled the service in the developer console,
+    then a service could respond with the project id and set
+    `service_disabled` to true.
+
+    Also see RetryInfo and Help types for other details about handling a
+    quota failure.
+    """
+
+    violations: List[QuotaFailureViolation]
+
+
+@dataclass
+class PreconditionFailureViolation:
+    """Describes a single precondition violation."""
+
+    type: str
+    subject: str
+    description: str
+
+
+@dataclass
+class PreconditionFailure:
+    """Describes what preconditions have failed."""
+
+    violations: List[PreconditionFailureViolation]
+
+
+@dataclass
+class BadRequestFieldViolation:
+    """Describes a single field violation in a bad request."""
+
+    field: str
+    description: str
+
+
+@dataclass
+class BadRequest:
+    """
+    Describes violations in a client request. This error type
+    focuses on the syntactic aspects of the request.
+    """
+
+    field_violations: List[BadRequestFieldViolation]
+
+
+@dataclass
+class ResourceInfo:
+    """Describes the resource that is being accessed."""
+
+    resource_type: str
+    resource_name: str
+    owner: str
+    description: str
+
+
+@dataclass
+class HelpLink:
+    """Describes a single help link."""
+
+    description: str
+    url: str
+
+
+@dataclass
+class Help:
+    """
+    Provides links to documentation or for performing an out of
+    band action.
+
+    For example, if a quota check failed with an error indicating
+    the calling project hasn't enabled the accessed service, this
+    can contain a URL pointing directly to the right place in the
+    developer console to flip the bit.
+    """
+
+    links: List[HelpLink]
+
+
+@dataclass
+class ErrorDetails:
+    """
+    ErrorDetails contains the error details of an API error. It
+    is the union of known error details types and unknown details.
+    """
+
+    error_info: Optional[ErrorInfo] = None
+    request_info: Optional[RequestInfo] = None
+    retry_info: Optional[RetryInfo] = None
+    debug_info: Optional[DebugInfo] = None
+    quota_failure: Optional[QuotaFailure] = None
+    precondition_failure: Optional[PreconditionFailure] = None
+    bad_request: Optional[BadRequest] = None
+    resource_info: Optional[ResourceInfo] = None
+    help: Optional[Help] = None
+    unknown_details: List[Any] = field(default_factory=list)
+
+
+# Supported error details proto types.
+_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"
+_REQUEST_INFO_TYPE = "type.googleapis.com/google.rpc.RequestInfo"
+_RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo"
+_DEBUG_INFO_TYPE = "type.googleapis.com/google.rpc.DebugInfo"
+_QUOTA_FAILURE_TYPE = "type.googleapis.com/google.rpc.QuotaFailure"
+_PRECONDITION_FAILURE_TYPE = "type.googleapis.com/google.rpc.PreconditionFailure"
+_BAD_REQUEST_TYPE = "type.googleapis.com/google.rpc.BadRequest"
+_RESOURCE_INFO_TYPE = "type.googleapis.com/google.rpc.ResourceInfo"
+_HELP_TYPE = "type.googleapis.com/google.rpc.Help"
+
+
+def parse_error_details(details: List[Any]) -> ErrorDetails:
+    ed = ErrorDetails()
+
+    if not details:
+        return ed
+
+    for d in details:
+        pd = _parse_json_error_details(d)
+
+        if isinstance(pd, ErrorInfo):
+            ed.error_info = pd
+        elif isinstance(pd, RequestInfo):
+            ed.request_info = pd
+        elif isinstance(pd, RetryInfo):
+            ed.retry_info = pd
+        elif isinstance(pd, DebugInfo):
+            ed.debug_info = pd
+        elif isinstance(pd, QuotaFailure):
+            ed.quota_failure = pd
+        elif isinstance(pd, PreconditionFailure):
+            ed.precondition_failure = pd
+        elif isinstance(pd, BadRequest):
+            ed.bad_request = pd
+        elif isinstance(pd, ResourceInfo):
+            ed.resource_info = pd
+        elif isinstance(pd, Help):
+            ed.help = pd
+        else:
+            ed.unknown_details.append(pd)
+
+    return ed
+
+
+def _parse_json_error_details(value: Any) -> Any:
+    """
+    Attempts to parse an error details type from the given JSON value. If the
+    value is not a known error details type, it returns the input as is.
+
+    :param value: The JSON value to parse.
+    :return: The parsed error details type or the input value if it is not
+        a known error details type.
+    """
+
+    if not isinstance(value, dict):
+        return value  # not a JSON object
+
+    t = value.get("@type")
+    if not isinstance(t, str):
+        return value  # JSON object with no @type field
+
+    try:
+        if t == _ERROR_INFO_TYPE:
+            return _parse_error_info(value)
+        elif t == _REQUEST_INFO_TYPE:
+            return _parse_req_info(value)
+        elif t == _RETRY_INFO_TYPE:
+            return _parse_retry_info(value)
+        elif t == _DEBUG_INFO_TYPE:
+            return _parse_debug_info(value)
+        elif t == _QUOTA_FAILURE_TYPE:
+            return _parse_quota_failure(value)
+        elif t == _PRECONDITION_FAILURE_TYPE:
+            return _parse_precondition_failure(value)
+        elif t == _BAD_REQUEST_TYPE:
+            return _parse_bad_request(value)
+        elif t == _RESOURCE_INFO_TYPE:
+            return _parse_resource_info(value)
+        elif t == _HELP_TYPE:
+            return _parse_help(value)
+        else:  # unknown type
+            return value
+    except (TypeError, ValueError):
+        return value  # not a valid known type
+    except Exception:
+        return value
+
+
+def _parse_error_info(d: Dict[str, Any]) -> ErrorInfo:
+    return ErrorInfo(
+        domain=_parse_string(d.get("domain", "")),
+        reason=_parse_string(d.get("reason", "")),
+        metadata=_parse_dict(d.get("metadata", {})),
+    )
+
+
+def _parse_req_info(d: Dict[str, Any]) -> RequestInfo:
+    return RequestInfo(
+        request_id=_parse_string(d.get("request_id", "")),
+        serving_data=_parse_string(d.get("serving_data", "")),
+    )
+
+
+def _parse_retry_info(d: Dict[str, Any]) -> RetryInfo:
+    delay = 0.0
+    if "retry_delay" in d:
+        delay = _parse_seconds(d["retry_delay"])
+
+    return RetryInfo(
+        retry_delay_seconds=delay,
+    )
+
+
+def _parse_debug_info(d: Dict[str, Any]) -> DebugInfo:
+    di = DebugInfo(
+        stack_entries=[],
+        detail=_parse_string(d.get("detail", "")),
+    )
+
+    if "stack_entries" not in d:
+        return di
+
+    if not isinstance(d["stack_entries"], list):
+        raise ValueError(f"Expected list, got {d['stack_entries']!r}")
+    for entry in d["stack_entries"]:
+        di.stack_entries.append(_parse_string(entry))
+
+    return di
+
+
+def _parse_quota_failure_violation(d: Dict[str, Any]) -> QuotaFailureViolation:
+    return QuotaFailureViolation(
+        subject=_parse_string(d.get("subject", "")),
+        description=_parse_string(d.get("description", "")),
+    )
+
+
+def _parse_quota_failure(d: Dict[str, Any]) -> QuotaFailure:
+    violations = []
+    if "violations" in d:
+        if not isinstance(d["violations"], list):
+            raise ValueError(f"Expected list, got {d['violations']!r}")
+        for violation in d["violations"]:
+            if not isinstance(violation, dict):
+                raise ValueError(f"Expected dict, got {violation!r}")
+            violations.append(_parse_quota_failure_violation(violation))
+    return QuotaFailure(violations=violations)
+
+
+def _parse_precondition_failure_violation(d: Dict[str, Any]) -> PreconditionFailureViolation:
+    return PreconditionFailureViolation(
+        type=_parse_string(d.get("type", "")),
+        subject=_parse_string(d.get("subject", "")),
+        description=_parse_string(d.get("description", "")),
+    )
+
+
+def _parse_precondition_failure(d: Dict[str, Any]) -> PreconditionFailure:
+    violations = []
+    if "violations" in d:
+        if not isinstance(d["violations"], list):
+            raise ValueError(f"Expected list, got {d['violations']!r}")
+        for v in d["violations"]:
+            if not isinstance(v, dict):
+                raise ValueError(f"Expected dict, got {v!r}")
+            violations.append(_parse_precondition_failure_violation(v))
+    return PreconditionFailure(violations=violations)
+
+
+def _parse_bad_request_field_violation(d: Dict[str, Any]) -> BadRequestFieldViolation:
+    return BadRequestFieldViolation(
+        field=_parse_string(d.get("field", "")),
+        description=_parse_string(d.get("description", "")),
+    )
+
+
+def _parse_bad_request(d: Dict[str, Any]) -> BadRequest:
+    field_violations = []
+    if "field_violations" in d:
+        if not isinstance(d["field_violations"], list):
+            raise ValueError(f"Expected list, got {d['field_violations']!r}")
+        for violation in d["field_violations"]:
+            if not isinstance(violation, dict):
+                raise ValueError(f"Expected dict, got {violation!r}")
+            field_violations.append(_parse_bad_request_field_violation(violation))
+    return BadRequest(field_violations=field_violations)
+
+
+def _parse_resource_info(d: Dict[str, Any]) -> ResourceInfo:
+    return ResourceInfo(
+        resource_type=_parse_string(d.get("resource_type", "")),
+        resource_name=_parse_string(d.get("resource_name", "")),
+        owner=_parse_string(d.get("owner", "")),
+        description=_parse_string(d.get("description", "")),
+    )
+
+
+def _parse_help_link(d: Dict[str, Any]) -> HelpLink:
+    return HelpLink(
+        description=_parse_string(d.get("description", "")),
+        url=_parse_string(d.get("url", "")),
+    )
+
+
+def _parse_help(d: Dict[str, Any]) -> Help:
+    links = []
+    if "links" in d:
+        if not isinstance(d["links"], list):
+            raise ValueError(f"Expected list, got {d['links']!r}")
+        for link in d["links"]:
+            if not isinstance(link, dict):
+                raise ValueError(f"Expected dict, got {link!r}")
+            links.append(_parse_help_link(link))
+    return Help(links=links)
+
+
+def _parse_string(a: Any) -> str:
+    if isinstance(a, str):
+        return a
+    raise ValueError(f"Expected string, got {a!r}")
+
+
+def _parse_dict(a: Any) -> Dict[str, str]:
+    if not isinstance(a, dict):
+        raise ValueError(f"Expected Dict[str, str], got {a!r}")
+    for key, value in a.items():
+        if not isinstance(key, str) or not isinstance(value, str):
+            raise ValueError(f"Expected Dict[str, str], got {a!r}")
+    return a
+
+
+def _parse_seconds(a: Any) -> float:
+    """
+    Parse a duration string into a float representing the number of seconds.
+
+    The duration type is encoded as a string rather than an where the string
+    ends in the suffix "s" (indicating seconds) and is preceded by a decimal
+    number of seconds. For example, "3.000000001s", represents a duration of
+    3 seconds and 1 nanosecond.
+    """
+
+    if not isinstance(a, str):
+        raise ValueError(f"Expected string, got {a!r}")
+
+    match = re.match(r"^(\d+(\.\d+)?)s$", a)
+    if match:
+        return float(match.group(1))
+
+    raise ValueError(f"Expected duration string, got {a!r}")
diff --git a/databricks/sdk/errors/parser.py b/databricks/sdk/errors/parser.py
index 5861df770..64d83de05 100644
--- a/databricks/sdk/errors/parser.py
+++ b/databricks/sdk/errors/parser.py
@@ -91,3 +91,5 @@ def get_api_error(self, response: requests.Response) -> Optional[DatabricksError
         # is successful, but the response is not what we expect. We need to handle this case separately.
         if _is_private_link_redirect(response):
             return _get_private_link_validation_error(response.url)
+
+        return None
diff --git a/tests/test_base_client.py b/tests/test_base_client.py
index 820c52c11..8b3501d49 100644
--- a/tests/test_base_client.py
+++ b/tests/test_base_client.py
@@ -70,7 +70,7 @@ def test_streaming_response_read_closes(config):
                 "message": "errorMessage",
                 "details": [
                     {
-                        "type": DatabricksError._error_info_type,
+                        "type": "type.googleapis.com/google.rpc.ErrorInfo",
                         "reason": "error reason",
                         "domain": "error domain",
                         "metadata": {"etag": "error etag"},
@@ -87,7 +87,7 @@ def test_streaming_response_read_closes(config):
                 "errorMessage",
                 details=[
                     {
-                        "type": DatabricksError._error_info_type,
+                        "type": "type.googleapis.com/google.rpc.ErrorInfo",
                         "reason": "error reason",
                         "domain": "error domain",
                         "metadata": {"etag": "error etag"},
@@ -553,7 +553,7 @@ def test_rewind_seekable_stream(test_case: RetryTestCase, failure: Tuple[Callabl
     client._session = session
 
     def do():
-        client.do("POST", f"test.com/foo", data=data)
+        client.do("POST", "test.com/foo", data=data)
 
     if test_case._expected_failure:
         expected_attempts_made = 1
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 0e775bd50..e0032900f 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -1,11 +1,13 @@
 import http.client
 import json
-from typing import List, Optional, Tuple
+from dataclasses import dataclass, field
+from typing import Any, List, Optional
 
 import pytest
 import requests
 
 from databricks.sdk import errors
+from databricks.sdk.errors import details
 
 
 def fake_response(
@@ -39,10 +41,14 @@ def fake_valid_response(
     error_code: str,
     message: str,
     path: Optional[str] = None,
+    details: List[Any] = [],
 ) -> requests.Response:
     body = {"message": message}
     if error_code:
         body["error_code"] = error_code
+    if len(details) > 0:
+        body["details"] = details
+
     return fake_response(method, status_code, json.dumps(body), path)
 
 
@@ -55,176 +61,313 @@ def make_private_link_response() -> requests.Response:
     return resp
 
 
-# This should be `(int, str, type)` but doesn't work in Python 3.7-3.8.
-base_subclass_test_cases: List[Tuple[int, str, type]] = [
-    (400, "", errors.BadRequest),
-    (400, "INVALID_PARAMETER_VALUE", errors.BadRequest),
-    (400, "INVALID_PARAMETER_VALUE", errors.InvalidParameterValue),
-    (400, "REQUEST_LIMIT_EXCEEDED", errors.TooManyRequests),
-    (400, "", IOError),
-    (401, "", errors.Unauthenticated),
-    (401, "", IOError),
-    (403, "", errors.PermissionDenied),
-    (403, "", IOError),
-    (404, "", errors.NotFound),
-    (404, "", IOError),
-    (409, "", errors.ResourceConflict),
-    (409, "ABORTED", errors.Aborted),
-    (409, "ABORTED", errors.ResourceConflict),
-    (409, "ALREADY_EXISTS", errors.AlreadyExists),
-    (409, "ALREADY_EXISTS", errors.ResourceConflict),
-    (409, "", IOError),
-    (429, "", errors.TooManyRequests),
-    (429, "REQUEST_LIMIT_EXCEEDED", errors.TooManyRequests),
-    (429, "REQUEST_LIMIT_EXCEEDED", errors.RequestLimitExceeded),
-    (429, "RESOURCE_EXHAUSTED", errors.TooManyRequests),
-    (429, "RESOURCE_EXHAUSTED", errors.ResourceExhausted),
-    (429, "", IOError),
-    (499, "", errors.Cancelled),
-    (499, "", IOError),
-    (500, "", errors.InternalError),
-    (500, "UNKNOWN", errors.InternalError),
-    (500, "UNKNOWN", errors.Unknown),
-    (500, "DATA_LOSS", errors.InternalError),
-    (500, "DATA_LOSS", errors.DataLoss),
-    (500, "", IOError),
-    (501, "", errors.NotImplemented),
-    (501, "", IOError),
-    (503, "", errors.TemporarilyUnavailable),
-    (503, "", IOError),
-    (504, "", errors.DeadlineExceeded),
-    (504, "", IOError),
-    (444, "", errors.DatabricksError),
-    (444, "", IOError),
-]
+@dataclass
+class TestCase:
+    name: str
+    response: requests.Response
+    want_err_type: type
+    want_message: str = ""
+    want_details: details.ErrorDetails = field(default_factory=details.ErrorDetails)
 
-subclass_test_cases = [(fake_valid_response("GET", x[0], x[1], "nope"), x[2], "nope") for x in base_subclass_test_cases]
 
+_basic_test_cases_no_details = [
+    TestCase(
+        name=f'{x[0]} "{x[1]}" {x[2]}',
+        response=fake_valid_response("GET", x[0], x[1], "nope"),
+        want_err_type=x[2],
+        want_message="nope",
+    )
+    for x in [
+        (400, "", errors.BadRequest),
+        (400, "INVALID_PARAMETER_VALUE", errors.BadRequest),
+        (400, "INVALID_PARAMETER_VALUE", errors.InvalidParameterValue),
+        (400, "REQUEST_LIMIT_EXCEEDED", errors.TooManyRequests),
+        (400, "", IOError),
+        (401, "", errors.Unauthenticated),
+        (401, "", IOError),
+        (403, "", errors.PermissionDenied),
+        (403, "", IOError),
+        (404, "", errors.NotFound),
+        (404, "", IOError),
+        (409, "", errors.ResourceConflict),
+        (409, "ABORTED", errors.Aborted),
+        (409, "ABORTED", errors.ResourceConflict),
+        (409, "ALREADY_EXISTS", errors.AlreadyExists),
+        (409, "ALREADY_EXISTS", errors.ResourceConflict),
+        (409, "", IOError),
+        (429, "", errors.TooManyRequests),
+        (429, "REQUEST_LIMIT_EXCEEDED", errors.TooManyRequests),
+        (429, "REQUEST_LIMIT_EXCEEDED", errors.RequestLimitExceeded),
+        (429, "RESOURCE_EXHAUSTED", errors.TooManyRequests),
+        (429, "RESOURCE_EXHAUSTED", errors.ResourceExhausted),
+        (429, "", IOError),
+        (499, "", errors.Cancelled),
+        (499, "", IOError),
+        (500, "", errors.InternalError),
+        (500, "UNKNOWN", errors.InternalError),
+        (500, "UNKNOWN", errors.Unknown),
+        (500, "DATA_LOSS", errors.InternalError),
+        (500, "DATA_LOSS", errors.DataLoss),
+        (500, "", IOError),
+        (501, "", errors.NotImplemented),
+        (501, "", IOError),
+        (503, "", errors.TemporarilyUnavailable),
+        (503, "", IOError),
+        (504, "", errors.DeadlineExceeded),
+        (504, "", IOError),
+        (444, "", errors.DatabricksError),
+        (444, "", IOError),
+    ]
+]
 
-@pytest.mark.parametrize(
-    "response, expected_error, expected_message",
-    subclass_test_cases
-    + [
-        (fake_response("GET", 400, ""), errors.BadRequest, "Bad Request"),
-        (
-            fake_valid_response("GET", 417, "WHOOPS", "nope"),
-            errors.DatabricksError,
-            "nope",
-        ),
-        (
-            fake_valid_response("GET", 522, "", "nope"),
-            errors.DatabricksError,
-            "nope",
+
+_test_case_with_details = [
+    TestCase(
+        name="all_error_details",
+        response=fake_valid_response(
+            method="GET",
+            status_code=404,
+            error_code="NOT_FOUND",
+            message="test message",
+            details=[
+                {
+                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
+                    "reason": "reason",
+                    "domain": "domain",
+                    "metadata": {"k1": "v1", "k2": "v2"},
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.RequestInfo",
+                    "request_id": "req42",
+                    "serving_data": "data",
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.RetryInfo",
+                    "retry_delay": "42.000000001s",
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.DebugInfo",
+                    "stack_entries": ["entry1", "entry2"],
+                    "detail": "detail",
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.QuotaFailure",
+                    "violations": [{"subject": "subject", "description": "description"}],
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.PreconditionFailure",
+                    "violations": [{"type": "type", "subject": "subject", "description": "description"}],
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.BadRequest",
+                    "field_violations": [{"field": "field", "description": "description"}],
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.ResourceInfo",
+                    "resource_type": "resource_type",
+                    "resource_name": "resource_name",
+                    "owner": "owner",
+                    "description": "description",
+                },
+                {
+                    "@type": "type.googleapis.com/google.rpc.Help",
+                    "links": [{"description": "description", "url": "url"}],
+                },
+            ],
         ),
-        (
-            make_private_link_response(),
-            errors.PrivateLinkValidationError,
-            (
-                "The requested workspace has AWS PrivateLink enabled and is not accessible from the current network. "
-                "Ensure that AWS PrivateLink is properly configured and that your device has access to the AWS VPC "
-                "endpoint. For more information, see "
-                "https://docs.databricks.com/en/security/network/classic/privatelink.html."
+        want_err_type=errors.NotFound,
+        want_message="test message",
+        want_details=details.ErrorDetails(
+            error_info=details.ErrorInfo(
+                reason="reason",
+                domain="domain",
+                metadata={"k1": "v1", "k2": "v2"},
             ),
-        ),
-        (
-            fake_valid_response(
-                "GET",
-                400,
-                "INVALID_PARAMETER_VALUE",
-                "Cluster abcde does not exist",
-                "/api/2.0/clusters/get",
+            request_info=details.RequestInfo(
+                request_id="req42",
+                serving_data="data",
             ),
-            errors.ResourceDoesNotExist,
-            "Cluster abcde does not exist",
-        ),
-        (
-            fake_valid_response(
-                "GET",
-                400,
-                "INVALID_PARAMETER_VALUE",
-                "Job abcde does not exist",
-                "/api/2.0/jobs/get",
+            retry_info=details.RetryInfo(
+                retry_delay_seconds=42.000000001,
             ),
-            errors.ResourceDoesNotExist,
-            "Job abcde does not exist",
-        ),
-        (
-            fake_valid_response(
-                "GET",
-                400,
-                "INVALID_PARAMETER_VALUE",
-                "Job abcde does not exist",
-                "/api/2.1/jobs/get",
+            debug_info=details.DebugInfo(
+                stack_entries=["entry1", "entry2"],
+                detail="detail",
             ),
-            errors.ResourceDoesNotExist,
-            "Job abcde does not exist",
-        ),
-        (
-            fake_valid_response(
-                "GET",
-                400,
-                "INVALID_PARAMETER_VALUE",
-                "Invalid spark version",
-                "/api/2.1/jobs/get",
+            quota_failure=details.QuotaFailure(
+                violations=[
+                    details.QuotaFailureViolation(
+                        subject="subject",
+                        description="description",
+                    )
+                ],
             ),
-            errors.InvalidParameterValue,
-            "Invalid spark version",
-        ),
-        (
-            fake_response(
-                "GET",
-                400,
-                "MALFORMED_REQUEST: vpc_endpoints malformed parameters: VPC Endpoint ... with use_case ... cannot be attached in ... list",
+            precondition_failure=details.PreconditionFailure(
+                violations=[
+                    details.PreconditionFailureViolation(
+                        type="type",
+                        subject="subject",
+                        description="description",
+                    )
+                ],
+            ),
+            bad_request=details.BadRequest(
+                field_violations=[
+                    details.BadRequestFieldViolation(
+                        field="field",
+                        description="description",
+                    )
+                ],
             ),
-            errors.BadRequest,
-            "vpc_endpoints malformed parameters: VPC Endpoint ... with use_case ... cannot be attached in ... list",
+            resource_info=details.ResourceInfo(
+                resource_type="resource_type",
+                resource_name="resource_name",
+                owner="owner",
+                description="description",
+            ),
+            help=details.Help(
+                links=[
+                    details.HelpLink(
+                        description="description",
+                        url="url",
+                    )
+                ],
+            ),
+            unknown_details=[],
         ),
-        (
-            fake_response("GET", 400, "<pre>Worker environment not ready</pre>"),
-            errors.BadRequest,
-            "Worker environment not ready",
+    ),
+    TestCase(
+        name="unknown_error_details",
+        response=fake_valid_response(
+            method="GET",
+            status_code=404,
+            error_code="NOT_FOUND",
+            message="test message",
+            details=[
+                1,
+                "foo",
+                ["foo", "bar"],
+                {
+                    "@type": "type.googleapis.com/google.rpc.FooBar",
+                    "reason": "reason",
+                    "domain": "domain",
+                },
+            ],
         ),
-        (
-            fake_response("GET", 400, "this is not a real response"),
-            errors.BadRequest,
-            (
-                "unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. "
-                "Please report this issue with the following debugging information to the SDK issue tracker at "
-                "https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n"
-                "< 400 Bad Request\n"
-                "< this is not a real response```"
-            ),
+        want_err_type=errors.NotFound,
+        want_message="test message",
+        want_details=details.ErrorDetails(
+            unknown_details=[
+                1,
+                "foo",
+                ["foo", "bar"],
+                {
+                    "@type": "type.googleapis.com/google.rpc.FooBar",
+                    "reason": "reason",
+                    "domain": "domain",
+                },
+            ],
         ),
-        (
-            fake_response(
-                "GET",
-                404,
-                json.dumps(
-                    {
-                        "detail": "Group with id 1234 is not found",
-                        "status": "404",
-                        "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
-                    }
-                ),
+    ),
+]
+
+_test_case_other_errors = [
+    TestCase(
+        name="private_link_validation_error",
+        response=make_private_link_response(),
+        want_err_type=errors.PrivateLinkValidationError,
+        want_message=(
+            "The requested workspace has AWS PrivateLink enabled and is not accessible from the current network. "
+            "Ensure that AWS PrivateLink is properly configured and that your device has access to the AWS VPC "
+            "endpoint. For more information, see "
+            "https://docs.databricks.com/en/security/network/classic/privatelink.html."
+        ),
+    ),
+    TestCase(
+        name="malformed_request",
+        response=fake_response(
+            method="GET",
+            status_code=400,
+            response_body="MALFORMED_REQUEST: vpc_endpoints malformed parameters: VPC Endpoint ... with use_case ... cannot be attached in ... list",
+        ),
+        want_err_type=errors.BadRequest,
+        want_message="vpc_endpoints malformed parameters: VPC Endpoint ... with use_case ... cannot be attached in ... list",
+    ),
+    TestCase(
+        name="worker_environment_not_ready",
+        response=fake_response(
+            method="GET",
+            status_code=400,
+            response_body="<pre>Worker environment not ready</pre>",
+        ),
+        want_err_type=errors.BadRequest,
+        want_message="Worker environment not ready",
+    ),
+    TestCase(
+        name="unable_to_parse_response",
+        response=fake_response(
+            method="GET",
+            status_code=400,
+            response_body="this is not a real response",
+        ),
+        want_err_type=errors.BadRequest,
+        want_message=(
+            "unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. "
+            "Please report this issue with the following debugging information to the SDK issue tracker at "
+            "https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n"
+            "< 400 Bad Request\n"
+            "< this is not a real response```"
+        ),
+    ),
+    TestCase(
+        name="group_not_found",
+        response=fake_response(
+            method="GET",
+            status_code=404,
+            response_body=json.dumps(
+                {
+                    "detail": "Group with id 1234 is not found",
+                    "status": "404",
+                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
+                }
             ),
-            errors.NotFound,
-            "None Group with id 1234 is not found",
         ),
-        (
-            fake_response("GET", 404, json.dumps("This is JSON but not a dictionary")),
-            errors.NotFound,
-            'unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. Please report this issue with the following debugging information to the SDK issue tracker at https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n< 404 Not Found\n< "This is JSON but not a dictionary"```',
+        want_err_type=errors.NotFound,
+        want_message="None Group with id 1234 is not found",
+    ),
+    TestCase(
+        name="unable_to_parse_response2",
+        response=fake_response(
+            method="GET",
+            status_code=404,
+            response_body=json.dumps("This is JSON but not a dictionary"),
         ),
-        (
-            fake_raw_response("GET", 404, b"\x80"),
-            errors.NotFound,
-            "unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. Please report this issue with the following debugging information to the SDK issue tracker at https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n< 404 Not Found\n< �```",
+        want_err_type=errors.NotFound,
+        want_message='unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. Please report this issue with the following debugging information to the SDK issue tracker at https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n< 404 Not Found\n< "This is JSON but not a dictionary"```',
+    ),
+    TestCase(
+        name="unable_to_parse_response3",
+        response=fake_raw_response(
+            method="GET",
+            status_code=404,
+            response_body=b"\x80",
         ),
-    ],
-)
-def test_get_api_error(response, expected_error, expected_message):
+        want_err_type=errors.NotFound,
+        want_message="unable to parse response. This is likely a bug in the Databricks SDK for Python or the underlying API. Please report this issue with the following debugging information to the SDK issue tracker at https://github.com/databricks/databricks-sdk-go/issues. Request log:```GET /api/2.0/service\n< 404 Not Found\n< �```",
+    ),
+]
+
+
+_all_test_cases = _basic_test_cases_no_details + _test_case_with_details + _test_case_other_errors
+
+
+@pytest.mark.parametrize("test_case", [pytest.param(x, id=x.name) for x in _all_test_cases])
+def test_get_api_error(test_case: TestCase):
     parser = errors._Parser()
+
     with pytest.raises(errors.DatabricksError) as e:
-        raise parser.get_api_error(response)
-    assert isinstance(e.value, expected_error)
-    assert str(e.value) == expected_message
+        raise parser.get_api_error(test_case.response)
+
+    assert isinstance(e.value, test_case.want_err_type)
+    assert str(e.value) == test_case.want_message
+    assert e.value.get_error_details() == test_case.want_details


Reviews:
user: login: hectorcast-db
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T08:15:03Z
user: login: renaudhartert-db
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:25:41Z
user: login: hectorcast-db
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:29:29Z
user: login: renaudhartert-db
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:29:41Z

Review comments:
diff_hunk: @@ -88,20 +98,32 @@ def __init__(
             # add more context from SCIM responses
             message = f"{scimType} {message}".strip(" ")
             error_code = f"SCIM_{status}"
+
         super().__init__(message if message else error)
         self.error_code = error_code
         self.retry_after_secs = retry_after_secs
-        self.details = [ErrorDetail.from_dict(detail) for detail in details] if details else []
+        self._error_details = errdetails.parse_error_details(details)
         self.kwargs = kwargs
 
+        # Deprecated.
+        self.details = []
+        if details:
path: databricks/sdk/errors/base.py
user: login: hectorcast-db
body: `if not isinstance(d, dict):`

Before we didn't check this. Would it just crash?
created_at: 2025-03-05T08:04:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,417 @@
+import re
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class ErrorInfo:
+    """Describes the cause of the error with structured details."""
+
+    reason: str
+    domain: str
+    metadata: Dict[str, str]
+
+
+@dataclass
+class RequestInfo:
+    """
+    Contains metadata about the request that clients can attach when
+    filing a bug or providing other forms of feedback.
+    """
+
+    request_id: str
+    serving_data: str
+
+
+@dataclass
+class RetryInfo:
+    """
+    Describes when the clients can retry a failed request. Clients could
+    ignore the recommendation here or retry when this information is missing
+    from error responses.
+
+    It's always recommended that clients should use exponential backoff
+    when retrying.
+
+    Clients should wait until `retry_delay` amount of time has passed since
+    receiving the error response before retrying.  If retrying requests also
+    fail, clients should use an exponential backoff scheme to gradually
+    increase the delay between retries based on `retry_delay`, until either
+    a maximum number of retries have been reached or a maximum retry delay
+    cap has been reached.
+    """
+
+    retry_delay_seconds: float
+
+
+@dataclass
+class DebugInfo:
+    """Describes additional debugging info."""
+
+    stack_entries: List[str]
+    detail: str
+
+
+@dataclass
+class QuotaFailureViolation:
+    """Describes a single quota violation."""
+
+    subject: str
+    description: str
+
+
+@dataclass
+class QuotaFailure:
+    """
+    Describes how a quota check failed.
+
+    For example if a daily limit was exceeded for the calling project, a
+    service could respond with a QuotaFailure detail containing the project
+    id and the description of the quota limit that was exceeded.  If the
+    calling project hasn't enabled the service in the developer console,
+    then a service could respond with the project id and set
+    `service_disabled` to true.
+
+    Also see RetryInfo and Help types for other details about handling a
+    quota failure.
+    """
+
+    violations: List[QuotaFailureViolation]
+
+
+@dataclass
+class PreconditionFailureViolation:
+    """Describes a single precondition violation."""
+
+    type: str
+    subject: str
+    description: str
+
+
+@dataclass
+class PreconditionFailure:
+    """Describes what preconditions have failed."""
+
+    violations: List[PreconditionFailureViolation]
+
+
+@dataclass
+class BadRequestFieldViolation:
+    """Describes a single field violation in a bad request."""
+
+    field: str
+    description: str
+
+
+@dataclass
+class BadRequest:
+    """
+    Describes violations in a client request. This error type
+    focuses on the syntactic aspects of the request.
+    """
+
+    field_violations: List[BadRequestFieldViolation]
+
+
+@dataclass
+class ResourceInfo:
+    """Describes the resource that is being accessed."""
+
+    resource_type: str
+    resource_name: str
+    owner: str
+    description: str
+
+
+@dataclass
+class HelpLink:
+    """Describes a single help link."""
+
+    description: str
+    url: str
+
+
+@dataclass
+class Help:
+    """
+    Provides links to documentation or for performing an out of
+    band action.
+
+    For example, if a quota check failed with an error indicating
+    the calling project hasn't enabled the accessed service, this
+    can contain a URL pointing directly to the right place in the
+    developer console to flip the bit.
+    """
+
+    links: List[HelpLink]
+
+
+@dataclass
+class ErrorDetails:
+    """
+    ErrorDetails contains the error details of an API error. It
+    is the union of known error details types and unknown details.
+    """
+
+    error_info: Optional[ErrorInfo] = None
+    request_info: Optional[RequestInfo] = None
+    retry_info: Optional[RetryInfo] = None
+    debug_info: Optional[DebugInfo] = None
+    quota_failure: Optional[QuotaFailure] = None
+    precondition_failure: Optional[PreconditionFailure] = None
+    bad_request: Optional[BadRequest] = None
+    resource_info: Optional[ResourceInfo] = None
+    help: Optional[Help] = None
+    unknown_details: List[Any] = field(default_factory=list)
+
+
+# Supported error details proto types.
+_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"
+_REQUEST_INFO_TYPE = "type.googleapis.com/google.rpc.RequestInfo"
+_RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo"
+_DEBUG_INFO_TYPE = "type.googleapis.com/google.rpc.DebugInfo"
+_QUOTA_FAILURE_TYPE = "type.googleapis.com/google.rpc.QuotaFailure"
+_PRECONDITION_FAILURE_TYPE = "type.googleapis.com/google.rpc.PreconditionFailure"
+_BAD_REQUEST_TYPE = "type.googleapis.com/google.rpc.BadRequest"
+_RESOURCE_INFO_TYPE = "type.googleapis.com/google.rpc.ResourceInfo"
+_HELP_TYPE = "type.googleapis.com/google.rpc.Help"
+
+
+def parse_error_details(details: List[Any]) -> ErrorDetails:
+    ed = ErrorDetails()
+
+    if not details:
+        return ed
+
+    for d in details:
+        pd = _parse_json_error_details(d)
path: databricks/sdk/errors/details.py
user: login: hectorcast-db
body: Can we have multiple details of the same type?
created_at: 2025-03-05T08:14:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,417 @@
+import re
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class ErrorInfo:
+    """Describes the cause of the error with structured details."""
+
+    reason: str
+    domain: str
+    metadata: Dict[str, str]
+
+
+@dataclass
+class RequestInfo:
+    """
+    Contains metadata about the request that clients can attach when
+    filing a bug or providing other forms of feedback.
+    """
+
+    request_id: str
+    serving_data: str
+
+
+@dataclass
+class RetryInfo:
+    """
+    Describes when the clients can retry a failed request. Clients could
+    ignore the recommendation here or retry when this information is missing
+    from error responses.
+
+    It's always recommended that clients should use exponential backoff
+    when retrying.
+
+    Clients should wait until `retry_delay` amount of time has passed since
+    receiving the error response before retrying.  If retrying requests also
+    fail, clients should use an exponential backoff scheme to gradually
+    increase the delay between retries based on `retry_delay`, until either
+    a maximum number of retries have been reached or a maximum retry delay
+    cap has been reached.
+    """
+
+    retry_delay_seconds: float
+
+
+@dataclass
+class DebugInfo:
+    """Describes additional debugging info."""
+
+    stack_entries: List[str]
+    detail: str
+
+
+@dataclass
+class QuotaFailureViolation:
+    """Describes a single quota violation."""
+
+    subject: str
+    description: str
+
+
+@dataclass
+class QuotaFailure:
+    """
+    Describes how a quota check failed.
+
+    For example if a daily limit was exceeded for the calling project, a
+    service could respond with a QuotaFailure detail containing the project
+    id and the description of the quota limit that was exceeded.  If the
+    calling project hasn't enabled the service in the developer console,
+    then a service could respond with the project id and set
+    `service_disabled` to true.
+
+    Also see RetryInfo and Help types for other details about handling a
+    quota failure.
+    """
+
+    violations: List[QuotaFailureViolation]
+
+
+@dataclass
+class PreconditionFailureViolation:
+    """Describes a single precondition violation."""
+
+    type: str
+    subject: str
+    description: str
+
+
+@dataclass
+class PreconditionFailure:
+    """Describes what preconditions have failed."""
+
+    violations: List[PreconditionFailureViolation]
+
+
+@dataclass
+class BadRequestFieldViolation:
+    """Describes a single field violation in a bad request."""
+
+    field: str
+    description: str
+
+
+@dataclass
+class BadRequest:
+    """
+    Describes violations in a client request. This error type
+    focuses on the syntactic aspects of the request.
+    """
+
+    field_violations: List[BadRequestFieldViolation]
+
+
+@dataclass
+class ResourceInfo:
+    """Describes the resource that is being accessed."""
+
+    resource_type: str
+    resource_name: str
+    owner: str
+    description: str
+
+
+@dataclass
+class HelpLink:
+    """Describes a single help link."""
+
+    description: str
+    url: str
+
+
+@dataclass
+class Help:
+    """
+    Provides links to documentation or for performing an out of
+    band action.
+
+    For example, if a quota check failed with an error indicating
+    the calling project hasn't enabled the accessed service, this
+    can contain a URL pointing directly to the right place in the
+    developer console to flip the bit.
+    """
+
+    links: List[HelpLink]
+
+
+@dataclass
+class ErrorDetails:
+    """
+    ErrorDetails contains the error details of an API error. It
+    is the union of known error details types and unknown details.
+    """
+
+    error_info: Optional[ErrorInfo] = None
+    request_info: Optional[RequestInfo] = None
+    retry_info: Optional[RetryInfo] = None
+    debug_info: Optional[DebugInfo] = None
+    quota_failure: Optional[QuotaFailure] = None
+    precondition_failure: Optional[PreconditionFailure] = None
+    bad_request: Optional[BadRequest] = None
+    resource_info: Optional[ResourceInfo] = None
+    help: Optional[Help] = None
+    unknown_details: List[Any] = field(default_factory=list)
+
+
+# Supported error details proto types.
+_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"
+_REQUEST_INFO_TYPE = "type.googleapis.com/google.rpc.RequestInfo"
+_RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo"
+_DEBUG_INFO_TYPE = "type.googleapis.com/google.rpc.DebugInfo"
+_QUOTA_FAILURE_TYPE = "type.googleapis.com/google.rpc.QuotaFailure"
+_PRECONDITION_FAILURE_TYPE = "type.googleapis.com/google.rpc.PreconditionFailure"
+_BAD_REQUEST_TYPE = "type.googleapis.com/google.rpc.BadRequest"
+_RESOURCE_INFO_TYPE = "type.googleapis.com/google.rpc.ResourceInfo"
+_HELP_TYPE = "type.googleapis.com/google.rpc.Help"
+
+
+def parse_error_details(details: List[Any]) -> ErrorDetails:
+    ed = ErrorDetails()
+
+    if not details:
+        return ed
+
+    for d in details:
+        pd = _parse_json_error_details(d)
path: databricks/sdk/errors/details.py
user: login: renaudhartert-db
body: No, see PR description:

> The core of the PR is the addition of a new ErrorDetails type which operates as the union of all error details type. It implements the fact that an error response should only contain at most one instance of each type.
created_at: 2025-03-05T10:25:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,20 +98,32 @@ def __init__(
             # add more context from SCIM responses
             message = f"{scimType} {message}".strip(" ")
             error_code = f"SCIM_{status}"
+
         super().__init__(message if message else error)
         self.error_code = error_code
         self.retry_after_secs = retry_after_secs
-        self.details = [ErrorDetail.from_dict(detail) for detail in details] if details else []
+        self._error_details = errdetails.parse_error_details(details)
         self.kwargs = kwargs
 
+        # Deprecated.
+        self.details = []
+        if details:
path: databricks/sdk/errors/base.py
user: login: renaudhartert-db
body: It would have yes. 
created_at: 2025-03-05T10:29:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-04T18:59:23Z
body: <!-- INTEGRATION_TESTS_MANUAL -->
If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
[go/deco-tests-run/sdk-py](https://go/deco-tests-run/sdk-py)

Inputs:
* PR number: 912
* Commit SHA: `cb62d27f87f59083511b0cc385aacd54eedd87cd`

Checks will be approved automatically on success.

author_association: NONE