Skip to content

Commit 27a1975

Browse files
Isolate test
1 parent a646592 commit 27a1975

File tree

3 files changed

+361
-2
lines changed

3 files changed

+361
-2
lines changed

databricks/sdk/errors/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class ErrorDetail:
10-
1110
def __init__(
1211
self,
1312
type: str = None,

databricks/sdk/errors/details.py

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import json
2+
from dataclasses import dataclass
3+
from datetime import timedelta
4+
from typing import Any, Dict, List, Optional
5+
6+
7+
@dataclass
8+
class ErrorInfo:
9+
"""Describes the cause of the error with structured details."""
10+
11+
reason: str
12+
domain: str
13+
metadata: Dict[str, str]
14+
15+
16+
@dataclass
17+
class RequestInfo:
18+
"""
19+
Contains metadata about the request that clients can attach when
20+
filing a bug or providing other forms of feedback.
21+
"""
22+
23+
request_id: str
24+
serving_data: str
25+
26+
27+
@dataclass
28+
class RetryInfo:
29+
"""
30+
Describes when the clients can retry a failed request. Clients could
31+
ignore the recommendation here or retry when this information is missing
32+
from error responses.
33+
34+
It's always recommended that clients should use exponential backoff
35+
when retrying.
36+
37+
Clients should wait until `retry_delay` amount of time has passed since
38+
receiving the error response before retrying. If retrying requests also
39+
fail, clients should use an exponential backoff scheme to gradually
40+
increase the delay between retries based on `retry_delay`, until either
41+
a maximum number of retries have been reached or a maximum retry delay
42+
cap has been reached.
43+
"""
44+
45+
retry_delay: timedelta
46+
47+
48+
@dataclass
49+
class DebugInfo:
50+
"""Describes additional debugging info."""
51+
52+
stack_entries: List[str]
53+
detail: str
54+
55+
56+
@dataclass
57+
class QuotaFailureViolation:
58+
"""Describes a single quota violation."""
59+
60+
subject: str
61+
description: str
62+
63+
64+
@dataclass
65+
class QuotaFailure:
66+
"""
67+
Describes how a quota check failed.
68+
69+
For example if a daily limit was exceeded for the calling project, a
70+
service could respond with a QuotaFailure detail containing the project
71+
id and the description of the quota limit that was exceeded. If the
72+
calling project hasn't enabled the service in the developer console,
73+
then a service could respond with the project id and set
74+
`service_disabled` to true.
75+
76+
Also see RetryInfo and Help types for other details about handling a
77+
quota failure.
78+
"""
79+
80+
violations: List[QuotaFailureViolation]
81+
82+
83+
@dataclass
84+
class PreconditionFailureViolation:
85+
"""Describes a single precondition violation."""
86+
87+
type: str
88+
subject: str
89+
description: str
90+
91+
92+
@dataclass
93+
class PreconditionFailure:
94+
"""Describes what preconditions have failed."""
95+
96+
violations: List[PreconditionFailureViolation]
97+
98+
99+
@dataclass
100+
class BadRequestFieldViolation:
101+
"""Describes a single field violation in a bad request."""
102+
103+
field: str
104+
description: str
105+
106+
107+
@dataclass
108+
class BadRequest:
109+
"""
110+
Describes violations in a client request. This error type
111+
focuses on the syntactic aspects of the request.
112+
"""
113+
114+
field_violations: List[BadRequestFieldViolation]
115+
116+
117+
@dataclass
118+
class ResourceInfo:
119+
"""Describes the resource that is being accessed."""
120+
121+
resource_type: str
122+
resource_name: str
123+
owner: Optional[str] = None
124+
description: Optional[str] = None
125+
126+
127+
@dataclass
128+
class HelpLink:
129+
"""Describes a single help link."""
130+
131+
description: str
132+
url: str
133+
134+
135+
@dataclass
136+
class Help:
137+
"""
138+
Provides links to documentation or for performing an out of
139+
band action.
140+
141+
For example, if a quota check failed with an error indicating
142+
the calling project hasn't enabled the accessed service, this
143+
can contain a URL pointing directly to the right place in the
144+
developer console to flip the bit.
145+
"""
146+
147+
links: List[HelpLink]
148+
149+
150+
@dataclass
151+
class ErrorDetails:
152+
"""
153+
ErrorDetails contains the error details of an API error. It
154+
is the union of known error details types and unknown details.
155+
"""
156+
157+
error_info: Optional[ErrorInfo] = None
158+
request_info: Optional[RequestInfo] = None
159+
retry_info: Optional[RetryInfo] = None
160+
debug_info: Optional[DebugInfo] = None
161+
quota_failure: Optional[QuotaFailure] = None
162+
precondition_failure: Optional[PreconditionFailure] = None
163+
bad_request: Optional[BadRequest] = None
164+
resource_info: Optional[ResourceInfo] = None
165+
help: Optional[Help] = None
166+
unknown_details: List[Any] = []
167+
168+
169+
@dataclass
170+
class _ErrorInfoPb:
171+
reason: str
172+
domain: str
173+
metadata: Dict[str, str]
174+
175+
176+
@dataclass
177+
class _RequestInfoPb:
178+
request_id: str
179+
serving_data: str
180+
181+
182+
@dataclass
183+
class _DurationPb:
184+
seconds: int
185+
nanos: int
186+
187+
188+
@dataclass
189+
class _RetryInfoPb:
190+
retry_delay: _DurationPb
191+
192+
193+
@dataclass
194+
class _DebugInfoPb:
195+
stack_entries: List[str]
196+
detail: str
197+
198+
199+
@dataclass
200+
class _QuotaFailureViolationPb:
201+
subject: str
202+
description: str
203+
204+
205+
@dataclass
206+
class _QuotaFailurePb:
207+
violations: List[_QuotaFailureViolationPb]
208+
209+
210+
@dataclass
211+
class _PreconditionFailureViolationPb:
212+
type: str
213+
subject: str
214+
description: str
215+
216+
217+
@dataclass
218+
class _PreconditionFailurePb:
219+
violations: List[_PreconditionFailureViolationPb]
220+
221+
222+
@dataclass
223+
class _BadRequestFieldViolationPb:
224+
field: str
225+
description: str
226+
227+
228+
@dataclass
229+
class _BadRequestPb:
230+
field_violations: List[_BadRequestFieldViolationPb]
231+
232+
233+
@dataclass
234+
class _ResourceInfoPb:
235+
resource_type: str
236+
resource_name: str
237+
owner: str
238+
description: str
239+
240+
241+
@dataclass
242+
class _HelpLinkPb:
243+
description: str
244+
url: str
245+
246+
247+
@dataclass
248+
class _HelpPb:
249+
links: List[_HelpLinkPb]
250+
251+
252+
# Supported error details proto types.
253+
_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"
254+
_REQUEST_INFO_TYPE = "type.googleapis.com/google.rpc.RequestInfo"
255+
_RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo"
256+
_DEBUG_INFO_TYPE = "type.googleapis.com/google.rpc.DebugInfo"
257+
_QUOTA_FAILURE_TYPE = "type.googleapis.com/google.rpc.QuotaFailure"
258+
_PRECONDITION_FAILURE_TYPE = "type.googleapis.com/google.rpc.PreconditionFailure"
259+
_BAD_REQUEST_TYPE = "type.googleapis.com/google.rpc.BadRequest"
260+
_RESOURCE_INFO_TYPE = "type.googleapis.com/google.rpc.ResourceInfo"
261+
_HELP_TYPE = "type.googleapis.com/google.rpc.Help"
262+
263+
264+
def parse_error_details(details: List[Any]) -> ErrorDetails:
265+
ed = ErrorDetails()
266+
for d in details:
267+
if isinstance(d, ErrorInfo):
268+
ed.error_info = d
269+
elif isinstance(d, RequestInfo):
270+
ed.request_info = d
271+
elif isinstance(d, RetryInfo):
272+
ed.retry_info = d
273+
elif isinstance(d, DebugInfo):
274+
ed.debug_info = d
275+
elif isinstance(d, QuotaFailure):
276+
ed.quota_failure = d
277+
elif isinstance(d, PreconditionFailure):
278+
ed.precondition_failure = d
279+
elif isinstance(d, BadRequest):
280+
ed.bad_request = d
281+
elif isinstance(d, ResourceInfo):
282+
ed.resource_info = d
283+
elif isinstance(d, Help):
284+
ed.help = d
285+
else:
286+
ed.unknown_details.append(d)
287+
return ed
288+
289+
290+
def unmarshal_details(d: bytes) -> Any:
291+
"""
292+
Attempts to unmarshal the given bytes into a known error details type.
293+
294+
It works as follows:
295+
296+
- If the message is a known type, it unmarshals the message into that type.
297+
- If the message is not a known type, it returns the results of calling
298+
json.loads() on the raw message.
299+
- If json.loads() fails, it returns the input as is.
300+
"""
301+
302+
try:
303+
a = json.loads(d)
304+
except json.JSONDecodeError:
305+
return d # not a valid JSON message
306+
307+
if not isinstance(a, dict):
308+
return a # not a JSON object
309+
310+
t = a.get("@type")
311+
if not isinstance(t, str):
312+
return a # JSON object with no @type field
313+
314+
try:
315+
if t == _ERROR_INFO_TYPE:
316+
pb = _ErrorInfoPb(**a)
317+
return ErrorInfo(**pb.__dict__)
318+
elif t == _REQUEST_INFO_TYPE:
319+
pb = _RequestInfoPb(**a)
320+
return RequestInfo(**pb.__dict__)
321+
elif t == _RETRY_INFO_TYPE:
322+
pb = _RetryInfoPb(**a)
323+
return RetryInfo(
324+
retry_delay=timedelta(
325+
seconds=pb.retry_delay.seconds,
326+
microseconds=pb.retry_delay.nanos / 1000,
327+
)
328+
)
329+
elif t == _DEBUG_INFO_TYPE:
330+
pb = _DebugInfoPb(**a)
331+
return DebugInfo(**pb.__dict__)
332+
elif t == _QUOTA_FAILURE_TYPE:
333+
pb = _QuotaFailurePb(**a)
334+
violations = [QuotaFailureViolation(**v.__dict__) for v in pb.violations]
335+
return QuotaFailure(violations=violations)
336+
elif t == _PRECONDITION_FAILURE_TYPE:
337+
pb = _PreconditionFailurePb(**a)
338+
violations = [PreconditionFailureViolation(**v.__dict__) for v in pb.violations]
339+
return PreconditionFailure(violations=violations)
340+
elif t == _BAD_REQUEST_TYPE:
341+
pb = _BadRequestPb(**a)
342+
field_violations = [BadRequestFieldViolation(**v.__dict__) for v in pb.field_violations]
343+
return BadRequest(field_violations=field_violations)
344+
elif t == _RESOURCE_INFO_TYPE:
345+
pb = _ResourceInfoPb(**a)
346+
return ResourceInfo(**pb.__dict__)
347+
elif t == _HELP_TYPE:
348+
pb = _HelpPb(**a)
349+
links = [HelpLink(**l.__dict__) for l in pb.links]
350+
return Help(links=links)
351+
except (TypeError, ValueError):
352+
return a # not a valid known type
353+
354+
return a # unknown type

tests/test_model_serving_auth.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ def test_model_serving_auth_refresh(monkeypatch, mocker):
162162

163163

164164
def test_agent_user_credentials(monkeypatch, mocker):
165+
# Guarantee that the tests defaults to env variables rather than config file.
166+
#
167+
# TODO: this is hacky and we should find a better way to tell the config
168+
# that it should not read from the config file.
169+
monkeypatch.setenv("DATABRICKS_CONFIG_FILE", "x")
170+
165171
monkeypatch.setenv("IS_IN_DB_MODEL_SERVING_ENV", "true")
166172
monkeypatch.setenv("DB_MODEL_SERVING_HOST_URL", "x")
167173
monkeypatch.setattr(
@@ -205,4 +211,4 @@ def test_agent_user_credentials_in_non_model_serving_environments(monkeypatch):
205211
headers = cfg.authenticate()
206212

207213
assert cfg.host == "https://x"
208-
assert headers.get("Authorization") == f"Bearer token"
214+
assert headers.get("Authorization") == "Bearer token"

0 commit comments

Comments
 (0)