diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/msrest/universal_http/__init__.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/msrest/universal_http/__init__.py | 460 |
1 files changed, 460 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/msrest/universal_http/__init__.py b/.venv/lib/python3.12/site-packages/msrest/universal_http/__init__.py new file mode 100644 index 00000000..60927559 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/msrest/universal_http/__init__.py @@ -0,0 +1,460 @@ +# -------------------------------------------------------------------------- +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# The MIT License (MIT) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the ""Software""), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# -------------------------------------------------------------------------- +from __future__ import absolute_import # we have a "requests" module that conflicts with "requests" on Py2.7 +import abc +try: + import configparser + from configparser import NoOptionError +except ImportError: + import ConfigParser as configparser # type: ignore + from ConfigParser import NoOptionError # type: ignore +import json +import logging +import os.path +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse +import xml.etree.ElementTree as ET + +from typing import TYPE_CHECKING, Generic, TypeVar, cast, IO, List, Union, Any, Mapping, Dict, Optional, Tuple, Callable, Iterator, MutableMapping # pylint: disable=unused-import + +HTTPResponseType = TypeVar("HTTPResponseType", bound='HTTPClientResponse') + +# This file is NOT using any "requests" HTTP implementation +# However, the CaseInsensitiveDict is handy. +# If one day we reach the point where "requests" can be skip totally, +# might provide our own implementation +from requests.structures import CaseInsensitiveDict + +from ..exceptions import ClientRequestError, raise_with_traceback + +if TYPE_CHECKING: + from ..serialization import Model # pylint: disable=unused-import + + +_LOGGER = logging.getLogger(__name__) + +try: + ABC = abc.ABC +except AttributeError: # Python 2.7, abc exists, but not ABC + ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) # type: ignore + +try: + from contextlib import AbstractContextManager # type: ignore +except ImportError: # Python <= 3.5 + class AbstractContextManager(object): # type: ignore + def __enter__(self): + """Return `self` upon entering the runtime context.""" + return self + + @abc.abstractmethod + def __exit__(self, exc_type, exc_value, traceback): + """Raise any exception triggered within the runtime context.""" + return None + + +class HTTPSender(AbstractContextManager, ABC): + """An http sender ABC. + """ + + @abc.abstractmethod + def send(self, request, **config): + # type: (ClientRequest, Any) -> ClientResponse + """Send the request using this HTTP sender. + """ + pass + + +class HTTPSenderConfiguration(object): + """HTTP sender configuration. + + This is composed of generic HTTP configuration, and could be use as a common + HTTP configuration format. + + :param str filepath: Path to existing config file (optional). + """ + + def __init__(self, filepath=None): + # Communication configuration + self.connection = ClientConnection() + + # Headers (sent with every requests) + self.headers = {} # type: Dict[str, str] + + # ProxyConfiguration + self.proxies = ClientProxies() + + # Redirect configuration + self.redirect_policy = ClientRedirectPolicy() + + self._config = configparser.ConfigParser() + self._config.optionxform = str # type: ignore + + if filepath: + self.load(filepath) + + def _clear_config(self): + # type: () -> None + """Clearout config object in memory.""" + for section in self._config.sections(): + self._config.remove_section(section) + + def save(self, filepath): + # type: (str) -> None + """Save current configuration to file. + + :param str filepath: Path to file where settings will be saved. + :raises: ValueError if supplied filepath cannot be written to. + """ + sections = [ + "Connection", + "Proxies", + "RedirectPolicy"] + for section in sections: + self._config.add_section(section) + + self._config.set("Connection", "timeout", self.connection.timeout) + self._config.set("Connection", "verify", self.connection.verify) + self._config.set("Connection", "cert", self.connection.cert) + + self._config.set("Proxies", "proxies", self.proxies.proxies) + self._config.set("Proxies", "env_settings", + self.proxies.use_env_settings) + + self._config.set("RedirectPolicy", "allow", self.redirect_policy.allow) + self._config.set("RedirectPolicy", "max_redirects", + self.redirect_policy.max_redirects) + + try: + with open(filepath, 'w') as configfile: + self._config.write(configfile) + except (KeyError, EnvironmentError): + error = "Supplied config filepath invalid." + raise_with_traceback(ValueError, error) + finally: + self._clear_config() + + def load(self, filepath): + # type: (str) -> None + """Load configuration from existing file. + + :param str filepath: Path to existing config file. + :raises: ValueError if supplied config file is invalid. + """ + try: + self._config.read(filepath) + import ast + self.connection.timeout = \ + self._config.getint("Connection", "timeout") + self.connection.verify = \ + self._config.getboolean("Connection", "verify") + self.connection.cert = \ + self._config.get("Connection", "cert") + + self.proxies.proxies = \ + ast.literal_eval(self._config.get("Proxies", "proxies")) + self.proxies.use_env_settings = \ + self._config.getboolean("Proxies", "env_settings") + + self.redirect_policy.allow = \ + self._config.getboolean("RedirectPolicy", "allow") + self.redirect_policy.max_redirects = \ + self._config.getint("RedirectPolicy", "max_redirects") + + except (ValueError, EnvironmentError, NoOptionError): + error = "Supplied config file incompatible." + raise_with_traceback(ValueError, error) + finally: + self._clear_config() + + +class ClientRequest(object): + """Represents a HTTP request. + + URL can be given without query parameters, to be added later using "format_parameters". + + Instance can be created without data, to be added later using "add_content" + + Instance can be created without files, to be added later using "add_formdata" + + :param str method: HTTP method (GET, HEAD, etc.) + :param str url: At least complete scheme/host/path + :param dict[str,str] headers: HTTP headers + :param files: Files list. + :param data: Body to be sent. + :type data: bytes or str. + """ + def __init__(self, method, url, headers=None, files=None, data=None): + # type: (str, str, Mapping[str, str], Any, Any) -> None + self.method = method + self.url = url + self.headers = CaseInsensitiveDict(headers) + self.files = files + self.data = data + + def __repr__(self): + return '<ClientRequest [%s]>' % (self.method) + + @property + def body(self): + """Alias to data.""" + return self.data + + @body.setter + def body(self, value): + self.data = value + + def format_parameters(self, params): + # type: (Dict[str, str]) -> None + """Format parameters into a valid query string. + It's assumed all parameters have already been quoted as + valid URL strings. + + :param dict params: A dictionary of parameters. + """ + query = urlparse(self.url).query + if query: + self.url = self.url.partition('?')[0] + existing_params = { + p[0]: p[-1] + for p in [p.partition('=') for p in query.split('&')] + } + params.update(existing_params) + query_params = ["{}={}".format(k, v) for k, v in params.items()] + query = '?' + '&'.join(query_params) + self.url = self.url + query + + def add_content(self, data): + # type: (Optional[Union[Dict[str, Any], ET.Element]]) -> None + """Add a body to the request. + + :param data: Request body data, can be a json serializable + object (e.g. dictionary) or a generator (e.g. file data). + """ + if data is None: + return + + if isinstance(data, ET.Element): + bytes_data = ET.tostring(data, encoding="utf8") + self.headers['Content-Length'] = str(len(bytes_data)) + self.data = bytes_data + return + + # By default, assume JSON + try: + self.data = json.dumps(data) + self.headers['Content-Length'] = str(len(self.data)) + except TypeError: + self.data = data + + @staticmethod + def _format_data(data): + # type: (Union[str, IO]) -> Union[Tuple[None, str], Tuple[Optional[str], IO, str]] + """Format field data according to whether it is a stream or + a string for a form-data request. + + :param data: The request field data. + :type data: str or file-like object. + """ + if hasattr(data, 'read'): + data = cast(IO, data) + data_name = None + try: + if data.name[0] != '<' and data.name[-1] != '>': + data_name = os.path.basename(data.name) + except (AttributeError, TypeError): + pass + return (data_name, data, "application/octet-stream") + return (None, cast(str, data)) + + def add_formdata(self, content=None): + # type: (Optional[Dict[str, str]]) -> None + """Add data as a multipart form-data request to the request. + + We only deal with file-like objects or strings at this point. + The requests is not yet streamed. + + :param dict headers: Any headers to add to the request. + :param dict content: Dictionary of the fields of the formdata. + """ + if content is None: + content = {} + content_type = self.headers.pop('Content-Type', None) if self.headers else None + + if content_type and content_type.lower() == 'application/x-www-form-urlencoded': + # Do NOT use "add_content" that assumes input is JSON + self.data = {f: d for f, d in content.items() if d is not None} + else: # Assume "multipart/form-data" + self.files = {f: self._format_data(d) for f, d in content.items() if d is not None} + +class HTTPClientResponse(object): + """Represent a HTTP response. + + No body is defined here on purpose, since async pipeline + will provide async ways to access the body + + You have two different types of body: + - Full in-memory using "body" as bytes + """ + def __init__(self, request, internal_response): + # type: (ClientRequest, Any) -> None + self.request = request + self.internal_response = internal_response + self.status_code = None # type: Optional[int] + self.headers = {} # type: MutableMapping[str, str] + self.reason = None # type: Optional[str] + + def body(self): + # type: () -> bytes + """Return the whole body as bytes in memory. + """ + pass + + def text(self, encoding=None): + # type: (str) -> str + """Return the whole body as a string. + + :param str encoding: The encoding to apply. If None, use "utf-8-sig". + Implementation can be smarter if they want (using headers). + """ + return self.body().decode(encoding or "utf-8-sig") + + def raise_for_status(self): + """Raise for status. Should be overridden, but basic implementation provided. + """ + if self.status_code >= 400: + raise ClientRequestError("Received status code {}".format(self.status_code)) + + +class ClientResponse(HTTPClientResponse): + + def stream_download(self, chunk_size=None, callback=None): + # type: (Optional[int], Optional[Callable]) -> Iterator[bytes] + """Generator for streaming request body data. + + Should be implemented by sub-classes if streaming download + is supported. + + :param callback: Custom callback for monitoring progress. + :param int chunk_size: + """ + pass + + +class ClientRedirectPolicy(object): + """Redirect configuration settings. + """ + + def __init__(self): + self.allow = True + self.max_redirects = 30 + + def __bool__(self): + # type: () -> bool + """Whether redirects are allowed.""" + return self.allow + + def __call__(self): + # type: () -> int + """Return configuration to be applied to connection.""" + debug = "Configuring redirects: allow=%r, max=%r" + _LOGGER.debug(debug, self.allow, self.max_redirects) + return self.max_redirects + + +class ClientProxies(object): + """Proxy configuration settings. + Proxies can also be configured using HTTP_PROXY and HTTPS_PROXY + environment variables, in which case set use_env_settings to True. + """ + + def __init__(self): + self.proxies = {} + self.use_env_settings = True + + def __call__(self): + # type: () -> Dict[str, str] + """Return configuration to be applied to connection.""" + proxy_string = "\n".join( + [" {}: {}".format(k, v) for k, v in self.proxies.items()]) + + _LOGGER.debug("Configuring proxies: %r", proxy_string) + debug = "Evaluate proxies against ENV settings: %r" + _LOGGER.debug(debug, self.use_env_settings) + return self.proxies + + def add(self, protocol, proxy_url): + # type: (str, str) -> None + """Add proxy. + + :param str protocol: Protocol for which proxy is to be applied. Can + be 'http', 'https', etc. Can also include host. + :param str proxy_url: The proxy URL. Where basic auth is required, + use the format: http://user:password@host + """ + self.proxies[protocol] = proxy_url + + +class ClientConnection(object): + """Request connection configuration settings. + """ + + def __init__(self): + self.timeout = 100 + self.verify = True + self.cert = None + self.data_block_size = 4096 + + def __call__(self): + # type: () -> Dict[str, Union[str, int]] + """Return configuration to be applied to connection.""" + debug = "Configuring request: timeout=%r, verify=%r, cert=%r" + _LOGGER.debug(debug, self.timeout, self.verify, self.cert) + return {'timeout': self.timeout, + 'verify': self.verify, + 'cert': self.cert} + + +__all__ = [ + 'ClientRequest', + 'ClientResponse', + 'HTTPSender', + # Generic HTTP configuration + 'HTTPSenderConfiguration', + 'ClientRedirectPolicy', + 'ClientProxies', + 'ClientConnection' +] + +try: + from .async_abc import AsyncHTTPSender, AsyncClientResponse # pylint: disable=unused-import + from .async_abc import __all__ as _async_all + __all__ += _async_all +except SyntaxError: # Python 2 + pass +except ImportError: # pyinstaller won't include Py3 files in Py2.7 mode + pass |