diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/msrest/universal_http')
5 files changed, 1355 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 diff --git a/.venv/lib/python3.12/site-packages/msrest/universal_http/aiohttp.py b/.venv/lib/python3.12/site-packages/msrest/universal_http/aiohttp.py new file mode 100644 index 00000000..0be8f299 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/msrest/universal_http/aiohttp.py @@ -0,0 +1,101 @@ +# -------------------------------------------------------------------------- +# +# 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 typing import Any, Callable, AsyncIterator, Optional + +import aiohttp +from multidict import CIMultiDict + +from . import AsyncHTTPSender, ClientRequest, AsyncClientResponse + +# Matching requests, because why not? +CONTENT_CHUNK_SIZE = 10 * 1024 + + +class AioHTTPSender(AsyncHTTPSender): + """AioHttp HTTP sender implementation. + """ + + def __init__(self, *, loop=None): + self._session = aiohttp.ClientSession(loop=loop) + + async def __aenter__(self): + await self._session.__aenter__() + return self + + async def __aexit__(self, *exc_details): # pylint: disable=arguments-differ + await self._session.__aexit__(*exc_details) + + async def send(self, request: ClientRequest, **config: Any) -> AsyncClientResponse: + """Send the request using this HTTP sender. + + Will pre-load the body into memory to be available with a sync method. + pass stream=True to avoid this behavior. + """ + result = await self._session.request( + request.method, + request.url, + **config + ) + response = AioHttpClientResponse(request, result) + if not config.get("stream", False): + await response.load_body() + return response + + +class AioHttpClientResponse(AsyncClientResponse): + def __init__(self, request: ClientRequest, aiohttp_response: aiohttp.ClientResponse) -> None: + super(AioHttpClientResponse, self).__init__(request, aiohttp_response) + # https://aiohttp.readthedocs.io/en/stable/client_reference.html#aiohttp.ClientResponse + self.status_code = aiohttp_response.status + self.headers = CIMultiDict(aiohttp_response.headers) + self.reason = aiohttp_response.reason + self._body = None + + def body(self) -> bytes: + """Return the whole body as bytes in memory. + """ + if not self._body: + raise ValueError("Body is not available. Call async method load_body, or do your call with stream=False.") + return self._body + + async def load_body(self) -> None: + """Load in memory the body, so it could be accessible from sync methods.""" + self._body = await self.internal_response.read() + + def raise_for_status(self): + self.internal_response.raise_for_status() + + def stream_download(self, chunk_size: Optional[int] = None, callback: Optional[Callable] = None) -> AsyncIterator[bytes]: + """Generator for streaming request body data. + """ + chunk_size = chunk_size or CONTENT_CHUNK_SIZE + async def async_gen(resp): + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + callback(chunk, resp) + return async_gen(self.internal_response) diff --git a/.venv/lib/python3.12/site-packages/msrest/universal_http/async_abc.py b/.venv/lib/python3.12/site-packages/msrest/universal_http/async_abc.py new file mode 100644 index 00000000..1b7f4c7c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/msrest/universal_http/async_abc.py @@ -0,0 +1,90 @@ +# -------------------------------------------------------------------------- +# +# 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. +# +# -------------------------------------------------------------------------- +import abc + +from typing import Any, List, Union, Callable, AsyncIterator, Optional + +try: + from contextlib import AbstractAsyncContextManager # type: ignore +except ImportError: # Python <= 3.7 + class AbstractAsyncContextManager(object): # type: ignore + async def __aenter__(self): + """Return `self` upon entering the runtime context.""" + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + """Raise any exception triggered within the runtime context.""" + return None + +from . import ClientRequest, HTTPClientResponse + + +class AsyncClientResponse(HTTPClientResponse): + + def stream_download(self, chunk_size: Optional[int] = None, callback: Optional[Callable] = None) -> AsyncIterator[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 AsyncHTTPSender(AbstractAsyncContextManager, abc.ABC): + """An http sender ABC. + """ + + @abc.abstractmethod + async def send(self, request: ClientRequest, **config: Any) -> AsyncClientResponse: + """Send the request using this HTTP sender. + """ + pass + + def build_context(self) -> Any: + """Allow the sender to build a context that will be passed + across the pipeline with the request. + + Return type has no constraints. Implementation is not + required and None by default. + """ + return None + + def __enter__(self): + raise TypeError("Use 'async with' instead") + + def __exit__(self, exc_type, exc_val, exc_tb): + # __exit__ should exist in pair with __enter__ but never executed + pass # pragma: no cover + + +__all__ = [ + 'AsyncHTTPSender', + 'AsyncClientResponse' +]
\ No newline at end of file diff --git a/.venv/lib/python3.12/site-packages/msrest/universal_http/async_requests.py b/.venv/lib/python3.12/site-packages/msrest/universal_http/async_requests.py new file mode 100644 index 00000000..40a3a352 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/msrest/universal_http/async_requests.py @@ -0,0 +1,240 @@ +# -------------------------------------------------------------------------- +# +# 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. +# +# -------------------------------------------------------------------------- +import asyncio +from collections.abc import AsyncIterator +import functools +import logging +from typing import Any, Callable, Optional, AsyncIterator as AsyncIteratorType + +from oauthlib import oauth2 +import requests +from requests.models import CONTENT_CHUNK_SIZE + +from ..exceptions import ( + TokenExpiredError, + ClientRequestError, + raise_with_traceback) +from . import AsyncHTTPSender, ClientRequest, AsyncClientResponse +from .requests import ( + BasicRequestsHTTPSender, + RequestsHTTPSender, + HTTPRequestsClientResponse +) + + +_LOGGER = logging.getLogger(__name__) + + +class AsyncBasicRequestsHTTPSender(BasicRequestsHTTPSender, AsyncHTTPSender): # type: ignore + + async def __aenter__(self): + return super(AsyncBasicRequestsHTTPSender, self).__enter__() + + async def __aexit__(self, *exc_details): # pylint: disable=arguments-differ + return super(AsyncBasicRequestsHTTPSender, self).__exit__() + + async def send(self, request: ClientRequest, **kwargs: Any) -> AsyncClientResponse: # type: ignore + """Send the request using this HTTP sender. + """ + # It's not recommended to provide its own session, and is mostly + # to enable some legacy code to plug correctly + session = kwargs.pop('session', self.session) + + loop = kwargs.get("loop", asyncio.get_event_loop()) + future = loop.run_in_executor( + None, + functools.partial( + session.request, + request.method, + request.url, + **kwargs + ) + ) + try: + return AsyncRequestsClientResponse( + request, + await future + ) + except requests.RequestException as err: + msg = "Error occurred in request." + raise_with_traceback(ClientRequestError, msg, err) + +class AsyncRequestsHTTPSender(AsyncBasicRequestsHTTPSender, RequestsHTTPSender): # type: ignore + + async def send(self, request: ClientRequest, **kwargs: Any) -> AsyncClientResponse: # type: ignore + """Send the request using this HTTP sender. + """ + requests_kwargs = self._configure_send(request, **kwargs) + return await super(AsyncRequestsHTTPSender, self).send(request, **requests_kwargs) + + +class _MsrestStopIteration(Exception): + pass + +def _msrest_next(iterator): + """"To avoid: + TypeError: StopIteration interacts badly with generators and cannot be raised into a Future + """ + try: + return next(iterator) + except StopIteration: + raise _MsrestStopIteration() + +class StreamDownloadGenerator(AsyncIterator): + + def __init__(self, response: requests.Response, user_callback: Optional[Callable] = None, block: Optional[int] = None) -> None: + self.response = response + self.block = block or CONTENT_CHUNK_SIZE + self.user_callback = user_callback + self.iter_content_func = self.response.iter_content(self.block) + + async def __anext__(self): + loop = asyncio.get_event_loop() + try: + chunk = await loop.run_in_executor( + None, + _msrest_next, + self.iter_content_func, + ) + if not chunk: + raise _MsrestStopIteration() + if self.user_callback and callable(self.user_callback): + self.user_callback(chunk, self.response) + return chunk + except _MsrestStopIteration: + self.response.close() + raise StopAsyncIteration() + except Exception as err: + _LOGGER.warning("Unable to stream download: %s", err) + self.response.close() + raise + +class AsyncRequestsClientResponse(AsyncClientResponse, HTTPRequestsClientResponse): + + def stream_download(self, chunk_size: Optional[int] = None, callback: Optional[Callable] = None) -> AsyncIteratorType[bytes]: + """Generator for streaming request body data. + + :param callback: Custom callback for monitoring progress. + :param int chunk_size: + """ + return StreamDownloadGenerator( + self.internal_response, + callback, + chunk_size + ) + + +# Trio support +try: + import trio + + class TrioStreamDownloadGenerator(AsyncIterator): + + def __init__(self, response: requests.Response, user_callback: Optional[Callable] = None, block: Optional[int] = None) -> None: + self.response = response + self.block = block or CONTENT_CHUNK_SIZE + self.user_callback = user_callback + self.iter_content_func = self.response.iter_content(self.block) + + async def __anext__(self): + try: + chunk = await trio.to_thread.run_sync( + _msrest_next, + self.iter_content_func, + ) + if not chunk: + raise _MsrestStopIteration() + if self.user_callback and callable(self.user_callback): + self.user_callback(chunk, self.response) + return chunk + except _MsrestStopIteration: + self.response.close() + raise StopAsyncIteration() + except Exception as err: + _LOGGER.warning("Unable to stream download: %s", err) + self.response.close() + raise + + class TrioAsyncRequestsClientResponse(AsyncClientResponse, HTTPRequestsClientResponse): + + def stream_download(self, chunk_size: Optional[int] = None, callback: Optional[Callable] = None) -> AsyncIteratorType[bytes]: + """Generator for streaming request body data. + + :param callback: Custom callback for monitoring progress. + :param int chunk_size: + """ + return TrioStreamDownloadGenerator( + self.internal_response, + callback, + chunk_size + ) + + + class AsyncTrioBasicRequestsHTTPSender(BasicRequestsHTTPSender, AsyncHTTPSender): # type: ignore + + async def __aenter__(self): + return super(AsyncTrioBasicRequestsHTTPSender, self).__enter__() + + async def __aexit__(self, *exc_details): # pylint: disable=arguments-differ + return super(AsyncTrioBasicRequestsHTTPSender, self).__exit__() + + async def send(self, request: ClientRequest, **kwargs: Any) -> AsyncClientResponse: # type: ignore + """Send the request using this HTTP sender. + """ + # It's not recommended to provide its own session, and is mostly + # to enable some legacy code to plug correctly + session = kwargs.pop('session', self.session) + + trio_limiter = kwargs.get("trio_limiter", None) + future = trio.to_thread.run_sync( + functools.partial( + session.request, + request.method, + request.url, + **kwargs + ), + limiter=trio_limiter + ) + try: + return TrioAsyncRequestsClientResponse( + request, + await future + ) + except requests.RequestException as err: + msg = "Error occurred in request." + raise_with_traceback(ClientRequestError, msg, err) + + class AsyncTrioRequestsHTTPSender(AsyncTrioBasicRequestsHTTPSender, RequestsHTTPSender): # type: ignore + + async def send(self, request: ClientRequest, **kwargs: Any) -> AsyncClientResponse: # type: ignore + """Send the request using this HTTP sender. + """ + requests_kwargs = self._configure_send(request, **kwargs) + return await super(AsyncTrioRequestsHTTPSender, self).send(request, **requests_kwargs) + +except ImportError: + # trio not installed + pass
\ No newline at end of file diff --git a/.venv/lib/python3.12/site-packages/msrest/universal_http/requests.py b/.venv/lib/python3.12/site-packages/msrest/universal_http/requests.py new file mode 100644 index 00000000..e5e04632 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/msrest/universal_http/requests.py @@ -0,0 +1,464 @@ +# -------------------------------------------------------------------------- +# +# 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. +# +# -------------------------------------------------------------------------- +""" +This module is the requests implementation of Pipeline ABC +""" +from __future__ import absolute_import # we have a "requests" module that conflicts with "requests" on Py2.7 +import contextlib +import logging +import threading +from typing import TYPE_CHECKING, List, Callable, Iterator, Any, Union, Dict, Optional # pylint: disable=unused-import +import warnings + +try: + from configparser import NoOptionError +except ImportError: + from ConfigParser import NoOptionError # type: ignore + +from oauthlib import oauth2 +import requests +from requests.models import CONTENT_CHUNK_SIZE + +from urllib3 import Retry # Needs requests 2.16 at least to be safe + +from ..exceptions import ( + TokenExpiredError, + ClientRequestError, + raise_with_traceback) +from . import HTTPSender, HTTPClientResponse, ClientResponse, HTTPSenderConfiguration + +if TYPE_CHECKING: + from . import ClientRequest # pylint: disable=unused-import + + +_LOGGER = logging.getLogger(__name__) + + + +class HTTPRequestsClientResponse(HTTPClientResponse): + def __init__(self, request, requests_response): + super(HTTPRequestsClientResponse, self).__init__(request, requests_response) + self.status_code = requests_response.status_code + self.headers = requests_response.headers + self.reason = requests_response.reason + + def body(self): + return self.internal_response.content + + def text(self, encoding=None): + if encoding: + self.internal_response.encoding = encoding + return self.internal_response.text + + def raise_for_status(self): + self.internal_response.raise_for_status() + +class RequestsClientResponse(HTTPRequestsClientResponse, ClientResponse): + + def stream_download(self, chunk_size=None, callback=None): + # type: (Optional[int], Optional[Callable]) -> Iterator[bytes] + """Generator for streaming request body data. + + :param callback: Custom callback for monitoring progress. + :param int chunk_size: + """ + chunk_size = chunk_size or CONTENT_CHUNK_SIZE + with contextlib.closing(self.internal_response) as response: + # https://github.com/PyCQA/pylint/issues/1437 + for chunk in response.iter_content(chunk_size): # pylint: disable=no-member + if not chunk: + break + if callback and callable(callback): + callback(chunk, response=response) + yield chunk + + +class BasicRequestsHTTPSender(HTTPSender): + """Implements a basic requests HTTP sender. + + Since requests team recommends to use one session per requests, you should + not consider this class as thread-safe, since it will use one Session + per instance. + + In this simple implementation: + - You provide the configured session if you want to, or a basic session is created. + - All kwargs received by "send" are sent to session.request directly + """ + + def __init__(self, session=None): + # type: (Optional[requests.Session]) -> None + self.session = session or requests.Session() + + def __enter__(self): + # type: () -> BasicRequestsHTTPSender + return self + + def __exit__(self, *exc_details): # pylint: disable=arguments-differ + self.close() + + def close(self): + self.session.close() + + def send(self, request, **kwargs): + # type: (ClientRequest, Any) -> ClientResponse + """Send request object according to configuration. + + Allowed kwargs are: + - session : will override the driver session and use yours. Should NOT be done unless really required. + - anything else is sent straight to requests. + + :param ClientRequest request: The request object to be sent. + """ + # It's not recommended to provide its own session, and is mostly + # to enable some legacy code to plug correctly + session = kwargs.pop('session', self.session) + try: + response = session.request( + request.method, + request.url, + **kwargs) + except requests.RequestException as err: + msg = "Error occurred in request." + raise_with_traceback(ClientRequestError, msg, err) + + return RequestsClientResponse(request, response) + + +def _patch_redirect(session): + # type: (requests.Session) -> None + """Whether redirect policy should be applied based on status code. + + HTTP spec says that on 301/302 not HEAD/GET, should NOT redirect. + But requests does, to follow browser more than spec + https://github.com/requests/requests/blob/f6e13ccfc4b50dc458ee374e5dba347205b9a2da/requests/sessions.py#L305-L314 + + This patches "requests" to be more HTTP compliant. + + Note that this is super dangerous, since technically this is not public API. + """ + def enforce_http_spec(resp, request): + if resp.status_code in (301, 302) and \ + request.method not in ['GET', 'HEAD']: + return False + return True + + redirect_logic = session.resolve_redirects + + def wrapped_redirect(resp, req, **kwargs): + attempt = enforce_http_spec(resp, req) + return redirect_logic(resp, req, **kwargs) if attempt else [] + wrapped_redirect.is_msrest_patched = True # type: ignore + + session.resolve_redirects = wrapped_redirect # type: ignore + +class RequestsHTTPSender(BasicRequestsHTTPSender): + """A requests HTTP sender that can consume a msrest.Configuration object. + + This instance will consume the following configuration attributes: + - connection + - proxies + - retry_policy + - redirect_policy + - enable_http_logger + - hooks + - session_configuration_callback + """ + + _protocols = ['http://', 'https://'] + + # Set of authorized kwargs at the operation level + _REQUESTS_KWARGS = [ + 'cookies', + 'verify', + 'timeout', + 'allow_redirects', + 'proxies', + 'verify', + 'cert' + ] + + def __init__(self, config=None): + # type: (Optional[RequestHTTPSenderConfiguration]) -> None + self._session_mapping = threading.local() + self.config = config or RequestHTTPSenderConfiguration() + super(RequestsHTTPSender, self).__init__() + + @property # type: ignore + def session(self): + try: + return self._session_mapping.session + except AttributeError: + self._session_mapping.session = requests.Session() + self._init_session(self._session_mapping.session) + return self._session_mapping.session + + @session.setter + def session(self, value): + self._init_session(value) + self._session_mapping.session = value + + def _init_session(self, session): + # type: (requests.Session) -> None + """Init session level configuration of requests. + + This is initialization I want to do once only on a session. + """ + _patch_redirect(session) + + # Change max_retries in current all installed adapters + max_retries = self.config.retry_policy() + for protocol in self._protocols: + session.adapters[protocol].max_retries = max_retries + + def _configure_send(self, request, **kwargs): + # type: (ClientRequest, Any) -> Dict[str, str] + """Configure the kwargs to use with requests. + + See "send" for kwargs details. + + :param ClientRequest request: The request object to be sent. + :returns: The requests.Session.request kwargs + :rtype: dict[str,str] + """ + requests_kwargs = {} # type: Any + session = kwargs.pop('session', self.session) + + # If custom session was not create here + if session is not self.session: + self._init_session(session) + + session.max_redirects = int(self.config.redirect_policy()) + session.trust_env = bool(self.config.proxies.use_env_settings) + + # Initialize requests_kwargs with "config" value + requests_kwargs.update(self.config.connection()) + requests_kwargs['allow_redirects'] = bool(self.config.redirect_policy) + requests_kwargs['headers'] = self.config.headers.copy() + + proxies = self.config.proxies() + if proxies: + requests_kwargs['proxies'] = proxies + + # Replace by operation level kwargs + # We allow some of them, since some like stream or json are controlled by msrest + for key in kwargs: + if key in self._REQUESTS_KWARGS: + requests_kwargs[key] = kwargs[key] + + # Hooks. Deprecated, should be a policy + def make_user_hook_cb(user_hook, session): + def user_hook_cb(r, *args, **kwargs): + kwargs.setdefault("msrest", {})['session'] = session + return user_hook(r, *args, **kwargs) + return user_hook_cb + + hooks = [] + for user_hook in self.config.hooks: + hooks.append(make_user_hook_cb(user_hook, self.session)) + + if hooks: + requests_kwargs['hooks'] = {'response': hooks} + + # Configuration callback. Deprecated, should be a policy + output_kwargs = self.config.session_configuration_callback( + session, + self.config, + kwargs, + **requests_kwargs + ) + if output_kwargs is not None: + requests_kwargs = output_kwargs + + # If custom session was not create here + if session is not self.session: + requests_kwargs['session'] = session + + ### Autorest forced kwargs now ### + + # If Autorest needs this response to be streamable. True for compat. + requests_kwargs['stream'] = kwargs.get('stream', True) + + if request.files: + requests_kwargs['files'] = request.files + elif request.data: + requests_kwargs['data'] = request.data + requests_kwargs['headers'].update(request.headers) + + return requests_kwargs + + def send(self, request, **kwargs): + # type: (ClientRequest, Any) -> ClientResponse + """Send request object according to configuration. + + Available kwargs: + - session : will override the driver session and use yours. Should NOT be done unless really required. + - A subset of what requests.Session.request can receive: + + - cookies + - verify + - timeout + - allow_redirects + - proxies + - verify + - cert + + Everything else will be silently ignored. + + :param ClientRequest request: The request object to be sent. + """ + requests_kwargs = self._configure_send(request, **kwargs) + return super(RequestsHTTPSender, self).send(request, **requests_kwargs) + + +class ClientRetryPolicy(object): + """Retry configuration settings. + Container for retry policy object. + """ + + safe_codes = [i for i in range(500) if i != 408] + [501, 505] + + def __init__(self): + self.policy = Retry() + self.policy.total = 3 + self.policy.connect = 3 + self.policy.read = 3 + self.policy.backoff_factor = 0.8 + self.policy.BACKOFF_MAX = 90 + + retry_codes = [i for i in range(999) if i not in self.safe_codes] + self.policy.status_forcelist = retry_codes + self.policy.method_whitelist = ['HEAD', 'TRACE', 'GET', 'PUT', + 'OPTIONS', 'DELETE', 'POST', 'PATCH'] + + def __call__(self): + # type: () -> Retry + """Return configuration to be applied to connection.""" + debug = ("Configuring retry: max_retries=%r, " + "backoff_factor=%r, max_backoff=%r") + _LOGGER.debug( + debug, self.retries, self.backoff_factor, self.max_backoff) + return self.policy + + @property + def retries(self): + # type: () -> int + """Total number of allowed retries.""" + return self.policy.total + + @retries.setter + def retries(self, value): + # type: (int) -> None + self.policy.total = value + self.policy.connect = value + self.policy.read = value + + @property + def backoff_factor(self): + # type: () -> Union[int, float] + """Factor by which back-off delay is incrementally increased.""" + return self.policy.backoff_factor + + @backoff_factor.setter + def backoff_factor(self, value): + # type: (Union[int, float]) -> None + self.policy.backoff_factor = value + + @property + def max_backoff(self): + # type: () -> int + """Max retry back-off delay.""" + return self.policy.BACKOFF_MAX + + @max_backoff.setter + def max_backoff(self, value): + # type: (int) -> None + self.policy.BACKOFF_MAX = value + +def default_session_configuration_callback(session, global_config, local_config, **kwargs): # pylint: disable=unused-argument + # type: (requests.Session, RequestHTTPSenderConfiguration, Dict[str,str], str) -> Dict[str, str] + """Configuration callback if you need to change default session configuration. + + :param requests.Session session: The session. + :param Configuration global_config: The global configuration. + :param dict[str,str] local_config: The on-the-fly configuration passed on the call. + :param dict[str,str] kwargs: The current computed values for session.request method. + :return: Must return kwargs, to be passed to session.request. If None is return, initial kwargs will be used. + :rtype: dict[str,str] + """ + return kwargs + +class RequestHTTPSenderConfiguration(HTTPSenderConfiguration): + """Requests specific HTTP sender configuration. + + :param str filepath: Path to existing config file (optional). + """ + + def __init__(self, filepath=None): + # type: (Optional[str]) -> None + + super(RequestHTTPSenderConfiguration, self).__init__() + + # Retry configuration + self.retry_policy = ClientRetryPolicy() + + # Requests hooks. Must respect requests hook callback signature + # Note that we will inject the following parameters: + # - kwargs['msrest']['session'] with the current session + self.hooks = [] # type: List[Callable[[requests.Response, str, str], None]] + + self.session_configuration_callback = default_session_configuration_callback + + if filepath: + self.load(filepath) + + def save(self, filepath): + """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. + """ + self._config.add_section("RetryPolicy") + self._config.set("RetryPolicy", "retries", str(self.retry_policy.retries)) + self._config.set("RetryPolicy", "backoff_factor", + str(self.retry_policy.backoff_factor)) + self._config.set("RetryPolicy", "max_backoff", + str(self.retry_policy.max_backoff)) + super(RequestHTTPSenderConfiguration, self).save(filepath) + + def load(self, filepath): + try: + self.retry_policy.retries = \ + self._config.getint("RetryPolicy", "retries") + self.retry_policy.backoff_factor = \ + self._config.getfloat("RetryPolicy", "backoff_factor") + self.retry_policy.max_backoff = \ + self._config.getint("RetryPolicy", "max_backoff") + except (ValueError, EnvironmentError, NoOptionError): + error = "Supplied config file incompatible." + raise_with_traceback(ValueError, error) + finally: + self._clear_config() + super(RequestHTTPSenderConfiguration, self).load(filepath) |