137 lines
4.3 KiB
Python
137 lines
4.3 KiB
Python
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
|