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

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 

30 

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 

47 

48_LOGGER = logging.getLogger(__name__) 

49 

50if TYPE_CHECKING: 

51 from azure.core.pipeline.policies import RequestHistory 

52 

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

59 

60 

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] 

81 

82 

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. 

86 

87 .. note:: This method is deprecated since we don't support Python 2 anymore. Use raise/from instead. 

88 

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 

104 

105 

106@runtime_checkable 

107class _HttpResponseCommonAPI(Protocol): 

108 """Protocol used by exceptions for HTTP response. 

109 

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 """ 

113 

114 @property 

115 def reason(self) -> Optional[str]: 

116 ... 

117 

118 @property 

119 def status_code(self) -> Optional[int]: 

120 ... 

121 

122 def text(self) -> str: 

123 ... 

124 

125 @property 

126 def request(self) -> object: # object as type, since all we need is str() on it 

127 ... 

128 

129 

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 

133 

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 """ 

137 

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 

147 

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 

153 

154 

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 

165 

166 

167class ODataV4Format: 

168 """Class to describe OData V4 error format. 

169 

170 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 

171 

172 Example of JSON: 

173 

174 .. code-block:: json 

175 

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 } 

197 

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 """ 

209 

210 CODE_LABEL = "code" 

211 MESSAGE_LABEL = "message" 

212 TARGET_LABEL = "target" 

213 DETAILS_LABEL = "details" 

214 INNERERROR_LABEL = "innererror" 

215 

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__ 

220 

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) 

224 

225 if not (self.code or self.message): 

226 raise ValueError("Impossible to extract code/message from received JSON:\n" + json.dumps(json_object)) 

227 

228 # Optional fields 

229 self.target: Optional[str] = json_object.get(cls.TARGET_LABEL) 

230 

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 

238 

239 self.innererror: Mapping[str, Any] = json_object.get(cls.INNERERROR_LABEL, {}) 

240 

241 @property 

242 def error(self: SelfODataV4Format) -> SelfODataV4Format: 

243 import warnings 

244 

245 warnings.warn( 

246 "error.error from azure exceptions is deprecated, just simply use 'error' once", 

247 DeprecationWarning, 

248 ) 

249 return self 

250 

251 def __str__(self) -> str: 

252 return "({}) {}\n{}".format(self.code, self.message, self.message_details()) 

253 

254 def message_details(self) -> str: 

255 """Return a detailed string of the error. 

256 

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) 

264 

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()) 

270 

271 if self.innererror: 

272 error_str += "\nInner error: {}".format(json.dumps(self.innererror, indent=4)) 

273 return error_str 

274 

275 

276class AzureError(Exception): 

277 """Base exception for all errors. 

278 

279 :param object message: The message object stringified as 'message' attribute 

280 :keyword error: The original exception if any 

281 :paramtype error: Exception 

282 

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 """ 

293 

294 def __init__(self, message: Optional[object], *args: Any, **kwargs: Any) -> None: 

295 self.inner_exception: Optional[BaseException] = kwargs.get("error") 

296 

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] 

301 

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) 

307 

308 def raise_with_traceback(self) -> None: 

309 """Raise the exception with the existing traceback. 

310 

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 

319 

320 

321class ServiceRequestError(AzureError): 

322 """An error occurred while attempt to make a request to the service. 

323 No request was sent. 

324 """ 

325 

326 

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""" 

331 

332 

333class ServiceRequestTimeoutError(ServiceRequestError): 

334 """Error raised when timeout happens""" 

335 

336 

337class ServiceResponseTimeoutError(ServiceResponseError): 

338 """Error raised when timeout happens""" 

339 

340 

341class HttpResponseError(AzureError): 

342 """A request was made, and a non-success status code was received from the service. 

343 

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 

347 

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 """ 

359 

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) 

365 

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 

372 

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) 

382 

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) 

391 

392 super(HttpResponseError, self).__init__(message=message, **kwargs) 

393 

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 

406 

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 

417 

418 

419class DecodeError(HttpResponseError): 

420 """Error raised during response deserialization.""" 

421 

422 

423class IncompleteReadError(DecodeError): 

424 """Error raised if peer closes the connection before we have received the complete message body.""" 

425 

426 

427class ResourceExistsError(HttpResponseError): 

428 """An error response with status code 4xx. 

429 This will not be raised directly by the Azure core pipeline.""" 

430 

431 

432class ResourceNotFoundError(HttpResponseError): 

433 """An error response, typically triggered by a 412 response (for update) or 404 (for get/post)""" 

434 

435 

436class ClientAuthenticationError(HttpResponseError): 

437 """An error response with status code 4xx. 

438 This will not be raised directly by the Azure core pipeline.""" 

439 

440 

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.""" 

444 

445 

446class ResourceNotModifiedError(HttpResponseError): 

447 """An error response with status code 304. 

448 This will not be raised directly by the Azure core pipeline.""" 

449 

450 

451class TooManyRedirectsError(HttpResponseError, Generic[HTTPRequestType, HTTPResponseType]): 

452 """Reached the maximum number of redirect attempts. 

453 

454 :param history: The history of requests made while trying to fulfill the request. 

455 :type history: list[~azure.core.pipeline.policies.RequestHistory] 

456 """ 

457 

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) 

464 

465 

466class ODataV4Error(HttpResponseError): 

467 """An HTTP response error where the JSON is decoded as OData V4 error format. 

468 

469 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 

470 

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 """ 

483 

484 _ERROR_FORMAT = ODataV4Format 

485 

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 

495 

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]] = {} 

501 

502 if message and "message" not in kwargs: 

503 kwargs["message"] = message 

504 

505 super(ODataV4Error, self).__init__(response=response, **kwargs) 

506 

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) 

516 

517 def __str__(self) -> str: 

518 if self._error_format: 

519 return str(self._error_format) 

520 return super(ODataV4Error, self).__str__() 

521 

522 

523class StreamConsumedError(AzureError): 

524 """Error thrown if you try to access the stream of a response once consumed. 

525 

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. 

528 

529 :param response: The response that triggered the exception. 

530 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse 

531 """ 

532 

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) 

539 

540 

541class StreamClosedError(AzureError): 

542 """Error thrown if you try to access the stream of a response once closed. 

543 

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. 

546 

547 :param response: The response that triggered the exception. 

548 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse 

549 """ 

550 

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) 

557 

558 

559class ResponseNotReadError(AzureError): 

560 """Error thrown if you try to access a response's content without reading first. 

561 

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. 

564 

565 :param response: The response that triggered the exception. 

566 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse 

567 """ 

568 

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) 

575 

576 

577class SerializationError(ValueError): 

578 """Raised if an error is encountered during serialization.""" 

579 

580 

581class DeserializationError(ValueError): 

582 """Raised if an error is encountered during deserialization."""