Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/azure/core/pipeline/policies/_universal.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

332 statements  

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

26""" 

27This module is the requests implementation of Pipeline ABC 

28""" 

29import json 

30import inspect 

31import logging 

32import os 

33import platform 

34import xml.etree.ElementTree as ET 

35import types 

36import re 

37import uuid 

38from typing import IO, cast, Union, Optional, AnyStr, Dict, Any, Set, MutableMapping 

39import urllib.parse 

40 

41from azure.core import __version__ as azcore_version 

42from azure.core.exceptions import DecodeError 

43 

44from azure.core.pipeline import PipelineRequest, PipelineResponse 

45from ._base import SansIOHTTPPolicy 

46 

47from ..transport import HttpRequest as LegacyHttpRequest 

48from ..transport._base import _HttpResponseBase as LegacySansIOHttpResponse 

49from ...rest import HttpRequest 

50from ...rest._rest_py3 import _HttpResponseBase as SansIOHttpResponse 

51 

52_LOGGER = logging.getLogger(__name__) 

53 

54HTTPRequestType = Union[LegacyHttpRequest, HttpRequest] 

55HTTPResponseType = Union[LegacySansIOHttpResponse, SansIOHttpResponse] 

56PipelineResponseType = PipelineResponse[HTTPRequestType, HTTPResponseType] 

57 

58 

59class HeadersPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

60 """A simple policy that sends the given headers with the request. 

61 

62 This will overwrite any headers already defined in the request. Headers can be 

63 configured up front, where any custom headers will be applied to all outgoing 

64 operations, and additional headers can also be added dynamically per operation. 

65 

66 :param dict base_headers: Headers to send with the request. 

67 

68 .. admonition:: Example: 

69 

70 .. literalinclude:: ../samples/test_example_sansio.py 

71 :start-after: [START headers_policy] 

72 :end-before: [END headers_policy] 

73 :language: python 

74 :dedent: 4 

75 :caption: Configuring a headers policy. 

76 """ 

77 

78 def __init__(self, base_headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> None: 

79 self._headers: Dict[str, str] = base_headers or {} 

80 self._headers.update(kwargs.pop("headers", {})) 

81 

82 @property 

83 def headers(self) -> Dict[str, str]: 

84 """The current headers collection. 

85 

86 :rtype: dict[str, str] 

87 :return: The current headers collection. 

88 """ 

89 return self._headers 

90 

91 def add_header(self, key: str, value: str) -> None: 

92 """Add a header to the configuration to be applied to all requests. 

93 

94 :param str key: The header. 

95 :param str value: The header's value. 

96 """ 

97 self._headers[key] = value 

98 

99 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

100 """Updates with the given headers before sending the request to the next policy. 

101 

102 :param request: The PipelineRequest object 

103 :type request: ~azure.core.pipeline.PipelineRequest 

104 """ 

105 request.http_request.headers.update(self.headers) 

106 additional_headers = request.context.options.pop("headers", {}) 

107 if additional_headers: 

108 request.http_request.headers.update(additional_headers) 

109 

110 

111class _Unset: 

112 pass 

113 

114 

115class RequestIdPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

116 """A simple policy that sets the given request id in the header. 

117 

118 This will overwrite request id that is already defined in the request. Request id can be 

119 configured up front, where the request id will be applied to all outgoing 

120 operations, and additional request id can also be set dynamically per operation. 

121 

122 :keyword str request_id: The request id to be added into header. 

123 :keyword bool auto_request_id: Auto generates a unique request ID per call if true which is by default. 

124 :keyword str request_id_header_name: Header name to use. Default is "x-ms-client-request-id". 

125 

126 .. admonition:: Example: 

127 

128 .. literalinclude:: ../samples/test_example_sansio.py 

129 :start-after: [START request_id_policy] 

130 :end-before: [END request_id_policy] 

131 :language: python 

132 :dedent: 4 

133 :caption: Configuring a request id policy. 

134 """ 

135 

136 def __init__( 

137 self, # pylint: disable=unused-argument 

138 *, 

139 request_id: Union[str, Any] = _Unset, 

140 auto_request_id: bool = True, 

141 request_id_header_name: str = "x-ms-client-request-id", 

142 **kwargs: Any 

143 ) -> None: 

144 super() 

145 self._request_id = request_id 

146 self._auto_request_id = auto_request_id 

147 self._request_id_header_name = request_id_header_name 

148 

149 def set_request_id(self, value: str) -> None: 

150 """Add the request id to the configuration to be applied to all requests. 

151 

152 :param str value: The request id value. 

153 """ 

154 self._request_id = value 

155 

156 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

157 """Updates with the given request id before sending the request to the next policy. 

158 

159 :param request: The PipelineRequest object 

160 :type request: ~azure.core.pipeline.PipelineRequest 

161 """ 

162 request_id = unset = object() 

163 if "request_id" in request.context.options: 

164 request_id = request.context.options.pop("request_id") 

165 if request_id is None: 

166 return 

167 elif self._request_id is None: 

168 return 

169 elif self._request_id is not _Unset: 

170 if self._request_id_header_name in request.http_request.headers: 

171 return 

172 request_id = self._request_id 

173 elif self._auto_request_id: 

174 if self._request_id_header_name in request.http_request.headers: 

175 return 

176 request_id = str(uuid.uuid1()) 

177 if request_id is not unset: 

178 header = {self._request_id_header_name: cast(str, request_id)} 

179 request.http_request.headers.update(header) 

180 

181 

182class UserAgentPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

183 """User-Agent Policy. Allows custom values to be added to the User-Agent header. 

184 

185 :param str base_user_agent: Sets the base user agent value. 

186 

187 :keyword bool user_agent_overwrite: Overwrites User-Agent when True. Defaults to False. 

188 :keyword bool user_agent_use_env: Gets user-agent from environment. Defaults to True. 

189 :keyword str user_agent: If specified, this will be added in front of the user agent string. 

190 :keyword str sdk_moniker: If specified, the user agent string will be 

191 azsdk-python-[sdk_moniker] Python/[python_version] ([platform_version]) 

192 

193 Environment variables: 

194 

195 * ``AZURE_HTTP_USER_AGENT`` - If set and ``user_agent_use_env`` is True (the default), 

196 the value is appended to the User-Agent header string sent with each request. 

197 

198 .. admonition:: Example: 

199 

200 .. literalinclude:: ../samples/test_example_sansio.py 

201 :start-after: [START user_agent_policy] 

202 :end-before: [END user_agent_policy] 

203 :language: python 

204 :dedent: 4 

205 :caption: Configuring a user agent policy. 

206 """ 

207 

208 _USERAGENT = "User-Agent" 

209 _ENV_ADDITIONAL_USER_AGENT = "AZURE_HTTP_USER_AGENT" 

210 

211 def __init__(self, base_user_agent: Optional[str] = None, **kwargs: Any) -> None: 

212 self.overwrite: bool = kwargs.pop("user_agent_overwrite", False) 

213 self.use_env: bool = kwargs.pop("user_agent_use_env", True) 

214 application_id: Optional[str] = kwargs.pop("user_agent", None) 

215 sdk_moniker: str = kwargs.pop("sdk_moniker", "core/{}".format(azcore_version)) 

216 

217 if base_user_agent: 

218 self._user_agent = base_user_agent 

219 else: 

220 self._user_agent = "azsdk-python-{} Python/{} ({})".format( 

221 sdk_moniker, platform.python_version(), platform.platform() 

222 ) 

223 

224 if application_id: 

225 self._user_agent = "{} {}".format(application_id, self._user_agent) 

226 

227 @property 

228 def user_agent(self) -> str: 

229 """The current user agent value. 

230 

231 :return: The current user agent value. 

232 :rtype: str 

233 """ 

234 if self.use_env: 

235 add_user_agent_header = os.environ.get(self._ENV_ADDITIONAL_USER_AGENT, None) 

236 if add_user_agent_header is not None: 

237 return "{} {}".format(self._user_agent, add_user_agent_header) 

238 return self._user_agent 

239 

240 def add_user_agent(self, value: str) -> None: 

241 """Add value to current user agent with a space. 

242 

243 :param str value: value to add to user agent. 

244 """ 

245 self._user_agent = "{} {}".format(self._user_agent, value) 

246 

247 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

248 """Modifies the User-Agent header before the request is sent. 

249 

250 :param request: The PipelineRequest object 

251 :type request: ~azure.core.pipeline.PipelineRequest 

252 """ 

253 http_request = request.http_request 

254 options_dict = request.context.options 

255 if "user_agent" in options_dict: 

256 user_agent = options_dict.pop("user_agent") 

257 if options_dict.pop("user_agent_overwrite", self.overwrite): 

258 http_request.headers[self._USERAGENT] = user_agent 

259 else: 

260 user_agent = "{} {}".format(user_agent, self.user_agent) 

261 http_request.headers[self._USERAGENT] = user_agent 

262 

263 elif self.overwrite or self._USERAGENT not in http_request.headers: 

264 http_request.headers[self._USERAGENT] = self.user_agent 

265 

266 

267class NetworkTraceLoggingPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

268 """The logging policy in the pipeline is used to output HTTP network trace to the configured logger. 

269 

270 This accepts both global configuration, and per-request level with "enable_http_logger" 

271 

272 :param bool logging_enable: Use to enable per operation. Defaults to False. 

273 

274 .. admonition:: Example: 

275 

276 .. literalinclude:: ../samples/test_example_sansio.py 

277 :start-after: [START network_trace_logging_policy] 

278 :end-before: [END network_trace_logging_policy] 

279 :language: python 

280 :dedent: 4 

281 :caption: Configuring a network trace logging policy. 

282 """ 

283 

284 def __init__(self, logging_enable: bool = False, **kwargs: Any): # pylint: disable=unused-argument 

285 self.enable_http_logger = logging_enable 

286 

287 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

288 """Logs HTTP request to the DEBUG logger. 

289 

290 :param request: The PipelineRequest object. 

291 :type request: ~azure.core.pipeline.PipelineRequest 

292 """ 

293 http_request = request.http_request 

294 options = request.context.options 

295 logging_enable = options.pop("logging_enable", self.enable_http_logger) 

296 request.context["logging_enable"] = logging_enable 

297 if logging_enable: 

298 if not _LOGGER.isEnabledFor(logging.DEBUG): 

299 return 

300 

301 try: 

302 log_string = "Request URL: '{}'".format(http_request.url) 

303 log_string += "\nRequest method: '{}'".format(http_request.method) 

304 log_string += "\nRequest headers:" 

305 for header, value in http_request.headers.items(): 

306 log_string += "\n '{}': '{}'".format(header, value) 

307 log_string += "\nRequest body:" 

308 

309 # We don't want to log the binary data of a file upload. 

310 if isinstance(http_request.body, types.GeneratorType): 

311 log_string += "\nFile upload" 

312 _LOGGER.debug(log_string) 

313 return 

314 try: 

315 if isinstance(http_request.body, types.AsyncGeneratorType): 

316 log_string += "\nFile upload" 

317 _LOGGER.debug(log_string) 

318 return 

319 except AttributeError: 

320 pass 

321 if http_request.body: 

322 log_string += "\n{}".format(str(http_request.body)) 

323 _LOGGER.debug(log_string) 

324 return 

325 log_string += "\nThis request has no body" 

326 _LOGGER.debug(log_string) 

327 except Exception as err: # pylint: disable=broad-except 

328 _LOGGER.debug("Failed to log request: %r", err) 

329 

330 def on_response( 

331 self, 

332 request: PipelineRequest[HTTPRequestType], 

333 response: PipelineResponse[HTTPRequestType, HTTPResponseType], 

334 ) -> None: 

335 """Logs HTTP response to the DEBUG logger. 

336 

337 :param request: The PipelineRequest object. 

338 :type request: ~azure.core.pipeline.PipelineRequest 

339 :param response: The PipelineResponse object. 

340 :type response: ~azure.core.pipeline.PipelineResponse 

341 """ 

342 http_response = response.http_response 

343 try: 

344 logging_enable = response.context["logging_enable"] 

345 if logging_enable: 

346 if not _LOGGER.isEnabledFor(logging.DEBUG): 

347 return 

348 

349 log_string = "Response status: '{}'".format(http_response.status_code) 

350 log_string += "\nResponse headers:" 

351 for res_header, value in http_response.headers.items(): 

352 log_string += "\n '{}': '{}'".format(res_header, value) 

353 

354 # We don't want to log binary data if the response is a file. 

355 log_string += "\nResponse content:" 

356 pattern = re.compile(r'attachment; ?filename=["\w.]+', re.IGNORECASE) 

357 header = http_response.headers.get("content-disposition") 

358 

359 if header and pattern.match(header): 

360 filename = header.partition("=")[2] 

361 log_string += "\nFile attachments: {}".format(filename) 

362 elif http_response.headers.get("content-type", "").endswith("octet-stream"): 

363 log_string += "\nBody contains binary data." 

364 elif http_response.headers.get("content-type", "").startswith("image"): 

365 log_string += "\nBody contains image data." 

366 else: 

367 if response.context.options.get("stream", False): 

368 log_string += "\nBody is streamable." 

369 else: 

370 log_string += "\n{}".format(http_response.text()) 

371 _LOGGER.debug(log_string) 

372 except Exception as err: # pylint: disable=broad-except 

373 _LOGGER.debug("Failed to log response: %s", repr(err)) 

374 

375 

376class _HiddenClassProperties(type): 

377 # Backward compatible for DEFAULT_HEADERS_WHITELIST 

378 # https://github.com/Azure/azure-sdk-for-python/issues/26331 

379 

380 @property 

381 def DEFAULT_HEADERS_WHITELIST(cls) -> Set[str]: 

382 return cls.DEFAULT_HEADERS_ALLOWLIST 

383 

384 @DEFAULT_HEADERS_WHITELIST.setter 

385 def DEFAULT_HEADERS_WHITELIST(cls, value: Set[str]) -> None: 

386 cls.DEFAULT_HEADERS_ALLOWLIST = value 

387 

388 

389class HttpLoggingPolicy( 

390 SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType], 

391 metaclass=_HiddenClassProperties, 

392): 

393 """The Pipeline policy that handles logging of HTTP requests and responses. 

394 

395 :param logger: The logger to use for logging. Default to azure.core.pipeline.policies.http_logging_policy. 

396 :type logger: logging.Logger 

397 

398 Environment variables: 

399 

400 * ``AZURE_SDK_LOGGING_MULTIRECORD`` - If set to any truthy value, HTTP request and response 

401 details are logged as separate log records instead of a single combined record. 

402 """ 

403 

404 DEFAULT_HEADERS_ALLOWLIST: Set[str] = set( 

405 [ 

406 "x-ms-request-id", 

407 "x-ms-client-request-id", 

408 "x-ms-return-client-request-id", 

409 "x-ms-error-code", 

410 "traceparent", 

411 "Accept", 

412 "Cache-Control", 

413 "Connection", 

414 "Content-Length", 

415 "Content-Type", 

416 "Date", 

417 "ETag", 

418 "Expires", 

419 "If-Match", 

420 "If-Modified-Since", 

421 "If-None-Match", 

422 "If-Unmodified-Since", 

423 "Last-Modified", 

424 "Pragma", 

425 "Request-Id", 

426 "Retry-After", 

427 "Server", 

428 "Transfer-Encoding", 

429 "User-Agent", 

430 "WWW-Authenticate", # OAuth Challenge header. 

431 "x-vss-e2eid", # Needed by Azure DevOps pipelines. 

432 "x-msedge-ref", # Needed by Azure DevOps pipelines. 

433 ] 

434 ) 

435 REDACTED_PLACEHOLDER: str = "REDACTED" 

436 MULTI_RECORD_LOG: str = "AZURE_SDK_LOGGING_MULTIRECORD" 

437 

438 def __init__(self, logger: Optional[logging.Logger] = None, **kwargs: Any): # pylint: disable=unused-argument 

439 self.logger: logging.Logger = logger or logging.getLogger("azure.core.pipeline.policies.http_logging_policy") 

440 self.allowed_query_params: Set[str] = set() 

441 self.allowed_header_names: Set[str] = set(self.__class__.DEFAULT_HEADERS_ALLOWLIST) 

442 

443 def _redact_query_param(self, key: str, value: str) -> str: 

444 lower_case_allowed_query_params = [param.lower() for param in self.allowed_query_params] 

445 return value if key.lower() in lower_case_allowed_query_params else HttpLoggingPolicy.REDACTED_PLACEHOLDER 

446 

447 def _redact_header(self, key: str, value: str) -> str: 

448 lower_case_allowed_header_names = [header.lower() for header in self.allowed_header_names] 

449 return value if key.lower() in lower_case_allowed_header_names else HttpLoggingPolicy.REDACTED_PLACEHOLDER 

450 

451 def on_request( # pylint: disable=too-many-return-statements 

452 self, request: PipelineRequest[HTTPRequestType] 

453 ) -> None: 

454 """Logs HTTP method, url and headers. 

455 

456 :param request: The PipelineRequest object. 

457 :type request: ~azure.core.pipeline.PipelineRequest 

458 """ 

459 http_request = request.http_request 

460 options = request.context.options 

461 # Get logger in my context first (request has been retried) 

462 # then read from kwargs (pop if that's the case) 

463 # then use my instance logger 

464 logger = request.context.setdefault("logger", options.pop("logger", self.logger)) 

465 

466 if not logger.isEnabledFor(logging.INFO): 

467 return 

468 

469 try: 

470 parsed_url = list(urllib.parse.urlparse(http_request.url)) 

471 parsed_qp = urllib.parse.parse_qsl(parsed_url[4], keep_blank_values=True) 

472 filtered_qp = [(key, self._redact_query_param(key, value)) for key, value in parsed_qp] 

473 # 4 is query 

474 parsed_url[4] = "&".join(["=".join(part) for part in filtered_qp]) 

475 redacted_url = urllib.parse.urlunparse(parsed_url) 

476 

477 multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False) 

478 if multi_record: 

479 logger.info("Request URL: %r", redacted_url) 

480 logger.info("Request method: %r", http_request.method) 

481 logger.info("Request headers:") 

482 for header, value in http_request.headers.items(): 

483 value = self._redact_header(header, value) 

484 logger.info(" %r: %r", header, value) 

485 if isinstance(http_request.body, types.GeneratorType): 

486 logger.info("File upload") 

487 return 

488 try: 

489 if isinstance(http_request.body, types.AsyncGeneratorType): 

490 logger.info("File upload") 

491 return 

492 except AttributeError: 

493 pass 

494 if http_request.body: 

495 logger.info("A body is sent with the request") 

496 return 

497 logger.info("No body was attached to the request") 

498 return 

499 log_string = "Request URL: '{}'".format(redacted_url) 

500 log_string += "\nRequest method: '{}'".format(http_request.method) 

501 log_string += "\nRequest headers:" 

502 for header, value in http_request.headers.items(): 

503 value = self._redact_header(header, value) 

504 log_string += "\n '{}': '{}'".format(header, value) 

505 if isinstance(http_request.body, types.GeneratorType): 

506 log_string += "\nFile upload" 

507 logger.info(log_string) 

508 return 

509 try: 

510 if isinstance(http_request.body, types.AsyncGeneratorType): 

511 log_string += "\nFile upload" 

512 logger.info(log_string) 

513 return 

514 except AttributeError: 

515 pass 

516 if http_request.body: 

517 log_string += "\nA body is sent with the request" 

518 logger.info(log_string) 

519 return 

520 log_string += "\nNo body was attached to the request" 

521 logger.info(log_string) 

522 

523 except Exception: # pylint: disable=broad-except 

524 logger.warning("Failed to log request.") 

525 

526 def on_response( 

527 self, 

528 request: PipelineRequest[HTTPRequestType], 

529 response: PipelineResponse[HTTPRequestType, HTTPResponseType], 

530 ) -> None: 

531 """Logs HTTP response status and headers. 

532 

533 :param request: The PipelineRequest object. 

534 :type request: ~azure.core.pipeline.PipelineRequest 

535 :param response: The PipelineResponse object. 

536 :type response: ~azure.core.pipeline.PipelineResponse 

537 """ 

538 http_response = response.http_response 

539 

540 # Get logger in my context first (request has been retried) 

541 # then read from kwargs (pop if that's the case) 

542 # then use my instance logger 

543 # If on_request was called, should always read from context 

544 options = request.context.options 

545 logger = request.context.setdefault("logger", options.pop("logger", self.logger)) 

546 

547 try: 

548 if not logger.isEnabledFor(logging.INFO): 

549 return 

550 

551 multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False) 

552 if multi_record: 

553 logger.info("Response status: %r", http_response.status_code) 

554 logger.info("Response headers:") 

555 for res_header, value in http_response.headers.items(): 

556 value = self._redact_header(res_header, value) 

557 logger.info(" %r: %r", res_header, value) 

558 return 

559 log_string = "Response status: {}".format(http_response.status_code) 

560 log_string += "\nResponse headers:" 

561 for res_header, value in http_response.headers.items(): 

562 value = self._redact_header(res_header, value) 

563 log_string += "\n '{}': '{}'".format(res_header, value) 

564 logger.info(log_string) 

565 except Exception: # pylint: disable=broad-except 

566 logger.warning("Failed to log response.") 

567 

568 

569class ContentDecodePolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

570 """Policy for decoding unstreamed response content. 

571 

572 :param response_encoding: The encoding to use if known for this service (will disable auto-detection) 

573 :type response_encoding: str 

574 """ 

575 

576 # Accept "text" because we're open minded people... 

577 JSON_REGEXP = re.compile(r"^(application|text)/([0-9a-z+.-]+\+)?json$") 

578 

579 # Name used in context 

580 CONTEXT_NAME = "deserialized_data" 

581 

582 def __init__( 

583 self, response_encoding: Optional[str] = None, **kwargs: Any # pylint: disable=unused-argument 

584 ) -> None: 

585 self._response_encoding = response_encoding 

586 

587 @classmethod 

588 def deserialize_from_text( 

589 cls, 

590 data: Optional[Union[AnyStr, IO[AnyStr]]], 

591 mime_type: Optional[str] = None, 

592 response: Optional[HTTPResponseType] = None, 

593 ) -> Any: 

594 """Decode response data according to content-type. 

595 

596 Accept a stream of data as well, but will be load at once in memory for now. 

597 If no content-type, will return the string version (not bytes, not stream) 

598 

599 :param data: The data to deserialize. 

600 :type data: str or bytes or file-like object 

601 :param response: The HTTP response. 

602 :type response: ~azure.core.pipeline.transport.HttpResponse 

603 :param str mime_type: The mime type. As mime type, charset is not expected. 

604 :param response: If passed, exception will be annotated with that response 

605 :type response: any 

606 :raises ~azure.core.exceptions.DecodeError: If deserialization fails 

607 :returns: A dict (JSON), XML tree or str, depending of the mime_type 

608 :rtype: dict[str, Any] or xml.etree.ElementTree.Element or str 

609 """ 

610 if not data: 

611 return None 

612 

613 if hasattr(data, "read"): 

614 # Assume a stream 

615 data = cast(IO, data).read() 

616 

617 if isinstance(data, bytes): 

618 data_as_str = data.decode(encoding="utf-8-sig") 

619 else: 

620 # Explain to mypy the correct type. 

621 data_as_str = cast(str, data) 

622 

623 if mime_type is None: 

624 return data_as_str 

625 

626 if cls.JSON_REGEXP.match(mime_type): 

627 try: 

628 return json.loads(data_as_str) 

629 except ValueError as err: 

630 raise DecodeError( 

631 message="JSON is invalid: {}".format(err), 

632 response=response, 

633 error=err, 

634 ) from err 

635 elif "xml" in (mime_type or []): 

636 try: 

637 return ET.fromstring(data_as_str) # nosec 

638 except ET.ParseError as err: 

639 # It might be because the server has an issue, and returned JSON with 

640 # content-type XML.... 

641 # So let's try a JSON load, and if it's still broken 

642 # let's flow the initial exception 

643 def _json_attemp(data): 

644 try: 

645 return True, json.loads(data) 

646 except ValueError: 

647 return False, None # Don't care about this one 

648 

649 success, json_result = _json_attemp(data) 

650 if success: 

651 return json_result 

652 # If i'm here, it's not JSON, it's not XML, let's scream 

653 # and raise the last context in this block (the XML exception) 

654 # The function hack is because Py2.7 messes up with exception 

655 # context otherwise. 

656 _LOGGER.critical("Wasn't XML not JSON, failing") 

657 raise DecodeError("XML is invalid", response=response) from err 

658 elif mime_type.startswith("text/"): 

659 return data_as_str 

660 raise DecodeError("Cannot deserialize content-type: {}".format(mime_type)) 

661 

662 @classmethod 

663 def deserialize_from_http_generics( 

664 cls, 

665 response: HTTPResponseType, 

666 encoding: Optional[str] = None, 

667 ) -> Any: 

668 """Deserialize from HTTP response. 

669 

670 Headers will tested for "content-type" 

671 

672 :param response: The HTTP response 

673 :type response: any 

674 :param str encoding: The encoding to use if known for this service (will disable auto-detection) 

675 :raises ~azure.core.exceptions.DecodeError: If deserialization fails 

676 :returns: A dict (JSON), XML tree or str, depending of the mime_type 

677 :rtype: dict[str, Any] or xml.etree.ElementTree.Element or str 

678 """ 

679 # Try to use content-type from headers if available 

680 if response.content_type: 

681 mime_type = response.content_type.split(";")[0].strip().lower() 

682 # Ouch, this server did not declare what it sent... 

683 # Let's guess it's JSON... 

684 # Also, since Autorest was considering that an empty body was a valid JSON, 

685 # need that test as well.... 

686 else: 

687 mime_type = "application/json" 

688 

689 # Rely on transport implementation to give me "text()" decoded correctly 

690 if hasattr(response, "read"): 

691 # since users can call deserialize_from_http_generics by themselves 

692 # we want to make sure our new responses are read before we try to 

693 # deserialize. Only read sync responses since we're in a sync function 

694 # 

695 # Technically HttpResponse do not contain a "read()", but we don't know what 

696 # people have been able to pass here, so keep this code for safety, 

697 # even if it's likely dead code 

698 if not inspect.iscoroutinefunction(response.read): # type: ignore 

699 response.read() # type: ignore 

700 return cls.deserialize_from_text(response.text(encoding), mime_type, response=response) 

701 

702 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

703 """Set the response encoding in the request context. 

704 

705 :param request: The PipelineRequest object. 

706 :type request: ~azure.core.pipeline.PipelineRequest 

707 """ 

708 options = request.context.options 

709 response_encoding = options.pop("response_encoding", self._response_encoding) 

710 if response_encoding: 

711 request.context["response_encoding"] = response_encoding 

712 

713 def on_response( 

714 self, 

715 request: PipelineRequest[HTTPRequestType], 

716 response: PipelineResponse[HTTPRequestType, HTTPResponseType], 

717 ) -> None: 

718 """Extract data from the body of a REST response object. 

719 This will load the entire payload in memory. 

720 Will follow Content-Type to parse. 

721 We assume everything is UTF8 (BOM acceptable). 

722 

723 :param request: The PipelineRequest object. 

724 :type request: ~azure.core.pipeline.PipelineRequest 

725 :param response: The PipelineResponse object. 

726 :type response: ~azure.core.pipeline.PipelineResponse 

727 :raises JSONDecodeError: If JSON is requested and parsing is impossible. 

728 :raises UnicodeDecodeError: If bytes is not UTF8 

729 :raises xml.etree.ElementTree.ParseError: If bytes is not valid XML 

730 :raises ~azure.core.exceptions.DecodeError: If deserialization fails 

731 """ 

732 # If response was asked as stream, do NOT read anything and quit now 

733 if response.context.options.get("stream", True): 

734 return 

735 

736 response_encoding = request.context.get("response_encoding") 

737 

738 response.context[self.CONTEXT_NAME] = self.deserialize_from_http_generics( 

739 response.http_response, response_encoding 

740 ) 

741 

742 

743class ProxyPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]): 

744 """A proxy policy. 

745 

746 Dictionary mapping protocol or protocol and host to the URL of the proxy 

747 to be used on each Request. 

748 

749 :param MutableMapping proxies: Maps protocol or protocol and hostname to the URL 

750 of the proxy. 

751 

752 .. admonition:: Example: 

753 

754 .. literalinclude:: ../samples/test_example_sansio.py 

755 :start-after: [START proxy_policy] 

756 :end-before: [END proxy_policy] 

757 :language: python 

758 :dedent: 4 

759 :caption: Configuring a proxy policy. 

760 """ 

761 

762 def __init__( 

763 self, proxies: Optional[MutableMapping[str, str]] = None, **kwargs: Any 

764 ): # pylint: disable=unused-argument 

765 self.proxies = proxies 

766 

767 def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None: 

768 """Adds the proxy information to the request context. 

769 

770 :param request: The PipelineRequest object 

771 :type request: ~azure.core.pipeline.PipelineRequest 

772 """ 

773 ctxt = request.context.options 

774 if self.proxies and "proxies" not in ctxt: 

775 ctxt["proxies"] = self.proxies