This commit is contained in:
Christian Mantha
2026-03-02 19:10:52 -05:00
commit 2ca0b9ef7c
28907 changed files with 5233713 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
from .base import DatabricksError, ErrorDetail
from .customizer import _ErrorCustomizer
from .parser import _Parser
from .platform import *
from .private_link import PrivateLinkValidationError
from .sdk import *

View File

@@ -0,0 +1,136 @@
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import requests
from . import details as errdetails
# Deprecated.
class ErrorDetail:
def __init__(
self,
type: Optional[str] = None,
reason: Optional[str] = None,
domain: Optional[str] = None,
metadata: Optional[dict] = None,
**kwargs,
):
self.type = type
self.reason = reason
self.domain = domain
self.metadata = metadata
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ErrorDetail":
# 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"""
def __init__(
self,
message: Optional[str] = None,
*,
error_code: Optional[str] = None,
detail: Optional[str] = None,
status: Optional[str] = None,
scimType: Optional[str] = None,
error: Optional[str] = None,
retry_after_secs: Optional[int] = None,
details: Optional[List[Dict[str, Any]]] = None,
**kwargs,
):
"""
:param message:
:param error_code:
:param detail: [Deprecated]
:param status: [Deprecated]
:param scimType: [Deprecated]
:param error: [Deprecated]
:param retry_after_secs: [Deprecated]
:param details:
:param kwargs:
"""
if detail:
# Handle SCIM error message details
# @see https://tools.ietf.org/html/rfc7644#section-3.7.3
if detail == "null":
message = "SCIM API Internal Error"
else:
message = detail
# 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._error_details = errdetails.parse_error_details(details or [])
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]:
if self.details is None:
return []
return [detail for detail in self.details if detail.type == errdetails._ERROR_INFO_TYPE]
def get_error_details(self) -> errdetails.ErrorDetails:
return self._error_details
@dataclass
class _ErrorOverride:
# The name of the override. Used for logging purposes.
debug_name: str
# A regex that must match the path of the request for this override to be applied.
path_regex: re.Pattern
# The HTTP method of the request for the override to apply
verb: str
# The custom error class to use for this override.
custom_error: type
# A regular expression that must match the error code for this override to be applied. If None,
# this field is ignored.
status_code_matcher: Optional[re.Pattern] = None
# A regular expression that must match the error code for this override to be applied. If None,
# this field is ignored.
error_code_matcher: Optional[re.Pattern] = None
# A regular expression that must match the message for this override to be applied. If None,
# this field is ignored.
message_matcher: Optional[re.Pattern] = None
def matches(self, response: requests.Response, raw_error: dict):
if response.request.method != self.verb:
return False
if not self.path_regex.match(response.request.path_url):
return False
if self.status_code_matcher and not self.status_code_matcher.match(str(response.status_code)):
return False
if self.error_code_matcher and not self.error_code_matcher.match(raw_error.get("error_code", "")):
return False
if self.message_matcher and not self.message_matcher.match(raw_error.get("message", "")):
return False
return True

View File

@@ -0,0 +1,50 @@
import abc
import logging
import requests
class _ErrorCustomizer(abc.ABC):
"""A customizer for errors from the Databricks REST API."""
@abc.abstractmethod
def customize_error(self, response: requests.Response, kwargs: dict):
"""Customize the error constructor parameters."""
class _RetryAfterCustomizer(_ErrorCustomizer):
"""An error customizer that sets the retry_after_secs parameter based on the Retry-After header."""
_DEFAULT_RETRY_AFTER_SECONDS = 1
"""The default number of seconds to wait before retrying a request if the Retry-After header is missing or is not
a valid integer."""
@classmethod
def _parse_retry_after(cls, response: requests.Response) -> int:
retry_after = response.headers.get("Retry-After")
if retry_after is None:
logging.debug(
f"No Retry-After header received in response with status code 429 or 503. Defaulting to {cls._DEFAULT_RETRY_AFTER_SECONDS}"
)
# 429 requests should include a `Retry-After` header, but if it's missing,
# we default to 1 second.
return cls._DEFAULT_RETRY_AFTER_SECONDS
# If the request is throttled, try parse the `Retry-After` header and sleep
# for the specified number of seconds. Note that this header can contain either
# an integer or a RFC1123 datetime string.
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
#
# For simplicity, we only try to parse it as an integer, as this is what Databricks
# platform returns. Otherwise, we fall back and don't sleep.
try:
return int(retry_after)
except ValueError:
logging.debug(
f"Invalid Retry-After header received: {retry_after}. Defaulting to {cls._DEFAULT_RETRY_AFTER_SECONDS}"
)
# defaulting to 1 sleep second to make self._is_retryable() simpler
return cls._DEFAULT_RETRY_AFTER_SECONDS
def customize_error(self, response: requests.Response, kwargs: dict):
if response.status_code in (429, 503):
kwargs["retry_after_secs"] = self._parse_retry_after(response)

View File

@@ -0,0 +1,119 @@
import abc
import json
import logging
import re
from typing import Optional
import requests
class _ErrorDeserializer(abc.ABC):
"""A parser for errors from the Databricks REST API."""
@abc.abstractmethod
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
"""Parses an error from the Databricks REST API. If the error cannot be parsed, returns None."""
class _EmptyDeserializer(_ErrorDeserializer):
"""A parser that handles empty responses."""
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
if len(response_body) == 0:
return {"message": response.reason}
return None
class _StandardErrorDeserializer(_ErrorDeserializer):
"""
Parses errors from the Databricks REST API using the standard error format.
"""
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
try:
payload_str = response_body.decode("utf-8")
resp = json.loads(payload_str)
except UnicodeDecodeError as e:
logging.debug(
"_StandardErrorParser: unable to decode response using utf-8",
exc_info=e,
)
return None
except json.JSONDecodeError as e:
logging.debug(
"_StandardErrorParser: unable to deserialize response as json",
exc_info=e,
)
return None
if not isinstance(resp, dict):
logging.debug("_StandardErrorParser: response is valid JSON but not a dictionary")
return None
error_args = {
"message": resp.get("message", "request failed"),
"error_code": resp.get("error_code"),
"details": resp.get("details"),
}
# Handle API 1.2-style errors
if "error" in resp:
error_args["message"] = resp["error"]
# Handle SCIM Errors
detail = resp.get("detail")
status = resp.get("status")
scim_type = resp.get("scimType")
if detail:
# Handle SCIM error message details
# @see https://tools.ietf.org/html/rfc7644#section-3.7.3
if detail == "null":
detail = "SCIM API Internal Error"
error_args["message"] = f"{scim_type} {detail}".strip(" ")
error_args["error_code"] = f"SCIM_{status}"
return error_args
class _StringErrorDeserializer(_ErrorDeserializer):
"""
Parses errors from the Databricks REST API in the format "ERROR_CODE: MESSAGE".
"""
__STRING_ERROR_REGEX = re.compile(r"([A-Z_]+): (.*)")
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
payload_str = response_body.decode("utf-8")
match = self.__STRING_ERROR_REGEX.match(payload_str)
if not match:
logging.debug("_StringErrorParser: unable to parse response as string")
return None
error_code, message = match.groups()
return {
"error_code": error_code,
"message": message,
"status": response.status_code,
}
class _HtmlErrorDeserializer(_ErrorDeserializer):
"""
Parses errors from the Databricks REST API in HTML format.
"""
__HTML_ERROR_REGEXES = [
re.compile(r"<pre>(.*)</pre>"),
re.compile(r"<title>(.*)</title>"),
]
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
payload_str = response_body.decode("utf-8")
for regex in self.__HTML_ERROR_REGEXES:
match = regex.search(payload_str)
if match:
message = match.group(1) if match.group(1) else response.reason
return {
"status": response.status_code,
"message": message,
"error_code": response.reason.upper().replace(" ", "_"),
}
logging.debug("_HtmlErrorParser: no <pre> tag found in error response")
return None

View File

@@ -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}")

View File

@@ -0,0 +1,27 @@
import requests
from databricks.sdk.errors import platform
from databricks.sdk.errors.base import DatabricksError
from .overrides import _ALL_OVERRIDES
def _error_mapper(response: requests.Response, raw: dict) -> DatabricksError:
for override in _ALL_OVERRIDES:
if override.matches(response, raw):
return override.custom_error(**raw)
status_code = response.status_code
error_code = raw.get("error_code", None)
if error_code in platform.ERROR_CODE_MAPPING:
# more specific error codes override more generic HTTP status codes
return platform.ERROR_CODE_MAPPING[error_code](**raw)
if status_code in platform.STATUS_CODE_MAPPING:
# more generic HTTP status codes matched after more specific error codes,
# where there's a default exception class per HTTP status code, and we do
# rely on Databricks platform exception mapper to do the right thing.
return platform.STATUS_CODE_MAPPING[status_code](**raw)
# backwards-compatible error creation for cases like using older versions of
# the SDK on way never releases of the platform.
return DatabricksError(**raw)

View File

@@ -0,0 +1,36 @@
# Code generated from OpenAPI specs by Databricks SDK Generator. DO NOT EDIT.
import re
from .base import _ErrorOverride
from .platform import ResourceDoesNotExist
_ALL_OVERRIDES = [
_ErrorOverride(
debug_name="Clusters InvalidParameterValue=>ResourceDoesNotExist",
path_regex=re.compile(r"^/api/2\.\d/clusters/get"),
verb="GET",
status_code_matcher=re.compile(r"^400$"),
error_code_matcher=re.compile(r"INVALID_PARAMETER_VALUE"),
message_matcher=re.compile(r"Cluster .* does not exist"),
custom_error=ResourceDoesNotExist,
),
_ErrorOverride(
debug_name="Jobs InvalidParameterValue=>ResourceDoesNotExist",
path_regex=re.compile(r"^/api/2\.\d/jobs/get"),
verb="GET",
status_code_matcher=re.compile(r"^400$"),
error_code_matcher=re.compile(r"INVALID_PARAMETER_VALUE"),
message_matcher=re.compile(r"Job .* does not exist"),
custom_error=ResourceDoesNotExist,
),
_ErrorOverride(
debug_name="Job Runs InvalidParameterValue=>ResourceDoesNotExist",
path_regex=re.compile(r"^/api/2\.\d/jobs/runs/get"),
verb="GET",
status_code_matcher=re.compile(r"^400$"),
error_code_matcher=re.compile(r"INVALID_PARAMETER_VALUE"),
message_matcher=re.compile(r"(Run .* does not exist|Run: .* in job: .* doesn\'t exist)"),
custom_error=ResourceDoesNotExist,
),
]

View File

@@ -0,0 +1,100 @@
import logging
from typing import List, Optional
import requests
from ..logger import RoundTrip
from .base import DatabricksError
from .customizer import _ErrorCustomizer, _RetryAfterCustomizer
from .deserializer import (_EmptyDeserializer, _ErrorDeserializer,
_HtmlErrorDeserializer, _StandardErrorDeserializer,
_StringErrorDeserializer)
from .mapper import _error_mapper
from .private_link import (_get_private_link_validation_error,
_is_private_link_redirect)
# A list of _ErrorDeserializers that are tried in order to parse an API error from a response body. Most errors should
# be parsable by the _StandardErrorDeserializer, but additional parsers can be added here for specific error formats.
# The order of the parsers is not important, as the set of errors that can be parsed by each parser should be disjoint.
_error_deserializers = [
_EmptyDeserializer(),
_StandardErrorDeserializer(),
_StringErrorDeserializer(),
_HtmlErrorDeserializer(),
]
# A list of _ErrorCustomizers that are applied to the error arguments after they are parsed. Customizers can modify the
# error arguments in any way, including adding or removing fields. Customizers are applied in order, so later
# customizers can override the changes made by earlier customizers.
_error_customizers = [
_RetryAfterCustomizer(),
]
def _unknown_error(response: requests.Response, debug_headers: bool = False) -> str:
"""A standard error message that can be shown when an API response cannot be parsed.
This error message includes a link to the issue tracker for the SDK for users to report the issue to us.
:param response: The response object from the API request.
:param debug_headers: Whether to include headers in the request log. Defaults to False to defensively handle cases where request headers might contain sensitive data (e.g. tokens).
"""
request_log = RoundTrip(response, debug_headers=debug_headers, debug_truncate_bytes=10 * 1024).generate()
return (
"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 "
f"https://github.com/databricks/databricks-sdk-go/issues. Request log:```{request_log}```"
)
class _Parser:
"""
A parser for errors from the Databricks REST API. It attempts to deserialize an error using a sequence of
deserializers, and then customizes the deserialized error using a sequence of customizers. If the error cannot be
deserialized, it returns a generic error with debugging information and instructions to report the issue to the SDK
issue tracker.
"""
def __init__(
self,
extra_error_parsers: List[_ErrorDeserializer] = [],
extra_error_customizers: List[_ErrorCustomizer] = [],
debug_headers: bool = False,
):
self._error_parsers = _error_deserializers + (extra_error_parsers if extra_error_parsers is not None else [])
self._error_customizers = _error_customizers + (
extra_error_customizers if extra_error_customizers is not None else []
)
self._debug_headers = debug_headers
def get_api_error(self, response: requests.Response) -> Optional[DatabricksError]:
"""
Handles responses from the REST API and returns a DatabricksError if the response indicates an error.
:param response: The response from the REST API.
:return: A DatabricksError if the response indicates an error, otherwise None.
"""
if not response.ok:
content = response.content
for parser in self._error_parsers:
try:
error_args = parser.deserialize_error(response, content)
if error_args:
for customizer in self._error_customizers:
customizer.customize_error(response, error_args)
return _error_mapper(response, error_args)
except Exception as e:
logging.debug(
f"Error parsing response with {parser}, continuing",
exc_info=e,
)
return _error_mapper(
response,
{"message": "unable to parse response. " + _unknown_error(response, self._debug_headers)},
)
# Private link failures happen via a redirect to the login page. From a requests-perspective, the request
# 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

View File

@@ -0,0 +1,116 @@
# Code generated from OpenAPI specs by Databricks SDK Generator. DO NOT EDIT.
from .base import DatabricksError
class BadRequest(DatabricksError):
"""the request is invalid"""
class Unauthenticated(DatabricksError):
"""the request does not have valid authentication (AuthN) credentials for the operation"""
class PermissionDenied(DatabricksError):
"""the caller does not have permission to execute the specified operation"""
class NotFound(DatabricksError):
"""the operation was performed on a resource that does not exist"""
class ResourceConflict(DatabricksError):
"""maps to all HTTP 409 (Conflict) responses"""
class TooManyRequests(DatabricksError):
"""maps to HTTP code: 429 Too Many Requests"""
class Cancelled(DatabricksError):
"""the operation was explicitly canceled by the caller"""
class InternalError(DatabricksError):
"""some invariants expected by the underlying system have been broken"""
class NotImplemented(DatabricksError):
"""the operation is not implemented or is not supported/enabled in this service"""
class TemporarilyUnavailable(DatabricksError):
"""the service is currently unavailable"""
class DeadlineExceeded(DatabricksError):
"""the deadline expired before the operation could complete"""
class InvalidState(BadRequest):
"""unexpected state"""
class InvalidParameterValue(BadRequest):
"""supplied value for a parameter was invalid"""
class ResourceDoesNotExist(NotFound):
"""operation was performed on a resource that does not exist"""
class Aborted(ResourceConflict):
"""the operation was aborted, typically due to a concurrency issue such as a sequencer check
failure"""
class AlreadyExists(ResourceConflict):
"""operation was rejected due a conflict with an existing resource"""
class ResourceAlreadyExists(ResourceConflict):
"""operation was rejected due a conflict with an existing resource"""
class ResourceExhausted(TooManyRequests):
"""operation is rejected due to per-user rate limiting"""
class RequestLimitExceeded(TooManyRequests):
"""cluster request was rejected because it would exceed a resource limit"""
class Unknown(InternalError):
"""this error is used as a fallback if the platform-side mapping is missing some reason"""
class DataLoss(InternalError):
"""unrecoverable data loss or corruption"""
STATUS_CODE_MAPPING = {
400: BadRequest,
401: Unauthenticated,
403: PermissionDenied,
404: NotFound,
409: ResourceConflict,
429: TooManyRequests,
499: Cancelled,
500: InternalError,
501: NotImplemented,
503: TemporarilyUnavailable,
504: DeadlineExceeded,
}
ERROR_CODE_MAPPING = {
"INVALID_STATE": InvalidState,
"INVALID_PARAMETER_VALUE": InvalidParameterValue,
"RESOURCE_DOES_NOT_EXIST": ResourceDoesNotExist,
"ABORTED": Aborted,
"ALREADY_EXISTS": AlreadyExists,
"RESOURCE_ALREADY_EXISTS": ResourceAlreadyExists,
"RESOURCE_EXHAUSTED": ResourceExhausted,
"REQUEST_LIMIT_EXCEEDED": RequestLimitExceeded,
"UNKNOWN": Unknown,
"DATA_LOSS": DataLoss,
}

View File

@@ -0,0 +1,60 @@
from dataclasses import dataclass
from urllib import parse
import requests
from ..environments import Cloud, get_environment_for_hostname
from .platform import PermissionDenied
@dataclass
class _PrivateLinkInfo:
serviceName: str
endpointName: str
referencePage: str
def error_message(self):
return (
f"The requested workspace has {self.serviceName} enabled and is not accessible from the current network. "
f"Ensure that {self.serviceName} is properly configured and that your device has access to the "
f"{self.endpointName}. For more information, see {self.referencePage}."
)
_private_link_info_map = {
Cloud.AWS: _PrivateLinkInfo(
serviceName="AWS PrivateLink",
endpointName="AWS VPC endpoint",
referencePage="https://docs.databricks.com/en/security/network/classic/privatelink.html",
),
Cloud.AZURE: _PrivateLinkInfo(
serviceName="Azure Private Link",
endpointName="Azure Private Link endpoint",
referencePage="https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link-standard#authentication-troubleshooting",
),
Cloud.GCP: _PrivateLinkInfo(
serviceName="Private Service Connect",
endpointName="GCP VPC endpoint",
referencePage="https://docs.gcp.databricks.com/en/security/network/classic/private-service-connect.html",
),
}
class PrivateLinkValidationError(PermissionDenied):
"""Raised when a user tries to access a Private Link-enabled workspace, but the user's network does not have access
to the workspace."""
def _is_private_link_redirect(resp: requests.Response) -> bool:
parsed = parse.urlparse(resp.url)
return parsed.path == "/login.html" and "error=private-link-validation-error" in parsed.query
def _get_private_link_validation_error(url: str) -> PrivateLinkValidationError:
parsed = parse.urlparse(url)
env = get_environment_for_hostname(parsed.hostname)
return PrivateLinkValidationError(
message=_private_link_info_map[env.cloud].error_message(),
error_code="PRIVATE_LINK_VALIDATION_ERROR",
status_code=403,
)

View File

@@ -0,0 +1,6 @@
class OperationFailed(RuntimeError):
pass
class OperationTimeout(RuntimeError, TimeoutError):
pass