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"""
29from __future__ import absolute_import  # we have a "requests" module that conflicts with "requests" on Py2.7
30import json
31import inspect
32import logging
33import os
34import platform
35import xml.etree.ElementTree as ET
36import types
37import re
38import uuid
39from typing import (Mapping, IO, TypeVar, TYPE_CHECKING, Type, cast, List, Callable, Iterator, # pylint: disable=unused-import
40                    Any, Union, Dict, Optional, AnyStr)
41from six.moves import urllib
42
43from azure.core import __version__  as azcore_version
44from azure.core.exceptions import (
45    DecodeError,
46    raise_with_traceback
47)
48
49from azure.core.pipeline import PipelineRequest, PipelineResponse
50from ._base import SansIOHTTPPolicy
51
52if TYPE_CHECKING:
53    from azure.core.pipeline.transport import HttpResponse, AsyncHttpResponse
54
55_LOGGER = logging.getLogger(__name__)
56ContentDecodePolicyType = TypeVar('ContentDecodePolicyType', bound='ContentDecodePolicy')
57HTTPRequestType = TypeVar("HTTPRequestType")
58HTTPResponseType = TypeVar("HTTPResponseType")
59
60
61class HeadersPolicy(SansIOHTTPPolicy):
62    """A simple policy that sends the given headers with the request.
63
64    This will overwrite any headers already defined in the request. Headers can be
65    configured up front, where any custom headers will be applied to all outgoing
66    operations, and additional headers can also be added dynamically per operation.
67
68    :param dict base_headers: Headers to send with the request.
69
70    .. admonition:: Example:
71
72        .. literalinclude:: ../samples/test_example_sansio.py
73            :start-after: [START headers_policy]
74            :end-before: [END headers_policy]
75            :language: python
76            :dedent: 4
77            :caption: Configuring a headers policy.
78    """
79    def __init__(self, base_headers=None, **kwargs):  # pylint: disable=super-init-not-called
80        # type: (Dict[str, str], Any) -> None
81        self._headers = base_headers or {}
82        self._headers.update(kwargs.pop('headers', {}))
83
84    @property
85    def headers(self):
86        """The current headers collection."""
87        return self._headers
88
89    def add_header(self, key, value):
90        """Add a header to the configuration to be applied to all requests.
91
92        :param str key: The header.
93        :param str value: The header's value.
94        """
95        self._headers[key] = value
96
97    def on_request(self, request):
98        # type: (PipelineRequest) -> None
99        """Updates with the given headers before sending the request to the next policy.
100
101        :param request: The PipelineRequest object
102        :type request: ~azure.core.pipeline.PipelineRequest
103        """
104        request.http_request.headers.update(self.headers)
105        additional_headers = request.context.options.pop('headers', {})
106        if additional_headers:
107            request.http_request.headers.update(additional_headers)
108
109class _Unset(object):
110    pass
111
112class RequestIdPolicy(SansIOHTTPPolicy):
113    """A simple policy that sets the given request id in the header.
114
115    This will overwrite request id that is already defined in the request. Request id can be
116    configured up front, where the request id will be applied to all outgoing
117    operations, and additional request id can also be set dynamically per operation.
118
119    :keyword str request_id: The request id to be added into header.
120    :keyword bool auto_request_id: Auto generates a unique request ID per call if true which is by default.
121
122    .. admonition:: Example:
123
124        .. literalinclude:: ../samples/test_example_sansio.py
125            :start-after: [START request_id_policy]
126            :end-before: [END request_id_policy]
127            :language: python
128            :dedent: 4
129            :caption: Configuring a request id policy.
130    """
131    def __init__(self, **kwargs):  # pylint: disable=super-init-not-called
132        # type: (dict) -> None
133        self._request_id = kwargs.pop('request_id', _Unset)
134        self._auto_request_id = kwargs.pop('auto_request_id', True)
135
136    def set_request_id(self, value):
137        """Add the request id to the configuration to be applied to all requests.
138
139        :param str value: The request id value.
140        """
141        self._request_id = value
142
143    def on_request(self, request):
144        # type: (PipelineRequest) -> None
145        """Updates with the given request id before sending the request to the next policy.
146
147        :param request: The PipelineRequest object
148        :type request: ~azure.core.pipeline.PipelineRequest
149        """
150        request_id = unset = object()
151        if 'request_id' in request.context.options:
152            request_id = request.context.options.pop('request_id')
153            if request_id is None:
154                return
155        elif self._request_id is None:
156            return
157        elif self._request_id is not _Unset:
158            if "x-ms-client-request-id" in request.http_request.headers:
159                return
160            request_id = self._request_id
161        elif self._auto_request_id:
162            if "x-ms-client-request-id" in request.http_request.headers:
163                return
164            request_id = str(uuid.uuid1())
165        if request_id is not unset:
166            header = {"x-ms-client-request-id": request_id}
167            request.http_request.headers.update(header)
168
169class UserAgentPolicy(SansIOHTTPPolicy):
170    """User-Agent Policy. Allows custom values to be added to the User-Agent header.
171
172    :param str base_user_agent: Sets the base user agent value.
173
174    :keyword bool user_agent_overwrite: Overwrites User-Agent when True. Defaults to False.
175    :keyword bool user_agent_use_env: Gets user-agent from environment. Defaults to True.
176    :keyword str user_agent: If specified, this will be added in front of the user agent string.
177    :keyword str sdk_moniker: If specified, the user agent string will be
178        azsdk-python-[sdk_moniker] Python/[python_version] ([platform_version])
179
180    .. admonition:: Example:
181
182        .. literalinclude:: ../samples/test_example_sansio.py
183            :start-after: [START user_agent_policy]
184            :end-before: [END user_agent_policy]
185            :language: python
186            :dedent: 4
187            :caption: Configuring a user agent policy.
188    """
189    _USERAGENT = "User-Agent"
190    _ENV_ADDITIONAL_USER_AGENT = 'AZURE_HTTP_USER_AGENT'
191
192    def __init__(self, base_user_agent=None, **kwargs):  # pylint: disable=super-init-not-called
193        # type: (Optional[str], **Any) -> None
194        self.overwrite = kwargs.pop('user_agent_overwrite', False)
195        self.use_env = kwargs.pop('user_agent_use_env', True)
196        application_id = kwargs.pop('user_agent', None)
197        sdk_moniker = kwargs.pop('sdk_moniker', 'core/{}'.format(azcore_version))
198
199        if base_user_agent:
200            self._user_agent = base_user_agent
201        else:
202            self._user_agent = "azsdk-python-{} Python/{} ({})".format(
203                sdk_moniker,
204                platform.python_version(),
205                platform.platform()
206            )
207
208        if application_id:
209            self._user_agent = "{} {}".format(application_id, self._user_agent)
210
211    @property
212    def user_agent(self):
213        # type: () -> str
214        """The current user agent value."""
215        if self.use_env:
216            add_user_agent_header = os.environ.get(self._ENV_ADDITIONAL_USER_AGENT, None)
217            if add_user_agent_header is not None:
218                return "{} {}".format(self._user_agent, add_user_agent_header)
219        return self._user_agent
220
221    def add_user_agent(self, value):
222        # type: (str) -> None
223        """Add value to current user agent with a space.
224        :param str value: value to add to user agent.
225        """
226        self._user_agent = "{} {}".format(self._user_agent, value)
227
228    def on_request(self, request):
229        # type: (PipelineRequest) -> None
230        """Modifies the User-Agent header before the request is sent.
231
232        :param request: The PipelineRequest object
233        :type request: ~azure.core.pipeline.PipelineRequest
234        """
235        http_request = request.http_request
236        options_dict = request.context.options
237        if 'user_agent' in options_dict:
238            user_agent = options_dict.pop('user_agent')
239            if options_dict.pop('user_agent_overwrite', self.overwrite):
240                http_request.headers[self._USERAGENT] = user_agent
241            else:
242                user_agent = "{} {}".format(user_agent, self.user_agent)
243                http_request.headers[self._USERAGENT] = user_agent
244
245        elif self.overwrite or self._USERAGENT not in http_request.headers:
246            http_request.headers[self._USERAGENT] = self.user_agent
247
248
249class NetworkTraceLoggingPolicy(SansIOHTTPPolicy):
250
251    """The logging policy in the pipeline is used to output HTTP network trace to the configured logger.
252
253    This accepts both global configuration, and per-request level with "enable_http_logger"
254
255    :param bool logging_enable: Use to enable per operation. Defaults to False.
256
257    .. admonition:: Example:
258
259        .. literalinclude:: ../samples/test_example_sansio.py
260            :start-after: [START network_trace_logging_policy]
261            :end-before: [END network_trace_logging_policy]
262            :language: python
263            :dedent: 4
264            :caption: Configuring a network trace logging policy.
265    """
266    def __init__(self, logging_enable=False, **kwargs): # pylint: disable=unused-argument
267        self.enable_http_logger = logging_enable
268
269    def on_request(self, request):  # pylint: disable=too-many-return-statements
270        # type: (PipelineRequest) -> None
271        """Logs HTTP request to the DEBUG logger.
272
273        :param request: The PipelineRequest object.
274        :type request: ~azure.core.pipeline.PipelineRequest
275        """
276        http_request = request.http_request
277        options = request.context.options
278        logging_enable = options.pop("logging_enable", self.enable_http_logger)
279        request.context["logging_enable"] = logging_enable
280        if logging_enable:
281            if not _LOGGER.isEnabledFor(logging.DEBUG):
282                return
283
284            try:
285                log_string = "Request URL: '{}'".format(http_request.url)
286                log_string += "/nRequest method: '{}'".format(http_request.method)
287                log_string += "/nRequest headers:"
288                for header, value in http_request.headers.items():
289                    log_string += "/n    '{}': '{}'".format(header, value)
290                log_string += "/nRequest body:"
291
292                # We don't want to log the binary data of a file upload.
293                if isinstance(http_request.body, types.GeneratorType):
294                    log_string += "/nFile upload"
295                    _LOGGER.debug(log_string)
296                    return
297                try:
298                    if isinstance(http_request.body, types.AsyncGeneratorType):
299                        log_string += "/nFile upload"
300                        _LOGGER.debug(log_string)
301                        return
302                except AttributeError:
303                    pass
304                if http_request.body:
305                    log_string += "/n{}".format(str(http_request.body))
306                    _LOGGER.debug(log_string)
307                    return
308                log_string += "/nThis request has no body"
309                _LOGGER.debug(log_string)
310            except Exception as err:  # pylint: disable=broad-except
311                _LOGGER.debug("Failed to log request: %r", err)
312
313    def on_response(self, request, response):
314        # type: (PipelineRequest, PipelineResponse) -> None
315        """Logs HTTP response to the DEBUG logger.
316
317        :param request: The PipelineRequest object.
318        :type request: ~azure.core.pipeline.PipelineRequest
319        :param response: The PipelineResponse object.
320        :type response: ~azure.core.pipeline.PipelineResponse
321        """
322        http_response = response.http_response
323        try:
324            logging_enable = response.context["logging_enable"]
325            if logging_enable:
326                if not _LOGGER.isEnabledFor(logging.DEBUG):
327                    return
328
329                log_string = "Response status: '{}'".format(http_response.status_code)
330                log_string += "/nResponse headers:"
331                for res_header, value in http_response.headers.items():
332                    log_string += "/n    '{}': '{}'".format(res_header, value)
333
334                # We don't want to log binary data if the response is a file.
335                log_string += "/nResponse content:"
336                pattern = re.compile(r'attachment; ?filename=["\w.]+', re.IGNORECASE)
337                header = http_response.headers.get('content-disposition')
338
339                if header and pattern.match(header):
340                    filename = header.partition('=')[2]
341                    log_string += "/nFile attachments: {}".format(filename)
342                elif http_response.headers.get("content-type", "").endswith("octet-stream"):
343                    log_string += "/nBody contains binary data."
344                elif http_response.headers.get("content-type", "").startswith("image"):
345                    log_string += "/nBody contains image data."
346                else:
347                    if response.context.options.get('stream', False):
348                        log_string += "/nBody is streamable."
349                    else:
350                        log_string += "/n{}".format(http_response.text())
351                _LOGGER.debug(log_string)
352        except Exception as err:  # pylint: disable=broad-except
353            _LOGGER.debug("Failed to log response: %s", repr(err))
354
355
356class HttpLoggingPolicy(SansIOHTTPPolicy):
357    """The Pipeline policy that handles logging of HTTP requests and responses.
358    """
359
360    DEFAULT_HEADERS_WHITELIST = set([
361        "x-ms-request-id",
362        "x-ms-client-request-id",
363        "x-ms-return-client-request-id",
364        "traceparent",
365        "Accept",
366        "Cache-Control",
367        "Connection",
368        "Content-Length",
369        "Content-Type",
370        "Date",
371        "ETag",
372        "Expires",
373        "If-Match",
374        "If-Modified-Since",
375        "If-None-Match",
376        "If-Unmodified-Since",
377        "Last-Modified",
378        "Pragma",
379        "Request-Id",
380        "Retry-After",
381        "Server",
382        "Transfer-Encoding",
383        "User-Agent",
384    ])
385    REDACTED_PLACEHOLDER = "REDACTED"
386    MULTI_RECORD_LOG = "AZURE_SDK_LOGGING_MULTIRECORD"
387
388    def __init__(self, logger=None, **kwargs):  # pylint: disable=unused-argument
389        self.logger = logger or logging.getLogger(
390            "azure.core.pipeline.policies.http_logging_policy"
391        )
392        self.allowed_query_params = set()
393        self.allowed_header_names = set(self.__class__.DEFAULT_HEADERS_WHITELIST)
394
395    def _redact_query_param(self, key, value):
396        lower_case_allowed_query_params = [
397            param.lower() for param in self.allowed_query_params
398        ]
399        return value if key.lower() in lower_case_allowed_query_params else HttpLoggingPolicy.REDACTED_PLACEHOLDER
400
401    def _redact_header(self, key, value):
402        lower_case_allowed_header_names = [
403            header.lower() for header in self.allowed_header_names
404        ]
405        return value if key.lower() in lower_case_allowed_header_names else HttpLoggingPolicy.REDACTED_PLACEHOLDER
406
407    def on_request(self, request):  # pylint: disable=too-many-return-statements
408        # type: (PipelineRequest) -> None
409        """Logs HTTP method, url and headers.
410        :param request: The PipelineRequest object.
411        :type request: ~azure.core.pipeline.PipelineRequest
412        """
413        http_request = request.http_request
414        options = request.context.options
415        # Get logger in my context first (request has been retried)
416        # then read from kwargs (pop if that's the case)
417        # then use my instance logger
418        logger = request.context.setdefault("logger", options.pop("logger", self.logger))
419
420        if not logger.isEnabledFor(logging.INFO):
421            return
422
423        try:
424            parsed_url = list(urllib.parse.urlparse(http_request.url))
425            parsed_qp = urllib.parse.parse_qsl(parsed_url[4], keep_blank_values=True)
426            filtered_qp = [(key, self._redact_query_param(key, value)) for key, value in parsed_qp]
427            # 4 is query
428            parsed_url[4] = "&".join(["=".join(part) for part in filtered_qp])
429            redacted_url = urllib.parse.urlunparse(parsed_url)
430
431            multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False)
432            if multi_record:
433                logger.info("Request URL: %r", redacted_url)
434                logger.info("Request method: %r", http_request.method)
435                logger.info("Request headers:")
436                for header, value in http_request.headers.items():
437                    value = self._redact_header(header, value)
438                    logger.info("    %r: %r", header, value)
439                if isinstance(http_request.body, types.GeneratorType):
440                    logger.info("File upload")
441                    return
442                try:
443                    if isinstance(http_request.body, types.AsyncGeneratorType):
444                        logger.info("File upload")
445                        return
446                except AttributeError:
447                    pass
448                if http_request.body:
449                    logger.info("A body is sent with the request")
450                    return
451                logger.info("No body was attached to the request")
452                return
453            log_string = "Request URL: '{}'".format(redacted_url)
454            log_string += "/nRequest method: '{}'".format(http_request.method)
455            log_string += "/nRequest headers:"
456            for header, value in http_request.headers.items():
457                value = self._redact_header(header, value)
458                log_string += "/n    '{}': '{}'".format(header, value)
459            if isinstance(http_request.body, types.GeneratorType):
460                log_string += "/nFile upload"
461                logger.info(log_string)
462                return
463            try:
464                if isinstance(http_request.body, types.AsyncGeneratorType):
465                    log_string += "/nFile upload"
466                    logger.info(log_string)
467                    return
468            except AttributeError:
469                pass
470            if http_request.body:
471                log_string += "/nA body is sent with the request"
472                logger.info(log_string)
473                return
474            log_string += "/nNo body was attached to the request"
475            logger.info(log_string)
476
477        except Exception as err:  # pylint: disable=broad-except
478            logger.warning("Failed to log request: %s", repr(err))
479
480    def on_response(self, request, response):
481        # type: (PipelineRequest, PipelineResponse) -> None
482        http_response = response.http_response
483
484        try:
485            logger = response.context["logger"]
486
487            if not logger.isEnabledFor(logging.INFO):
488                return
489
490            multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False)
491            if multi_record:
492                logger.info("Response status: %r", http_response.status_code)
493                logger.info("Response headers:")
494                for res_header, value in http_response.headers.items():
495                    value = self._redact_header(res_header, value)
496                    logger.info("    %r: %r", res_header, value)
497                return
498            log_string = "Response status: {}".format(http_response.status_code)
499            log_string += "/nResponse headers:"
500            for res_header, value in http_response.headers.items():
501                value = self._redact_header(res_header, value)
502                log_string += "/n    '{}': '{}'".format(res_header, value)
503            logger.info(log_string)
504        except Exception as err:  # pylint: disable=broad-except
505            logger.warning("Failed to log response: %s", repr(err))
506
507class ContentDecodePolicy(SansIOHTTPPolicy):
508    """Policy for decoding unstreamed response content.
509
510    :param response_encoding: The encoding to use if known for this service (will disable auto-detection)
511    :type response_encoding: str
512    """
513    # Accept "text" because we're open minded people...
514    JSON_REGEXP = re.compile(r'^(application|text)/([0-9a-z+.]+\+)?json$')
515
516    # Name used in context
517    CONTEXT_NAME = "deserialized_data"
518
519    def __init__(self, response_encoding=None, **kwargs):  # pylint: disable=unused-argument
520        # type: (Optional[str], Any) -> None
521        self._response_encoding = response_encoding
522
523    @classmethod
524    def deserialize_from_text(
525        cls,  # type: Type[ContentDecodePolicyType]
526        data,  # type: Optional[Union[AnyStr, IO]]
527        mime_type=None,  # Optional[str]
528        response=None  # Optional[Union[HttpResponse, AsyncHttpResponse]]
529    ):
530        """Decode response data according to content-type.
531
532        Accept a stream of data as well, but will be load at once in memory for now.
533        If no content-type, will return the string version (not bytes, not stream)
534
535        :param response: The HTTP response.
536        :type response: ~azure.core.pipeline.transport.HttpResponse
537        :param str mime_type: The mime type. As mime type, charset is not expected.
538        :param response: If passed, exception will be annotated with that response
539        :raises ~azure.core.exceptions.DecodeError: If deserialization fails
540        :returns: A dict or XML tree, depending of the mime_type
541        """
542        if not data:
543            return None
544
545        if hasattr(data, 'read'):
546            # Assume a stream
547            data = cast(IO, data).read()
548
549        if isinstance(data, bytes):
550            data_as_str = data.decode(encoding='utf-8-sig')
551        else:
552            # Explain to mypy the correct type.
553            data_as_str = cast(str, data)
554
555        if mime_type is None:
556            return data_as_str
557
558        if cls.JSON_REGEXP.match(mime_type):
559            try:
560                return json.loads(data_as_str)
561            except ValueError as err:
562                raise DecodeError(message="JSON is invalid: {}".format(err), response=response, error=err)
563        elif "xml" in (mime_type or []):
564            try:
565                try:
566                    if isinstance(data, unicode):  # type: ignore
567                        # If I'm Python 2.7 and unicode XML will scream if I try a "fromstring" on unicode string
568                        data_as_str = cast(str, data_as_str.encode(encoding="utf-8"))
569                except NameError:
570                    pass
571                return ET.fromstring(data_as_str)   # nosec
572            except ET.ParseError:
573                # It might be because the server has an issue, and returned JSON with
574                # content-type XML....
575                # So let's try a JSON load, and if it's still broken
576                # let's flow the initial exception
577                def _json_attemp(data):
578                    try:
579                        return True, json.loads(data)
580                    except ValueError:
581                        return False, None # Don't care about this one
582                success, json_result = _json_attemp(data)
583                if success:
584                    return json_result
585                # If i'm here, it's not JSON, it's not XML, let's scream
586                # and raise the last context in this block (the XML exception)
587                # The function hack is because Py2.7 messes up with exception
588                # context otherwise.
589                _LOGGER.critical("Wasn't XML not JSON, failing")
590                raise_with_traceback(DecodeError, message="XML is invalid", response=response)
591        elif mime_type.startswith("text/"):
592            return data_as_str
593        raise DecodeError("Cannot deserialize content-type: {}".format(mime_type))
594
595    @classmethod
596    def deserialize_from_http_generics(
597        cls,  # type: Type[ContentDecodePolicyType]
598        response,  # Union[HttpResponse, AsyncHttpResponse]
599        encoding=None,  # Optional[str]
600    ):
601        """Deserialize from HTTP response.
602
603        Headers will tested for "content-type"
604
605        :param response: The HTTP response
606        :param encoding: The encoding to use if known for this service (will disable auto-detection)
607        :raises ~azure.core.exceptions.DecodeError: If deserialization fails
608        :returns: A dict or XML tree, depending of the mime-type
609        """
610        # Try to use content-type from headers if available
611        if response.content_type:
612            mime_type = response.content_type.split(";")[0].strip().lower()
613        # Ouch, this server did not declare what it sent...
614        # Let's guess it's JSON...
615        # Also, since Autorest was considering that an empty body was a valid JSON,
616        # need that test as well....
617        else:
618            mime_type = "application/json"
619
620        # Rely on transport implementation to give me "text()" decoded correctly
621        if hasattr(response, "read"):
622            try:
623                # since users can call deserialize_from_http_generics by themselves
624                # we want to make sure our new responses are read before we try to
625                # deserialize. Only read sync responses since we're in a sync function
626                if not inspect.iscoroutinefunction(response.read):
627                    response.read()
628            except AttributeError:
629                # raises an AttributeError in 2.7 bc inspect.iscoroutinefunction was added in 3.5
630                # Entering here means it's 2.7 and that the response has a read method, so we read
631                # bc it will be sync.
632                response.read()
633        return cls.deserialize_from_text(response.text(encoding), mime_type, response=response)
634
635    def on_request(self, request):
636        # type: (PipelineRequest) -> None
637        options = request.context.options
638        response_encoding = options.pop("response_encoding", self._response_encoding)
639        if response_encoding:
640            request.context["response_encoding"] = response_encoding
641
642    def on_response(self,
643        request, # type: PipelineRequest[HTTPRequestType]
644        response  # type: PipelineResponse[HTTPRequestType, Union[HttpResponse, AsyncHttpResponse]]
645    ):
646        # type: (...) -> None
647        """Extract data from the body of a REST response object.
648        This will load the entire payload in memory.
649        Will follow Content-Type to parse.
650        We assume everything is UTF8 (BOM acceptable).
651
652        :param request: The PipelineRequest object.
653        :type request: ~azure.core.pipeline.PipelineRequest
654        :param response: The PipelineResponse object.
655        :type response: ~azure.core.pipeline.PipelineResponse
656        :param raw_data: Data to be processed.
657        :param content_type: How to parse if raw_data is a string/bytes.
658        :raises JSONDecodeError: If JSON is requested and parsing is impossible.
659        :raises UnicodeDecodeError: If bytes is not UTF8
660        :raises xml.etree.ElementTree.ParseError: If bytes is not valid XML
661        :raises ~azure.core.exceptions.DecodeError: If deserialization fails
662        """
663        # If response was asked as stream, do NOT read anything and quit now
664        if response.context.options.get("stream", True):
665            return
666
667        response_encoding = request.context.get('response_encoding')
668
669        response.context[self.CONTEXT_NAME] = self.deserialize_from_http_generics(
670            response.http_response,
671            response_encoding
672        )
673
674
675class ProxyPolicy(SansIOHTTPPolicy):
676    """A proxy policy.
677
678    Dictionary mapping protocol or protocol and host to the URL of the proxy
679    to be used on each Request.
680
681    :param dict proxies: Maps protocol or protocol and hostname to the URL
682     of the proxy.
683
684    .. admonition:: Example:
685
686        .. literalinclude:: ../samples/test_example_sansio.py
687            :start-after: [START proxy_policy]
688            :end-before: [END proxy_policy]
689            :language: python
690            :dedent: 4
691            :caption: Configuring a proxy policy.
692    """
693    def __init__(self, proxies=None, **kwargs):  #pylint: disable=unused-argument,super-init-not-called
694        self.proxies = proxies
695
696    def on_request(self, request):
697        # type: (PipelineRequest) -> None
698        ctxt = request.context.options
699        if self.proxies and "proxies" not in ctxt:
700            ctxt["proxies"] = self.proxies
701