Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/azure/core/exceptions.py: 38%
212 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 06:33 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 06:33 +0000
1# --------------------------------------------------------------------------
2#
3# Copyright (c) Microsoft Corporation. All rights reserved.
4#
5# The MIT License (MIT)
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the ""Software""), to
9# deal in the Software without restriction, including without limitation the
10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11# sell copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24#
25# --------------------------------------------------------------------------
26from __future__ import annotations
27import json
28import logging
29import sys
31from types import TracebackType
32from typing import (
33 Callable,
34 Any,
35 Optional,
36 Union,
37 Type,
38 List,
39 Mapping,
40 TypeVar,
41 Generic,
42 Dict,
43 NoReturn,
44 TYPE_CHECKING,
45)
46from typing_extensions import Protocol, runtime_checkable
48_LOGGER = logging.getLogger(__name__)
50if TYPE_CHECKING:
51 from azure.core.pipeline.policies import RequestHistory
53HTTPResponseType = TypeVar("HTTPResponseType")
54HTTPRequestType = TypeVar("HTTPRequestType")
55KeyType = TypeVar("KeyType")
56ValueType = TypeVar("ValueType")
57# To replace when typing.Self is available in our baseline
58SelfODataV4Format = TypeVar("SelfODataV4Format", bound="ODataV4Format")
61__all__ = [
62 "AzureError",
63 "ServiceRequestError",
64 "ServiceResponseError",
65 "HttpResponseError",
66 "DecodeError",
67 "ResourceExistsError",
68 "ResourceNotFoundError",
69 "ClientAuthenticationError",
70 "ResourceModifiedError",
71 "ResourceNotModifiedError",
72 "TooManyRedirectsError",
73 "ODataV4Format",
74 "ODataV4Error",
75 "StreamConsumedError",
76 "StreamClosedError",
77 "ResponseNotReadError",
78 "SerializationError",
79 "DeserializationError",
80]
83def raise_with_traceback(exception: Callable, *args: Any, message: str = "", **kwargs: Any) -> NoReturn:
84 """Raise exception with a specified traceback.
85 This MUST be called inside a "except" clause.
87 .. note:: This method is deprecated since we don't support Python 2 anymore. Use raise/from instead.
89 :param Exception exception: Error type to be raised.
90 :param any args: Any additional args to be included with exception.
91 :keyword str message: Message to be associated with the exception. If omitted, defaults to an empty string.
92 """
93 exc_type, exc_value, exc_traceback = sys.exc_info()
94 # If not called inside an "except", exc_type will be None. Assume it will not happen
95 if exc_type is None:
96 raise ValueError("raise_with_traceback can only be used in except clauses")
97 exc_msg = "{}, {}: {}".format(message, exc_type.__name__, exc_value)
98 error = exception(exc_msg, *args, **kwargs)
99 try:
100 raise error.with_traceback(exc_traceback) # pylint: disable=raise-missing-from
101 except AttributeError: # Python 2
102 error.__traceback__ = exc_traceback
103 raise error # pylint: disable=raise-missing-from
106@runtime_checkable
107class _HttpResponseCommonAPI(Protocol):
108 """Protocol used by exceptions for HTTP response.
110 As HttpResponseError uses very few properties of HttpResponse, a protocol
111 is faster and simpler than import all the possible types (at least 6).
112 """
114 @property
115 def reason(self) -> Optional[str]:
116 ...
118 @property
119 def status_code(self) -> Optional[int]:
120 ...
122 def text(self) -> str:
123 ...
125 @property
126 def request(self) -> object: # object as type, since all we need is str() on it
127 ...
130class ErrorMap(Generic[KeyType, ValueType]):
131 """Error Map class. To be used in map_error method, behaves like a dictionary.
132 It returns the error type if it is found in custom_error_map. Or return default_error
134 :param dict custom_error_map: User-defined error map, it is used to map status codes to error types.
135 :keyword error default_error: Default error type. It is returned if the status code is not found in custom_error_map
136 """
138 def __init__(
139 self, # pylint: disable=unused-argument
140 custom_error_map: Optional[Mapping[KeyType, ValueType]] = None,
141 *,
142 default_error: Optional[ValueType] = None,
143 **kwargs: Any,
144 ) -> None:
145 self._custom_error_map = custom_error_map or {}
146 self._default_error = default_error
148 def get(self, key: KeyType) -> Optional[ValueType]:
149 ret = self._custom_error_map.get(key)
150 if ret:
151 return ret
152 return self._default_error
155def map_error(
156 status_code: int, response: _HttpResponseCommonAPI, error_map: Mapping[int, Type[HttpResponseError]]
157) -> None:
158 if not error_map:
159 return
160 error_type = error_map.get(status_code)
161 if not error_type:
162 return
163 error = error_type(response=response)
164 raise error
167class ODataV4Format:
168 """Class to describe OData V4 error format.
170 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
172 Example of JSON:
174 .. code-block:: json
176 {
177 "error": {
178 "code": "ValidationError",
179 "message": "One or more fields contain incorrect values: ",
180 "details": [
181 {
182 "code": "ValidationError",
183 "target": "representation",
184 "message": "Parsing error(s): String '' does not match regex pattern '^[^{}/ :]+(?: :\\\\d+)?$'.
185 Path 'host', line 1, position 297."
186 },
187 {
188 "code": "ValidationError",
189 "target": "representation",
190 "message": "Parsing error(s): The input OpenAPI file is not valid for the OpenAPI specificate
191 https: //github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
192 (schema https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json)."
193 }
194 ]
195 }
196 }
198 :param dict json_object: A Python dict representing a ODataV4 JSON
199 :ivar str ~.code: Its value is a service-defined error code.
200 This code serves as a sub-status for the HTTP error code specified in the response.
201 :ivar str message: Human-readable, language-dependent representation of the error.
202 :ivar str target: The target of the particular error (for example, the name of the property in error).
203 This field is optional and may be None.
204 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
205 for code and message, and MAY contain a name/value pair for target, as described above.
206 :ivar dict innererror: An object. The contents of this object are service-defined.
207 Usually this object contains information that will help debug the service.
208 """
210 CODE_LABEL = "code"
211 MESSAGE_LABEL = "message"
212 TARGET_LABEL = "target"
213 DETAILS_LABEL = "details"
214 INNERERROR_LABEL = "innererror"
216 def __init__(self, json_object: Mapping[str, Any]) -> None:
217 if "error" in json_object:
218 json_object = json_object["error"]
219 cls: Type[ODataV4Format] = self.__class__
221 # Required fields, but assume they could be missing still to be robust
222 self.code: Optional[str] = json_object.get(cls.CODE_LABEL)
223 self.message: Optional[str] = json_object.get(cls.MESSAGE_LABEL)
225 if not (self.code or self.message):
226 raise ValueError("Impossible to extract code/message from received JSON:\n" + json.dumps(json_object))
228 # Optional fields
229 self.target: Optional[str] = json_object.get(cls.TARGET_LABEL)
231 # details is recursive of this very format
232 self.details: List[ODataV4Format] = []
233 for detail_node in json_object.get(cls.DETAILS_LABEL) or []:
234 try:
235 self.details.append(self.__class__(detail_node))
236 except Exception: # pylint: disable=broad-except
237 pass
239 self.innererror: Mapping[str, Any] = json_object.get(cls.INNERERROR_LABEL, {})
241 @property
242 def error(self: SelfODataV4Format) -> SelfODataV4Format:
243 import warnings
245 warnings.warn(
246 "error.error from azure exceptions is deprecated, just simply use 'error' once",
247 DeprecationWarning,
248 )
249 return self
251 def __str__(self) -> str:
252 return "({}) {}\n{}".format(self.code, self.message, self.message_details())
254 def message_details(self) -> str:
255 """Return a detailed string of the error.
257 :return: A string with the details of the error.
258 :rtype: str
259 """
260 error_str = "Code: {}".format(self.code)
261 error_str += "\nMessage: {}".format(self.message)
262 if self.target:
263 error_str += "\nTarget: {}".format(self.target)
265 if self.details:
266 error_str += "\nException Details:"
267 for error_obj in self.details:
268 # Indent for visibility
269 error_str += "\n".join("\t" + s for s in str(error_obj).splitlines())
271 if self.innererror:
272 error_str += "\nInner error: {}".format(json.dumps(self.innererror, indent=4))
273 return error_str
276class AzureError(Exception):
277 """Base exception for all errors.
279 :param object message: The message object stringified as 'message' attribute
280 :keyword error: The original exception if any
281 :paramtype error: Exception
283 :ivar inner_exception: The exception passed with the 'error' kwarg
284 :vartype inner_exception: Exception
285 :ivar exc_type: The exc_type from sys.exc_info()
286 :ivar exc_value: The exc_value from sys.exc_info()
287 :ivar exc_traceback: The exc_traceback from sys.exc_info()
288 :ivar exc_msg: A string formatting of message parameter, exc_type and exc_value
289 :ivar str message: A stringified version of the message parameter
290 :ivar str continuation_token: A token reference to continue an incomplete operation. This value is optional
291 and will be `None` where continuation is either unavailable or not applicable.
292 """
294 def __init__(self, message: Optional[object], *args: Any, **kwargs: Any) -> None:
295 self.inner_exception: Optional[BaseException] = kwargs.get("error")
297 exc_info = sys.exc_info()
298 self.exc_type: Optional[Type[Any]] = exc_info[0]
299 self.exc_value: Optional[BaseException] = exc_info[1]
300 self.exc_traceback: Optional[TracebackType] = exc_info[2]
302 self.exc_type = self.exc_type if self.exc_type else type(self.inner_exception)
303 self.exc_msg: str = "{}, {}: {}".format(message, self.exc_type.__name__, self.exc_value)
304 self.message: str = str(message)
305 self.continuation_token: Optional[str] = kwargs.get("continuation_token")
306 super(AzureError, self).__init__(self.message, *args)
308 def raise_with_traceback(self) -> None:
309 """Raise the exception with the existing traceback.
311 .. deprecated:: 1.22.0
312 This method is deprecated as we don't support Python 2 anymore. Use raise/from instead.
313 """
314 try:
315 raise super(AzureError, self).with_traceback(self.exc_traceback) # pylint: disable=raise-missing-from
316 except AttributeError:
317 self.__traceback__: Optional[TracebackType] = self.exc_traceback
318 raise self # pylint: disable=raise-missing-from
321class ServiceRequestError(AzureError):
322 """An error occurred while attempt to make a request to the service.
323 No request was sent.
324 """
327class ServiceResponseError(AzureError):
328 """The request was sent, but the client failed to understand the response.
329 The connection may have timed out. These errors can be retried for idempotent or
330 safe operations"""
333class ServiceRequestTimeoutError(ServiceRequestError):
334 """Error raised when timeout happens"""
337class ServiceResponseTimeoutError(ServiceResponseError):
338 """Error raised when timeout happens"""
341class HttpResponseError(AzureError):
342 """A request was made, and a non-success status code was received from the service.
344 :param object message: The message object stringified as 'message' attribute
345 :param response: The response that triggered the exception.
346 :type response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
348 :ivar reason: The HTTP response reason
349 :vartype reason: str
350 :ivar status_code: HttpResponse's status code
351 :vartype status_code: int
352 :ivar response: The response that triggered the exception.
353 :vartype response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
354 :ivar model: The request body/response body model
355 :vartype model: ~msrest.serialization.Model
356 :ivar error: The formatted error
357 :vartype error: ODataV4Format
358 """
360 def __init__(
361 self, message: Optional[object] = None, response: Optional[_HttpResponseCommonAPI] = None, **kwargs: Any
362 ) -> None:
363 # Don't want to document this one yet.
364 error_format = kwargs.get("error_format", ODataV4Format)
366 self.reason: Optional[str] = None
367 self.status_code: Optional[int] = None
368 self.response: Optional[_HttpResponseCommonAPI] = response
369 if response:
370 self.reason = response.reason
371 self.status_code = response.status_code
373 # old autorest are setting "error" before calling __init__, so it might be there already
374 # transferring into self.model
375 model: Optional[Any] = kwargs.pop("model", None)
376 self.model: Optional[Any]
377 if model is not None: # autorest v5
378 self.model = model
379 else: # autorest azure-core, for KV 1.0, Storage 12.0, etc.
380 self.model = getattr(self, "error", None)
381 self.error: Optional[ODataV4Format] = self._parse_odata_body(error_format, response)
383 # By priority, message is:
384 # - odatav4 message, OR
385 # - parameter "message", OR
386 # - generic meassage using "reason"
387 if self.error:
388 message = str(self.error)
389 else:
390 message = message or "Operation returned an invalid status '{}'".format(self.reason)
392 super(HttpResponseError, self).__init__(message=message, **kwargs)
394 @staticmethod
395 def _parse_odata_body(
396 error_format: Type[ODataV4Format], response: Optional[_HttpResponseCommonAPI]
397 ) -> Optional[ODataV4Format]:
398 try:
399 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
400 odata_json = json.loads(response.text()) # type: ignore
401 return error_format(odata_json)
402 except Exception: # pylint: disable=broad-except
403 # If the body is not JSON valid, just stop now
404 pass
405 return None
407 def __str__(self) -> str:
408 retval = super(HttpResponseError, self).__str__()
409 try:
410 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
411 body = self.response.text() # type: ignore
412 if body and not self.error:
413 return "{}\nContent: {}".format(retval, body)[:2048]
414 except Exception: # pylint: disable=broad-except
415 pass
416 return retval
419class DecodeError(HttpResponseError):
420 """Error raised during response deserialization."""
423class IncompleteReadError(DecodeError):
424 """Error raised if peer closes the connection before we have received the complete message body."""
427class ResourceExistsError(HttpResponseError):
428 """An error response with status code 4xx.
429 This will not be raised directly by the Azure core pipeline."""
432class ResourceNotFoundError(HttpResponseError):
433 """An error response, typically triggered by a 412 response (for update) or 404 (for get/post)"""
436class ClientAuthenticationError(HttpResponseError):
437 """An error response with status code 4xx.
438 This will not be raised directly by the Azure core pipeline."""
441class ResourceModifiedError(HttpResponseError):
442 """An error response with status code 4xx, typically 412 Conflict.
443 This will not be raised directly by the Azure core pipeline."""
446class ResourceNotModifiedError(HttpResponseError):
447 """An error response with status code 304.
448 This will not be raised directly by the Azure core pipeline."""
451class TooManyRedirectsError(HttpResponseError, Generic[HTTPRequestType, HTTPResponseType]):
452 """Reached the maximum number of redirect attempts.
454 :param history: The history of requests made while trying to fulfill the request.
455 :type history: list[~azure.core.pipeline.policies.RequestHistory]
456 """
458 def __init__(
459 self, history: "List[RequestHistory[HTTPRequestType, HTTPResponseType]]", *args: Any, **kwargs: Any
460 ) -> None:
461 self.history = history
462 message = "Reached maximum redirect attempts."
463 super(TooManyRedirectsError, self).__init__(message, *args, **kwargs)
466class ODataV4Error(HttpResponseError):
467 """An HTTP response error where the JSON is decoded as OData V4 error format.
469 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
471 :param ~azure.core.rest.HttpResponse response: The response object.
472 :ivar dict odata_json: The parsed JSON body as attribute for convenience.
473 :ivar str ~.code: Its value is a service-defined error code.
474 This code serves as a sub-status for the HTTP error code specified in the response.
475 :ivar str message: Human-readable, language-dependent representation of the error.
476 :ivar str target: The target of the particular error (for example, the name of the property in error).
477 This field is optional and may be None.
478 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
479 for code and message, and MAY contain a name/value pair for target, as described above.
480 :ivar dict innererror: An object. The contents of this object are service-defined.
481 Usually this object contains information that will help debug the service.
482 """
484 _ERROR_FORMAT = ODataV4Format
486 def __init__(self, response: _HttpResponseCommonAPI, **kwargs: Any) -> None:
487 # Ensure field are declared, whatever can happen afterwards
488 self.odata_json: Optional[Dict[str, Any]] = None
489 try:
490 self.odata_json = json.loads(response.text())
491 odata_message = self.odata_json.setdefault("error", {}).get("message")
492 except Exception: # pylint: disable=broad-except
493 # If the body is not JSON valid, just stop now
494 odata_message = None
496 self.code: Optional[str] = None
497 message: Optional[str] = kwargs.get("message", odata_message)
498 self.target: Optional[str] = None
499 self.details: Optional[List[Any]] = []
500 self.innererror: Optional[Mapping[str, Any]] = {}
502 if message and "message" not in kwargs:
503 kwargs["message"] = message
505 super(ODataV4Error, self).__init__(response=response, **kwargs)
507 self._error_format: Optional[Union[str, ODataV4Format]] = None
508 if self.odata_json:
509 try:
510 error_node = self.odata_json["error"]
511 self._error_format = self._ERROR_FORMAT(error_node)
512 self.__dict__.update({k: v for k, v in self._error_format.__dict__.items() if v is not None})
513 except Exception: # pylint: disable=broad-except
514 _LOGGER.info("Received error message was not valid OdataV4 format.")
515 self._error_format = "JSON was invalid for format " + str(self._ERROR_FORMAT)
517 def __str__(self) -> str:
518 if self._error_format:
519 return str(self._error_format)
520 return super(ODataV4Error, self).__str__()
523class StreamConsumedError(AzureError):
524 """Error thrown if you try to access the stream of a response once consumed.
526 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
527 ~azure.core.rest.AsyncHttpResponse once the response's stream has been consumed.
529 :param response: The response that triggered the exception.
530 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
531 """
533 def __init__(self, response: _HttpResponseCommonAPI) -> None:
534 message = (
535 "You are attempting to read or stream the content from request {}. "
536 "You have likely already consumed this stream, so it can not be accessed anymore.".format(response.request)
537 )
538 super(StreamConsumedError, self).__init__(message)
541class StreamClosedError(AzureError):
542 """Error thrown if you try to access the stream of a response once closed.
544 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
545 ~azure.core.rest.AsyncHttpResponse once the response's stream has been closed.
547 :param response: The response that triggered the exception.
548 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
549 """
551 def __init__(self, response: _HttpResponseCommonAPI) -> None:
552 message = (
553 "The content for response from request {} can no longer be read or streamed, since the "
554 "response has already been closed.".format(response.request)
555 )
556 super(StreamClosedError, self).__init__(message)
559class ResponseNotReadError(AzureError):
560 """Error thrown if you try to access a response's content without reading first.
562 It is thrown if you try to access an ~azure.core.rest.HttpResponse or
563 ~azure.core.rest.AsyncHttpResponse's content without first reading the response's bytes in first.
565 :param response: The response that triggered the exception.
566 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
567 """
569 def __init__(self, response: _HttpResponseCommonAPI) -> None:
570 message = (
571 "You have not read in the bytes for the response from request {}. "
572 "Call .read() on the response first.".format(response.request)
573 )
574 super(ResponseNotReadError, self).__init__(message)
577class SerializationError(ValueError):
578 """Raised if an error is encountered during serialization."""
581class DeserializationError(ValueError):
582 """Raised if an error is encountered during deserialization."""