back to Reference (Gold) summary
Reference (Gold): requests
Pytest Summary for test tests
status | count |
---|---|
passed | 590 |
skipped | 15 |
xfailed | 1 |
total | 606 |
collected | 606 |
Failed pytests:
test_requests.py::TestRequests::test_response_iter_lines_reentrant
test_requests.py::TestRequests::test_response_iter_lines_reentrant
self =httpbin = .inner at 0x7f535da3cea0> @pytest.mark.xfail def test_response_iter_lines_reentrant(self, httpbin): """Response.iter_lines() is not reentrant safe""" r = requests.get(httpbin("stream/4"), stream=True) assert r.status_code == 200 next(r.iter_lines()) > assert len(list(r.iter_lines())) == 3 E assert 2 == 3 E + where 2 = len([b'"http://127.0.0.1:51300/stream/4", "args": {}, "headers": {"Host": "127.0.0.1:51300", "User-Agent": "python-request..."Accept-Encoding": "gzip, deflate, br", "Accept": "*/*", "Connection": "keep-alive"}, "origin": "127.0.0.1", "id": 3}']) E + where [b'"http://127.0.0.1:51300/stream/4", "args": {}, "headers": {"Host": "127.0.0.1:51300", "User-Agent": "python-request..."Accept-Encoding": "gzip, deflate, br", "Accept": "*/*", "Connection": "keep-alive"}, "origin": "127.0.0.1", "id": 3}'] = list( ) E + where = iter_lines() E + where iter_lines = .iter_lines tests/test_requests.py:2112: AssertionError
Patch diff
diff --git a/src/requests/__version__.py b/src/requests/__version__.py
index e2c6c146..2c105aca 100644
--- a/src/requests/__version__.py
+++ b/src/requests/__version__.py
@@ -1,10 +1,14 @@
-__title__ = 'requests'
-__description__ = 'Python HTTP for Humans.'
-__url__ = 'https://requests.readthedocs.io'
-__version__ = '2.32.3'
-__build__ = 143875
-__author__ = 'Kenneth Reitz'
-__author_email__ = 'me@kennethreitz.org'
-__license__ = 'Apache-2.0'
-__copyright__ = 'Copyright Kenneth Reitz'
-__cake__ = '✨ 🍰 ✨'
+# .-. .-. .-. . . .-. .-. .-. .-.
+# |( |- |.| | | |- `-. | `-.
+# ' ' `-' `-`.`-' `-' `-' ' `-'
+
+__title__ = "requests"
+__description__ = "Python HTTP for Humans."
+__url__ = "https://requests.readthedocs.io"
+__version__ = "2.32.3"
+__build__ = 0x023203
+__author__ = "Kenneth Reitz"
+__author_email__ = "me@kennethreitz.org"
+__license__ = "Apache-2.0"
+__copyright__ = "Copyright Kenneth Reitz"
+__cake__ = "\u2728 \U0001f370 \u2728"
diff --git a/src/requests/_internal_utils.py b/src/requests/_internal_utils.py
index 4342cd14..f2cf635e 100644
--- a/src/requests/_internal_utils.py
+++ b/src/requests/_internal_utils.py
@@ -6,24 +6,33 @@ Provides utility functions that are consumed internally by Requests
which depend on extremely few external helpers (such as compat)
"""
import re
+
from .compat import builtin_str
-_VALID_HEADER_NAME_RE_BYTE = re.compile(b'^[^:\\s][^:\\r\\n]*$')
-_VALID_HEADER_NAME_RE_STR = re.compile('^[^:\\s][^:\\r\\n]*$')
-_VALID_HEADER_VALUE_RE_BYTE = re.compile(b'^\\S[^\\r\\n]*$|^$')
-_VALID_HEADER_VALUE_RE_STR = re.compile('^\\S[^\\r\\n]*$|^$')
-_HEADER_VALIDATORS_STR = _VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR
-_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE,
- _VALID_HEADER_VALUE_RE_BYTE)
-HEADER_VALIDATORS = {bytes: _HEADER_VALIDATORS_BYTE, str:
- _HEADER_VALIDATORS_STR}
-
-
-def to_native_string(string, encoding='ascii'):
+
+_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$")
+_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$")
+_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$")
+_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$")
+
+_HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR)
+_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE)
+HEADER_VALIDATORS = {
+ bytes: _HEADER_VALIDATORS_BYTE,
+ str: _HEADER_VALIDATORS_STR,
+}
+
+
+def to_native_string(string, encoding="ascii"):
"""Given a string object, regardless of type, returns a representation of
that string in the native string type, encoding and decoding where
necessary. This assumes ASCII unless told otherwise.
"""
- pass
+ if isinstance(string, builtin_str):
+ out = string
+ else:
+ out = string.decode(encoding)
+
+ return out
def unicode_is_ascii(u_string):
@@ -33,4 +42,9 @@ def unicode_is_ascii(u_string):
and not Python 2 `str`.
:rtype: bool
"""
- pass
+ assert isinstance(u_string, str)
+ try:
+ u_string.encode("ascii")
+ return True
+ except UnicodeEncodeError:
+ return False
diff --git a/src/requests/adapters.py b/src/requests/adapters.py
index 57d9ddf8..9a58b160 100644
--- a/src/requests/adapters.py
+++ b/src/requests/adapters.py
@@ -5,14 +5,21 @@ requests.adapters
This module contains the transport adapters that Requests uses to define
and maintain connections.
"""
+
import os.path
-import socket
+import socket # noqa: F401
import typing
import warnings
+
from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
from urllib3.exceptions import HTTPError as _HTTPError
from urllib3.exceptions import InvalidHeader as _InvalidHeader
-from urllib3.exceptions import LocationValueError, MaxRetryError, NewConnectionError, ProtocolError
+from urllib3.exceptions import (
+ LocationValueError,
+ MaxRetryError,
+ NewConnectionError,
+ ProtocolError,
+)
from urllib3.exceptions import ProxyError as _ProxyError
from urllib3.exceptions import ReadTimeoutError, ResponseError
from urllib3.exceptions import SSLError as _SSLError
@@ -21,39 +28,121 @@ from urllib3.util import Timeout as TimeoutSauce
from urllib3.util import parse_url
from urllib3.util.retry import Retry
from urllib3.util.ssl_ import create_urllib3_context
+
from .auth import _basic_auth_str
from .compat import basestring, urlparse
from .cookies import extract_cookies_to_jar
-from .exceptions import ConnectionError, ConnectTimeout, InvalidHeader, InvalidProxyURL, InvalidSchema, InvalidURL, ProxyError, ReadTimeout, RetryError, SSLError
+from .exceptions import (
+ ConnectionError,
+ ConnectTimeout,
+ InvalidHeader,
+ InvalidProxyURL,
+ InvalidSchema,
+ InvalidURL,
+ ProxyError,
+ ReadTimeout,
+ RetryError,
+ SSLError,
+)
from .models import Response
from .structures import CaseInsensitiveDict
-from .utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths, get_auth_from_url, get_encoding_from_headers, prepend_scheme_if_needed, select_proxy, urldefragauth
+from .utils import (
+ DEFAULT_CA_BUNDLE_PATH,
+ extract_zipped_paths,
+ get_auth_from_url,
+ get_encoding_from_headers,
+ prepend_scheme_if_needed,
+ select_proxy,
+ urldefragauth,
+)
+
try:
from urllib3.contrib.socks import SOCKSProxyManager
except ImportError:
+
+ def SOCKSProxyManager(*args, **kwargs):
+ raise InvalidSchema("Missing dependencies for SOCKS support.")
+
+
if typing.TYPE_CHECKING:
from .models import PreparedRequest
+
+
DEFAULT_POOLBLOCK = False
DEFAULT_POOLSIZE = 10
DEFAULT_RETRIES = 0
DEFAULT_POOL_TIMEOUT = None
+
+
try:
- import ssl
+ import ssl # noqa: F401
+
_preloaded_ssl_context = create_urllib3_context()
- _preloaded_ssl_context.load_verify_locations(extract_zipped_paths(
- DEFAULT_CA_BUNDLE_PATH))
+ _preloaded_ssl_context.load_verify_locations(
+ extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
+ )
except ImportError:
+ # Bypass default SSLContext creation when Python
+ # interpreter isn't built with the ssl module.
_preloaded_ssl_context = None
+def _urllib3_request_context(
+ request: "PreparedRequest",
+ verify: "bool | str | None",
+ client_cert: "typing.Tuple[str, str] | str | None",
+ poolmanager: "PoolManager",
+) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])":
+ host_params = {}
+ pool_kwargs = {}
+ parsed_request_url = urlparse(request.url)
+ scheme = parsed_request_url.scheme.lower()
+ port = parsed_request_url.port
+
+ # Determine if we have and should use our default SSLContext
+ # to optimize performance on standard requests.
+ poolmanager_kwargs = getattr(poolmanager, "connection_pool_kw", {})
+ has_poolmanager_ssl_context = poolmanager_kwargs.get("ssl_context")
+ should_use_default_ssl_context = (
+ _preloaded_ssl_context is not None and not has_poolmanager_ssl_context
+ )
+
+ cert_reqs = "CERT_REQUIRED"
+ if verify is False:
+ cert_reqs = "CERT_NONE"
+ elif verify is True and should_use_default_ssl_context:
+ pool_kwargs["ssl_context"] = _preloaded_ssl_context
+ elif isinstance(verify, str):
+ if not os.path.isdir(verify):
+ pool_kwargs["ca_certs"] = verify
+ else:
+ pool_kwargs["ca_cert_dir"] = verify
+ pool_kwargs["cert_reqs"] = cert_reqs
+ if client_cert is not None:
+ if isinstance(client_cert, tuple) and len(client_cert) == 2:
+ pool_kwargs["cert_file"] = client_cert[0]
+ pool_kwargs["key_file"] = client_cert[1]
+ else:
+ # According to our docs, we allow users to specify just the client
+ # cert path
+ pool_kwargs["cert_file"] = client_cert
+ host_params = {
+ "scheme": scheme,
+ "host": parsed_request_url.hostname,
+ "port": port,
+ }
+ return host_params, pool_kwargs
+
+
class BaseAdapter:
"""The Base Transport Adapter"""
def __init__(self):
super().__init__()
- def send(self, request, stream=False, timeout=None, verify=True, cert=
- None, proxies=None):
+ def send(
+ self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
+ ):
"""Sends PreparedRequest object. Returns Response object.
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
@@ -68,11 +157,11 @@ class BaseAdapter:
:param cert: (optional) Any user-provided SSL certificate to be trusted.
:param proxies: (optional) The proxies dictionary to apply to the request.
"""
- pass
+ raise NotImplementedError
def close(self):
"""Cleans up adapter specific items."""
- pass
+ raise NotImplementedError
class HTTPAdapter(BaseAdapter):
@@ -101,37 +190,56 @@ class HTTPAdapter(BaseAdapter):
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
>>> s.mount('http://', a)
"""
- __attrs__ = ['max_retries', 'config', '_pool_connections',
- '_pool_maxsize', '_pool_block']
- def __init__(self, pool_connections=DEFAULT_POOLSIZE, pool_maxsize=
- DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES, pool_block=
- DEFAULT_POOLBLOCK):
+ __attrs__ = [
+ "max_retries",
+ "config",
+ "_pool_connections",
+ "_pool_maxsize",
+ "_pool_block",
+ ]
+
+ def __init__(
+ self,
+ pool_connections=DEFAULT_POOLSIZE,
+ pool_maxsize=DEFAULT_POOLSIZE,
+ max_retries=DEFAULT_RETRIES,
+ pool_block=DEFAULT_POOLBLOCK,
+ ):
if max_retries == DEFAULT_RETRIES:
self.max_retries = Retry(0, read=False)
else:
self.max_retries = Retry.from_int(max_retries)
self.config = {}
self.proxy_manager = {}
+
super().__init__()
+
self._pool_connections = pool_connections
self._pool_maxsize = pool_maxsize
self._pool_block = pool_block
+
self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
def __getstate__(self):
return {attr: getattr(self, attr, None) for attr in self.__attrs__}
def __setstate__(self, state):
+ # Can't handle by adding 'proxy_manager' to self.__attrs__ because
+ # self.poolmanager uses a lambda function, which isn't pickleable.
self.proxy_manager = {}
self.config = {}
+
for attr, value in state.items():
setattr(self, attr, value)
- self.init_poolmanager(self._pool_connections, self._pool_maxsize,
- block=self._pool_block)
- def init_poolmanager(self, connections, maxsize, block=
- DEFAULT_POOLBLOCK, **pool_kwargs):
+ self.init_poolmanager(
+ self._pool_connections, self._pool_maxsize, block=self._pool_block
+ )
+
+ def init_poolmanager(
+ self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs
+ ):
"""Initializes a urllib3 PoolManager.
This method should not be called from user code, and is only
@@ -143,7 +251,17 @@ class HTTPAdapter(BaseAdapter):
:param block: Block when no free connections are available.
:param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.
"""
- pass
+ # save these values for pickling
+ self._pool_connections = connections
+ self._pool_maxsize = maxsize
+ self._pool_block = block
+
+ self.poolmanager = PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ **pool_kwargs,
+ )
def proxy_manager_for(self, proxy, **proxy_kwargs):
"""Return urllib3 ProxyManager for the given proxy.
@@ -157,7 +275,31 @@ class HTTPAdapter(BaseAdapter):
:returns: ProxyManager
:rtype: urllib3.ProxyManager
"""
- pass
+ if proxy in self.proxy_manager:
+ manager = self.proxy_manager[proxy]
+ elif proxy.lower().startswith("socks"):
+ username, password = get_auth_from_url(proxy)
+ manager = self.proxy_manager[proxy] = SOCKSProxyManager(
+ proxy,
+ username=username,
+ password=password,
+ num_pools=self._pool_connections,
+ maxsize=self._pool_maxsize,
+ block=self._pool_block,
+ **proxy_kwargs,
+ )
+ else:
+ proxy_headers = self.proxy_headers(proxy)
+ manager = self.proxy_manager[proxy] = proxy_from_url(
+ proxy,
+ proxy_headers=proxy_headers,
+ num_pools=self._pool_connections,
+ maxsize=self._pool_maxsize,
+ block=self._pool_block,
+ **proxy_kwargs,
+ )
+
+ return manager
def cert_verify(self, conn, url, verify, cert):
"""Verify a SSL certificate. This method should not be called from user
@@ -171,7 +313,48 @@ class HTTPAdapter(BaseAdapter):
to a CA bundle to use
:param cert: The SSL certificate to verify.
"""
- pass
+ if url.lower().startswith("https") and verify:
+ conn.cert_reqs = "CERT_REQUIRED"
+
+ # Only load the CA certificates if 'verify' is a string indicating the CA bundle to use.
+ # Otherwise, if verify is a boolean, we don't load anything since
+ # the connection will be using a context with the default certificates already loaded,
+ # and this avoids a call to the slow load_verify_locations()
+ if verify is not True:
+ # `verify` must be a str with a path then
+ cert_loc = verify
+
+ if not os.path.exists(cert_loc):
+ raise OSError(
+ f"Could not find a suitable TLS CA certificate bundle, "
+ f"invalid path: {cert_loc}"
+ )
+
+ if not os.path.isdir(cert_loc):
+ conn.ca_certs = cert_loc
+ else:
+ conn.ca_cert_dir = cert_loc
+ else:
+ conn.cert_reqs = "CERT_NONE"
+ conn.ca_certs = None
+ conn.ca_cert_dir = None
+
+ if cert:
+ if not isinstance(cert, basestring):
+ conn.cert_file = cert[0]
+ conn.key_file = cert[1]
+ else:
+ conn.cert_file = cert
+ conn.key_file = None
+ if conn.cert_file and not os.path.exists(conn.cert_file):
+ raise OSError(
+ f"Could not find the TLS certificate file, "
+ f"invalid path: {conn.cert_file}"
+ )
+ if conn.key_file and not os.path.exists(conn.key_file):
+ raise OSError(
+ f"Could not find the TLS key file, invalid path: {conn.key_file}"
+ )
def build_response(self, req, resp):
"""Builds a :class:`Response <requests.Response>` object from a urllib3
@@ -183,7 +366,32 @@ class HTTPAdapter(BaseAdapter):
:param resp: The urllib3 response object.
:rtype: requests.Response
"""
- pass
+ response = Response()
+
+ # Fallback to None if there's no status_code, for whatever reason.
+ response.status_code = getattr(resp, "status", None)
+
+ # Make headers case-insensitive.
+ response.headers = CaseInsensitiveDict(getattr(resp, "headers", {}))
+
+ # Set encoding.
+ response.encoding = get_encoding_from_headers(response.headers)
+ response.raw = resp
+ response.reason = response.raw.reason
+
+ if isinstance(req.url, bytes):
+ response.url = req.url.decode("utf-8")
+ else:
+ response.url = req.url
+
+ # Add new cookies from the server.
+ extract_cookies_to_jar(response.cookies, req, resp)
+
+ # Give the Response some context.
+ response.request = req
+ response.connection = self
+
+ return response
def build_connection_pool_key_attributes(self, request, verify, cert=None):
"""Build the PoolKey attributes used by urllib3 to return a connection.
@@ -233,10 +441,9 @@ class HTTPAdapter(BaseAdapter):
portion of the Pool Key including scheme, hostname, and port. The
second is a dictionary of SSLContext related parameters.
"""
- pass
+ return _urllib3_request_context(request, verify, cert, self.poolmanager)
- def get_connection_with_tls_context(self, request, verify, proxies=None,
- cert=None):
+ def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
"""Returns a urllib3 connection for the given request and TLS settings.
This should not be called from user code, and is only exposed for use
when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
@@ -256,7 +463,34 @@ class HTTPAdapter(BaseAdapter):
:rtype:
urllib3.ConnectionPool
"""
- pass
+ proxy = select_proxy(request.url, proxies)
+ try:
+ host_params, pool_kwargs = self.build_connection_pool_key_attributes(
+ request,
+ verify,
+ cert,
+ )
+ except ValueError as e:
+ raise InvalidURL(e, request=request)
+ if proxy:
+ proxy = prepend_scheme_if_needed(proxy, "http")
+ proxy_url = parse_url(proxy)
+ if not proxy_url.host:
+ raise InvalidProxyURL(
+ "Please check proxy URL. It is malformed "
+ "and could be missing the host."
+ )
+ proxy_manager = self.proxy_manager_for(proxy)
+ conn = proxy_manager.connection_from_host(
+ **host_params, pool_kwargs=pool_kwargs
+ )
+ else:
+ # Only scheme should be lower case
+ conn = self.poolmanager.connection_from_host(
+ **host_params, pool_kwargs=pool_kwargs
+ )
+
+ return conn
def get_connection(self, url, proxies=None):
"""DEPRECATED: Users should move to `get_connection_with_tls_context`
@@ -270,7 +504,34 @@ class HTTPAdapter(BaseAdapter):
:param proxies: (optional) A Requests-style dictionary of proxies used on this request.
:rtype: urllib3.ConnectionPool
"""
- pass
+ warnings.warn(
+ (
+ "`get_connection` has been deprecated in favor of "
+ "`get_connection_with_tls_context`. Custom HTTPAdapter subclasses "
+ "will need to migrate for Requests>=2.32.2. Please see "
+ "https://github.com/psf/requests/pull/6710 for more details."
+ ),
+ DeprecationWarning,
+ )
+ proxy = select_proxy(url, proxies)
+
+ if proxy:
+ proxy = prepend_scheme_if_needed(proxy, "http")
+ proxy_url = parse_url(proxy)
+ if not proxy_url.host:
+ raise InvalidProxyURL(
+ "Please check proxy URL. It is malformed "
+ "and could be missing the host."
+ )
+ proxy_manager = self.proxy_manager_for(proxy)
+ conn = proxy_manager.connection_from_url(url)
+ else:
+ # Only scheme should be lower case
+ parsed = urlparse(url)
+ url = parsed.geturl()
+ conn = self.poolmanager.connection_from_url(url)
+
+ return conn
def close(self):
"""Disposes of any internal state.
@@ -278,7 +539,9 @@ class HTTPAdapter(BaseAdapter):
Currently, this closes the PoolManager and any active ProxyManager,
which closes any pooled connections.
"""
- pass
+ self.poolmanager.clear()
+ for proxy in self.proxy_manager.values():
+ proxy.clear()
def request_url(self, request, proxies):
"""Obtain the url to use when making the final request.
@@ -294,7 +557,23 @@ class HTTPAdapter(BaseAdapter):
:param proxies: A dictionary of schemes or schemes and hosts to proxy URLs.
:rtype: str
"""
- pass
+ proxy = select_proxy(request.url, proxies)
+ scheme = urlparse(request.url).scheme
+
+ is_proxied_http_request = proxy and scheme != "https"
+ using_socks_proxy = False
+ if proxy:
+ proxy_scheme = urlparse(proxy).scheme.lower()
+ using_socks_proxy = proxy_scheme.startswith("socks")
+
+ url = request.path_url
+ if url.startswith("//"): # Don't confuse urllib3
+ url = f"/{url.lstrip('/')}"
+
+ if is_proxied_http_request and not using_socks_proxy:
+ url = urldefragauth(request.url)
+
+ return url
def add_headers(self, request, **kwargs):
"""Add any headers needed by the connection. As of v2.0 this does
@@ -323,10 +602,17 @@ class HTTPAdapter(BaseAdapter):
:param proxy: The url of the proxy being used for this request.
:rtype: dict
"""
- pass
+ headers = {}
+ username, password = get_auth_from_url(proxy)
+
+ if username:
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
+
+ return headers
- def send(self, request, stream=False, timeout=None, verify=True, cert=
- None, proxies=None):
+ def send(
+ self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
+ ):
"""Sends PreparedRequest object. Returns Response object.
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
@@ -342,4 +628,92 @@ class HTTPAdapter(BaseAdapter):
:param proxies: (optional) The proxies dictionary to apply to the request.
:rtype: requests.Response
"""
- pass
+
+ try:
+ conn = self.get_connection_with_tls_context(
+ request, verify, proxies=proxies, cert=cert
+ )
+ except LocationValueError as e:
+ raise InvalidURL(e, request=request)
+
+ self.cert_verify(conn, request.url, verify, cert)
+ url = self.request_url(request, proxies)
+ self.add_headers(
+ request,
+ stream=stream,
+ timeout=timeout,
+ verify=verify,
+ cert=cert,
+ proxies=proxies,
+ )
+
+ chunked = not (request.body is None or "Content-Length" in request.headers)
+
+ if isinstance(timeout, tuple):
+ try:
+ connect, read = timeout
+ timeout = TimeoutSauce(connect=connect, read=read)
+ except ValueError:
+ raise ValueError(
+ f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
+ f"or a single float to set both timeouts to the same value."
+ )
+ elif isinstance(timeout, TimeoutSauce):
+ pass
+ else:
+ timeout = TimeoutSauce(connect=timeout, read=timeout)
+
+ try:
+ resp = conn.urlopen(
+ method=request.method,
+ url=url,
+ body=request.body,
+ headers=request.headers,
+ redirect=False,
+ assert_same_host=False,
+ preload_content=False,
+ decode_content=False,
+ retries=self.max_retries,
+ timeout=timeout,
+ chunked=chunked,
+ )
+
+ except (ProtocolError, OSError) as err:
+ raise ConnectionError(err, request=request)
+
+ except MaxRetryError as e:
+ if isinstance(e.reason, ConnectTimeoutError):
+ # TODO: Remove this in 3.0.0: see #2811
+ if not isinstance(e.reason, NewConnectionError):
+ raise ConnectTimeout(e, request=request)
+
+ if isinstance(e.reason, ResponseError):
+ raise RetryError(e, request=request)
+
+ if isinstance(e.reason, _ProxyError):
+ raise ProxyError(e, request=request)
+
+ if isinstance(e.reason, _SSLError):
+ # This branch is for urllib3 v1.22 and later.
+ raise SSLError(e, request=request)
+
+ raise ConnectionError(e, request=request)
+
+ except ClosedPoolError as e:
+ raise ConnectionError(e, request=request)
+
+ except _ProxyError as e:
+ raise ProxyError(e)
+
+ except (_SSLError, _HTTPError) as e:
+ if isinstance(e, _SSLError):
+ # This branch is for urllib3 versions earlier than v1.22
+ raise SSLError(e, request=request)
+ elif isinstance(e, ReadTimeoutError):
+ raise ReadTimeout(e, request=request)
+ elif isinstance(e, _InvalidHeader):
+ raise InvalidHeader(e, request=request)
+ else:
+ raise
+
+ return self.build_response(request, resp)
diff --git a/src/requests/api.py b/src/requests/api.py
index 73f1b784..59607445 100644
--- a/src/requests/api.py
+++ b/src/requests/api.py
@@ -7,6 +7,7 @@ This module implements the Requests API.
:copyright: (c) 2012 by Kenneth Reitz.
:license: Apache2, see LICENSE for more details.
"""
+
from . import sessions
@@ -50,94 +51,107 @@ def request(method, url, **kwargs):
>>> req
<Response [200]>
"""
- pass
+
+ # By using the 'with' statement we are sure the session is closed, thus we
+ # avoid leaving sockets open which can trigger a ResourceWarning in some
+ # cases, and look like a memory leak in others.
+ with sessions.Session() as session:
+ return session.request(method=method, url=url, **kwargs)
def get(url, params=None, **kwargs):
- """Sends a GET request.
+ r"""Sends a GET request.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary, list of tuples or bytes to send
in the query string for the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("get", url, params=params, **kwargs)
def options(url, **kwargs):
- """Sends an OPTIONS request.
+ r"""Sends an OPTIONS request.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("options", url, **kwargs)
def head(url, **kwargs):
- """Sends a HEAD request.
+ r"""Sends a HEAD request.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes. If
+ :param \*\*kwargs: Optional arguments that ``request`` takes. If
`allow_redirects` is not provided, it will be set to `False` (as
opposed to the default :meth:`request` behavior).
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ kwargs.setdefault("allow_redirects", False)
+ return request("head", url, **kwargs)
def post(url, data=None, json=None, **kwargs):
- """Sends a POST request.
+ r"""Sends a POST request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("post", url, data=data, json=json, **kwargs)
def put(url, data=None, **kwargs):
- """Sends a PUT request.
+ r"""Sends a PUT request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("put", url, data=data, **kwargs)
def patch(url, data=None, **kwargs):
- """Sends a PATCH request.
+ r"""Sends a PATCH request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("patch", url, data=data, **kwargs)
def delete(url, **kwargs):
- """Sends a DELETE request.
+ r"""Sends a DELETE request.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
- pass
+
+ return request("delete", url, **kwargs)
diff --git a/src/requests/auth.py b/src/requests/auth.py
index f08aecf6..4a7ce6dc 100644
--- a/src/requests/auth.py
+++ b/src/requests/auth.py
@@ -4,6 +4,7 @@ requests.auth
This module contains the authentication handlers for Requests.
"""
+
import hashlib
import os
import re
@@ -11,24 +12,65 @@ import threading
import time
import warnings
from base64 import b64encode
+
from ._internal_utils import to_native_string
from .compat import basestring, str, urlparse
from .cookies import extract_cookies_to_jar
from .utils import parse_dict_header
-CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
-CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
+CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
+CONTENT_TYPE_MULTI_PART = "multipart/form-data"
def _basic_auth_str(username, password):
"""Returns a Basic Auth string."""
- pass
+
+ # "I want us to put a big-ol' comment on top of it that
+ # says that this behaviour is dumb but we need to preserve
+ # it because people are relying on it."
+ # - Lukasa
+ #
+ # These are here solely to maintain backwards compatibility
+ # for things like ints. This will be removed in 3.0.0.
+ if not isinstance(username, basestring):
+ warnings.warn(
+ "Non-string usernames will no longer be supported in Requests "
+ "3.0.0. Please convert the object you've passed in ({!r}) to "
+ "a string or bytes object in the near future to avoid "
+ "problems.".format(username),
+ category=DeprecationWarning,
+ )
+ username = str(username)
+
+ if not isinstance(password, basestring):
+ warnings.warn(
+ "Non-string passwords will no longer be supported in Requests "
+ "3.0.0. Please convert the object you've passed in ({!r}) to "
+ "a string or bytes object in the near future to avoid "
+ "problems.".format(type(password)),
+ category=DeprecationWarning,
+ )
+ password = str(password)
+ # -- End Removal --
+
+ if isinstance(username, str):
+ username = username.encode("latin1")
+
+ if isinstance(password, str):
+ password = password.encode("latin1")
+
+ authstr = "Basic " + to_native_string(
+ b64encode(b":".join((username, password))).strip()
+ )
+
+ return authstr
class AuthBase:
"""Base class that all auth implementations derive from"""
def __call__(self, r):
- raise NotImplementedError('Auth hooks must be callable.')
+ raise NotImplementedError("Auth hooks must be callable.")
class HTTPBasicAuth(AuthBase):
@@ -39,15 +81,18 @@ class HTTPBasicAuth(AuthBase):
self.password = password
def __eq__(self, other):
- return all([self.username == getattr(other, 'username', None), self
- .password == getattr(other, 'password', None)])
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
def __ne__(self, other):
return not self == other
def __call__(self, r):
- r.headers['Authorization'] = _basic_auth_str(self.username, self.
- password)
+ r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
return r
@@ -55,8 +100,7 @@ class HTTPProxyAuth(HTTPBasicAuth):
"""Attaches HTTP Proxy Authentication to a given Request object."""
def __call__(self, r):
- r.headers['Proxy-Authorization'] = _basic_auth_str(self.username,
- self.password)
+ r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
return r
@@ -66,17 +110,133 @@ class HTTPDigestAuth(AuthBase):
def __init__(self, username, password):
self.username = username
self.password = password
+ # Keep state in per-thread local storage
self._thread_local = threading.local()
+ def init_per_thread_state(self):
+ # Ensure state is initialized just once per-thread
+ if not hasattr(self._thread_local, "init"):
+ self._thread_local.init = True
+ self._thread_local.last_nonce = ""
+ self._thread_local.nonce_count = 0
+ self._thread_local.chal = {}
+ self._thread_local.pos = None
+ self._thread_local.num_401_calls = None
+
def build_digest_header(self, method, url):
"""
:rtype: str
"""
- pass
+
+ realm = self._thread_local.chal["realm"]
+ nonce = self._thread_local.chal["nonce"]
+ qop = self._thread_local.chal.get("qop")
+ algorithm = self._thread_local.chal.get("algorithm")
+ opaque = self._thread_local.chal.get("opaque")
+ hash_utf8 = None
+
+ if algorithm is None:
+ _algorithm = "MD5"
+ else:
+ _algorithm = algorithm.upper()
+ # lambdas assume digest modules are imported at the top level
+ if _algorithm == "MD5" or _algorithm == "MD5-SESS":
+
+ def md5_utf8(x):
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.md5(x).hexdigest()
+
+ hash_utf8 = md5_utf8
+ elif _algorithm == "SHA":
+
+ def sha_utf8(x):
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha1(x).hexdigest()
+
+ hash_utf8 = sha_utf8
+ elif _algorithm == "SHA-256":
+
+ def sha256_utf8(x):
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha256(x).hexdigest()
+
+ hash_utf8 = sha256_utf8
+ elif _algorithm == "SHA-512":
+
+ def sha512_utf8(x):
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha512(x).hexdigest()
+
+ hash_utf8 = sha512_utf8
+
+ KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731
+
+ if hash_utf8 is None:
+ return None
+
+ # XXX not implemented yet
+ entdig = None
+ p_parsed = urlparse(url)
+ #: path is request-uri defined in RFC 2616 which should not be empty
+ path = p_parsed.path or "/"
+ if p_parsed.query:
+ path += f"?{p_parsed.query}"
+
+ A1 = f"{self.username}:{realm}:{self.password}"
+ A2 = f"{method}:{path}"
+
+ HA1 = hash_utf8(A1)
+ HA2 = hash_utf8(A2)
+
+ if nonce == self._thread_local.last_nonce:
+ self._thread_local.nonce_count += 1
+ else:
+ self._thread_local.nonce_count = 1
+ ncvalue = f"{self._thread_local.nonce_count:08x}"
+ s = str(self._thread_local.nonce_count).encode("utf-8")
+ s += nonce.encode("utf-8")
+ s += time.ctime().encode("utf-8")
+ s += os.urandom(8)
+
+ cnonce = hashlib.sha1(s).hexdigest()[:16]
+ if _algorithm == "MD5-SESS":
+ HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}")
+
+ if not qop:
+ respdig = KD(HA1, f"{nonce}:{HA2}")
+ elif qop == "auth" or "auth" in qop.split(","):
+ noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}"
+ respdig = KD(HA1, noncebit)
+ else:
+ # XXX handle auth-int.
+ return None
+
+ self._thread_local.last_nonce = nonce
+
+ # XXX should the partial digests be encoded too?
+ base = (
+ f'username="{self.username}", realm="{realm}", nonce="{nonce}", '
+ f'uri="{path}", response="{respdig}"'
+ )
+ if opaque:
+ base += f', opaque="{opaque}"'
+ if algorithm:
+ base += f', algorithm="{algorithm}"'
+ if entdig:
+ base += f', digest="{entdig}"'
+ if qop:
+ base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
+
+ return f"Digest {base}"
def handle_redirect(self, r, **kwargs):
"""Reset num_401_calls counter on redirects."""
- pass
+ if r.is_redirect:
+ self._thread_local.num_401_calls = 1
def handle_401(self, r, **kwargs):
"""
@@ -84,25 +244,71 @@ class HTTPDigestAuth(AuthBase):
:rtype: requests.Response
"""
- pass
+
+ # If response is not 4xx, do not auth
+ # See https://github.com/psf/requests/issues/3772
+ if not 400 <= r.status_code < 500:
+ self._thread_local.num_401_calls = 1
+ return r
+
+ if self._thread_local.pos is not None:
+ # Rewind the file position indicator of the body to where
+ # it was to resend the request.
+ r.request.body.seek(self._thread_local.pos)
+ s_auth = r.headers.get("www-authenticate", "")
+
+ if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
+ self._thread_local.num_401_calls += 1
+ pat = re.compile(r"digest ", flags=re.IGNORECASE)
+ self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
+
+ # Consume content and release the original connection
+ # to allow our new request to reuse the same one.
+ r.content
+ r.close()
+ prep = r.request.copy()
+ extract_cookies_to_jar(prep._cookies, r.request, r.raw)
+ prep.prepare_cookies(prep._cookies)
+
+ prep.headers["Authorization"] = self.build_digest_header(
+ prep.method, prep.url
+ )
+ _r = r.connection.send(prep, **kwargs)
+ _r.history.append(r)
+ _r.request = prep
+
+ return _r
+
+ self._thread_local.num_401_calls = 1
+ return r
def __call__(self, r):
+ # Initialize per-thread state, if needed
self.init_per_thread_state()
+ # If we have a saved nonce, skip the 401
if self._thread_local.last_nonce:
- r.headers['Authorization'] = self.build_digest_header(r.method,
- r.url)
+ r.headers["Authorization"] = self.build_digest_header(r.method, r.url)
try:
self._thread_local.pos = r.body.tell()
except AttributeError:
+ # In the case of HTTPDigestAuth being reused and the body of
+ # the previous request was a file-like object, pos has the
+ # file position of the previous body. Ensure it's set to
+ # None.
self._thread_local.pos = None
- r.register_hook('response', self.handle_401)
- r.register_hook('response', self.handle_redirect)
+ r.register_hook("response", self.handle_401)
+ r.register_hook("response", self.handle_redirect)
self._thread_local.num_401_calls = 1
+
return r
def __eq__(self, other):
- return all([self.username == getattr(other, 'username', None), self
- .password == getattr(other, 'password', None)])
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
def __ne__(self, other):
return not self == other
diff --git a/src/requests/certs.py b/src/requests/certs.py
index e6ec0c8d..be422c3e 100644
--- a/src/requests/certs.py
+++ b/src/requests/certs.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
"""
requests.certs
~~~~~~~~~~~~~~
@@ -10,5 +12,6 @@ environment, you can change the definition of where() to return a separately
packaged CA bundle.
"""
from certifi import where
-if __name__ == '__main__':
+
+if __name__ == "__main__":
print(where())
diff --git a/src/requests/compat.py b/src/requests/compat.py
index d4d04060..095de1b6 100644
--- a/src/requests/compat.py
+++ b/src/requests/compat.py
@@ -6,39 +6,89 @@ This module previously handled import compatibility issues
between Python 2 and Python 3. It remains for backwards
compatibility until the next major version.
"""
+
import importlib
import sys
+# -------------------
+# Character Detection
+# -------------------
+
def _resolve_char_detection():
"""Find supported character detection libraries."""
- pass
+ chardet = None
+ for lib in ("chardet", "charset_normalizer"):
+ if chardet is None:
+ try:
+ chardet = importlib.import_module(lib)
+ except ImportError:
+ pass
+ return chardet
chardet = _resolve_char_detection()
+
+# -------
+# Pythons
+# -------
+
+# Syntax sugar.
_ver = sys.version_info
+
+#: Python 2.x?
is_py2 = _ver[0] == 2
+
+#: Python 3.x?
is_py3 = _ver[0] == 3
+
+# json/simplejson module import resolution
has_simplejson = False
try:
import simplejson as json
+
has_simplejson = True
except ImportError:
import json
+
if has_simplejson:
from simplejson import JSONDecodeError
else:
from json import JSONDecodeError
+
+# Keep OrderedDict for backwards compatibility.
from collections import OrderedDict
from collections.abc import Callable, Mapping, MutableMapping
from http import cookiejar as cookielib
from http.cookies import Morsel
from io import StringIO
-from urllib.parse import quote, quote_plus, unquote, unquote_plus, urldefrag, urlencode, urljoin, urlparse, urlsplit, urlunparse
-from urllib.request import getproxies, getproxies_environment, parse_http_list, proxy_bypass, proxy_bypass_environment
+
+# --------------
+# Legacy Imports
+# --------------
+from urllib.parse import (
+ quote,
+ quote_plus,
+ unquote,
+ unquote_plus,
+ urldefrag,
+ urlencode,
+ urljoin,
+ urlparse,
+ urlsplit,
+ urlunparse,
+)
+from urllib.request import (
+ getproxies,
+ getproxies_environment,
+ parse_http_list,
+ proxy_bypass,
+ proxy_bypass_environment,
+)
+
builtin_str = str
str = str
bytes = bytes
-basestring = str, bytes
-numeric_types = int, float
-integer_types = int,
+basestring = (str, bytes)
+numeric_types = (int, float)
+integer_types = (int,)
diff --git a/src/requests/cookies.py b/src/requests/cookies.py
index 6392b7bc..f69d0cda 100644
--- a/src/requests/cookies.py
+++ b/src/requests/cookies.py
@@ -6,11 +6,14 @@ Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.
requests.utils imports from here, so be careful with imports.
"""
+
import calendar
import copy
import time
+
from ._internal_utils import to_native_string
from .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse
+
try:
import threading
except ImportError:
@@ -34,9 +37,67 @@ class MockRequest:
self._new_headers = {}
self.type = urlparse(self._r.url).scheme
+ def get_type(self):
+ return self.type
+
+ def get_host(self):
+ return urlparse(self._r.url).netloc
+
+ def get_origin_req_host(self):
+ return self.get_host()
+
+ def get_full_url(self):
+ # Only return the response's URL if the user hadn't set the Host
+ # header
+ if not self._r.headers.get("Host"):
+ return self._r.url
+ # If they did set it, retrieve it and reconstruct the expected domain
+ host = to_native_string(self._r.headers["Host"], encoding="utf-8")
+ parsed = urlparse(self._r.url)
+ # Reconstruct the URL as we expect it
+ return urlunparse(
+ [
+ parsed.scheme,
+ host,
+ parsed.path,
+ parsed.params,
+ parsed.query,
+ parsed.fragment,
+ ]
+ )
+
+ def is_unverifiable(self):
+ return True
+
+ def has_header(self, name):
+ return name in self._r.headers or name in self._new_headers
+
+ def get_header(self, name, default=None):
+ return self._r.headers.get(name, self._new_headers.get(name, default))
+
def add_header(self, key, val):
"""cookiejar has no legitimate use for this method; add it back if you find one."""
- pass
+ raise NotImplementedError(
+ "Cookie headers should be added with add_unredirected_header()"
+ )
+
+ def add_unredirected_header(self, name, value):
+ self._new_headers[name] = value
+
+ def get_new_headers(self):
+ return self._new_headers
+
+ @property
+ def unverifiable(self):
+ return self.is_unverifiable()
+
+ @property
+ def origin_req_host(self):
+ return self.get_origin_req_host()
+
+ @property
+ def host(self):
+ return self.get_host()
class MockResponse:
@@ -53,6 +114,12 @@ class MockResponse:
"""
self._headers = headers
+ def info(self):
+ return self._headers
+
+ def getheaders(self, name):
+ self._headers.getheaders(name)
+
def extract_cookies_to_jar(jar, request, response):
"""Extract the cookies from the response into a CookieJar.
@@ -61,7 +128,13 @@ def extract_cookies_to_jar(jar, request, response):
:param request: our own requests.Request object
:param response: urllib3.HTTPResponse object
"""
- pass
+ if not (hasattr(response, "_original_response") and response._original_response):
+ return
+ # the _original_response field is the wrapped httplib.HTTPResponse object,
+ req = MockRequest(request)
+ # pull out the HTTPMessage with the headers and put it in the mock:
+ res = MockResponse(response._original_response.msg)
+ jar.extract_cookies(res, req)
def get_cookie_header(jar, request):
@@ -70,7 +143,9 @@ def get_cookie_header(jar, request):
:rtype: str
"""
- pass
+ r = MockRequest(request)
+ jar.add_cookie_header(r)
+ return r.get_new_headers().get("Cookie")
def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
@@ -78,7 +153,18 @@ def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
Wraps CookieJar.clear(), is O(n).
"""
- pass
+ clearables = []
+ for cookie in cookiejar:
+ if cookie.name != name:
+ continue
+ if domain is not None and domain != cookie.domain:
+ continue
+ if path is not None and path != cookie.path:
+ continue
+ clearables.append((cookie.domain, cookie.path, cookie.name))
+
+ for domain, path, name in clearables:
+ cookiejar.clear(domain, path, name)
class CookieConflictError(RuntimeError):
@@ -112,14 +198,29 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. warning:: operation is O(n), not O(1).
"""
- pass
+ try:
+ return self._find_no_duplicates(name, domain, path)
+ except KeyError:
+ return default
def set(self, name, value, **kwargs):
"""Dict-like set() that also supports optional domain and path args in
order to resolve naming collisions from using one cookie jar over
multiple domains.
"""
- pass
+ # support client code that unsets cookies by assignment of a None value:
+ if value is None:
+ remove_cookie_by_name(
+ self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
+ )
+ return
+
+ if isinstance(value, Morsel):
+ c = morsel_to_cookie(value)
+ else:
+ c = create_cookie(name, value, **kwargs)
+ self.set_cookie(c)
+ return c
def iterkeys(self):
"""Dict-like iterkeys() that returns an iterator of names of cookies
@@ -127,7 +228,8 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: itervalues() and iteritems().
"""
- pass
+ for cookie in iter(self):
+ yield cookie.name
def keys(self):
"""Dict-like keys() that returns a list of names of cookies from the
@@ -135,7 +237,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: values() and items().
"""
- pass
+ return list(self.iterkeys())
def itervalues(self):
"""Dict-like itervalues() that returns an iterator of values of cookies
@@ -143,7 +245,8 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: iterkeys() and iteritems().
"""
- pass
+ for cookie in iter(self):
+ yield cookie.value
def values(self):
"""Dict-like values() that returns a list of values of cookies from the
@@ -151,7 +254,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: keys() and items().
"""
- pass
+ return list(self.itervalues())
def iteritems(self):
"""Dict-like iteritems() that returns an iterator of name-value tuples
@@ -159,7 +262,8 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: iterkeys() and itervalues().
"""
- pass
+ for cookie in iter(self):
+ yield cookie.name, cookie.value
def items(self):
"""Dict-like items() that returns a list of name-value tuples from the
@@ -168,15 +272,23 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
.. seealso:: keys() and values().
"""
- pass
+ return list(self.iteritems())
def list_domains(self):
"""Utility method to list all the domains in the jar."""
- pass
+ domains = []
+ for cookie in iter(self):
+ if cookie.domain not in domains:
+ domains.append(cookie.domain)
+ return domains
def list_paths(self):
"""Utility method to list all the paths in the jar."""
- pass
+ paths = []
+ for cookie in iter(self):
+ if cookie.path not in paths:
+ paths.append(cookie.path)
+ return paths
def multiple_domains(self):
"""Returns True if there are multiple domains in the jar.
@@ -184,7 +296,12 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
:rtype: bool
"""
- pass
+ domains = []
+ for cookie in iter(self):
+ if cookie.domain is not None and cookie.domain in domains:
+ return True
+ domains.append(cookie.domain)
+ return False # there is only one domain in jar
def get_dict(self, domain=None, path=None):
"""Takes as an argument an optional domain and path and returns a plain
@@ -193,7 +310,13 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
:rtype: dict
"""
- pass
+ dictionary = {}
+ for cookie in iter(self):
+ if (domain is None or cookie.domain == domain) and (
+ path is None or cookie.path == path
+ ):
+ dictionary[cookie.name] = cookie.value
+ return dictionary
def __contains__(self, name):
try:
@@ -223,9 +346,22 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
"""
remove_cookie_by_name(self, name)
+ def set_cookie(self, cookie, *args, **kwargs):
+ if (
+ hasattr(cookie.value, "startswith")
+ and cookie.value.startswith('"')
+ and cookie.value.endswith('"')
+ ):
+ cookie.value = cookie.value.replace('\\"', "")
+ return super().set_cookie(cookie, *args, **kwargs)
+
def update(self, other):
"""Updates this jar with cookies from another CookieJar or dict-like"""
- pass
+ if isinstance(other, cookielib.CookieJar):
+ for cookie in other:
+ self.set_cookie(copy.copy(cookie))
+ else:
+ super().update(other)
def _find(self, name, domain=None, path=None):
"""Requests uses this method internally to get cookie values.
@@ -239,7 +375,13 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
:param path: (optional) string containing path of cookie
:return: cookie.value
"""
- pass
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ return cookie.value
+
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
def _find_no_duplicates(self, name, domain=None, path=None):
"""Both ``__get_item__`` and ``get`` call this function: it's never
@@ -253,27 +395,61 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
that match name and optionally domain and path
:return: cookie.value
"""
- pass
+ toReturn = None
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ if toReturn is not None:
+ # if there are multiple cookies that meet passed in criteria
+ raise CookieConflictError(
+ f"There are multiple cookies with name, {name!r}"
+ )
+ # we will eventually return this as long as no cookie conflict
+ toReturn = cookie.value
+
+ if toReturn:
+ return toReturn
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
def __getstate__(self):
"""Unlike a normal CookieJar, this class is pickleable."""
state = self.__dict__.copy()
- state.pop('_cookies_lock')
+ # remove the unpickleable RLock object
+ state.pop("_cookies_lock")
return state
def __setstate__(self, state):
"""Unlike a normal CookieJar, this class is pickleable."""
self.__dict__.update(state)
- if '_cookies_lock' not in self.__dict__:
+ if "_cookies_lock" not in self.__dict__:
self._cookies_lock = threading.RLock()
def copy(self):
"""Return a copy of this RequestsCookieJar."""
- pass
+ new_cj = RequestsCookieJar()
+ new_cj.set_policy(self.get_policy())
+ new_cj.update(self)
+ return new_cj
def get_policy(self):
"""Return the CookiePolicy instance used."""
- pass
+ return self._policy
+
+
+def _copy_cookie_jar(jar):
+ if jar is None:
+ return None
+
+ if hasattr(jar, "copy"):
+ # We're dealing with an instance of RequestsCookieJar
+ return jar.copy()
+ # We're dealing with a generic CookieJar instance
+ new_jar = copy.copy(jar)
+ new_jar.clear()
+ for cookie in jar:
+ new_jar.set_cookie(copy.copy(cookie))
+ return new_jar
def create_cookie(name, value, **kwargs):
@@ -282,12 +458,64 @@ def create_cookie(name, value, **kwargs):
By default, the pair of `name` and `value` will be set for the domain ''
and sent on every request (this is sometimes called a "supercookie").
"""
- pass
+ result = {
+ "version": 0,
+ "name": name,
+ "value": value,
+ "port": None,
+ "domain": "",
+ "path": "/",
+ "secure": False,
+ "expires": None,
+ "discard": True,
+ "comment": None,
+ "comment_url": None,
+ "rest": {"HttpOnly": None},
+ "rfc2109": False,
+ }
+
+ badargs = set(kwargs) - set(result)
+ if badargs:
+ raise TypeError(
+ f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
+ )
+
+ result.update(kwargs)
+ result["port_specified"] = bool(result["port"])
+ result["domain_specified"] = bool(result["domain"])
+ result["domain_initial_dot"] = result["domain"].startswith(".")
+ result["path_specified"] = bool(result["path"])
+
+ return cookielib.Cookie(**result)
def morsel_to_cookie(morsel):
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
- pass
+
+ expires = None
+ if morsel["max-age"]:
+ try:
+ expires = int(time.time() + int(morsel["max-age"]))
+ except ValueError:
+ raise TypeError(f"max-age: {morsel['max-age']} must be integer")
+ elif morsel["expires"]:
+ time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
+ expires = calendar.timegm(time.strptime(morsel["expires"], time_template))
+ return create_cookie(
+ comment=morsel["comment"],
+ comment_url=bool(morsel["comment"]),
+ discard=False,
+ domain=morsel["domain"],
+ expires=expires,
+ name=morsel.key,
+ path=morsel["path"],
+ port=None,
+ rest={"HttpOnly": morsel["httponly"]},
+ rfc2109=False,
+ secure=bool(morsel["secure"]),
+ value=morsel.value,
+ version=morsel["version"] or 0,
+ )
def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
@@ -299,7 +527,16 @@ def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
already in the jar with new ones.
:rtype: CookieJar
"""
- pass
+ if cookiejar is None:
+ cookiejar = RequestsCookieJar()
+
+ if cookie_dict is not None:
+ names_from_jar = [cookie.name for cookie in cookiejar]
+ for name in cookie_dict:
+ if overwrite or (name not in names_from_jar):
+ cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
+
+ return cookiejar
def merge_cookies(cookiejar, cookies):
@@ -309,4 +546,16 @@ def merge_cookies(cookiejar, cookies):
:param cookies: Dictionary or CookieJar object to be added.
:rtype: CookieJar
"""
- pass
+ if not isinstance(cookiejar, cookielib.CookieJar):
+ raise ValueError("You can only merge into CookieJar")
+
+ if isinstance(cookies, dict):
+ cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)
+ elif isinstance(cookies, cookielib.CookieJar):
+ try:
+ cookiejar.update(cookies)
+ except AttributeError:
+ for cookie_in_jar in cookies:
+ cookiejar.set_cookie(cookie_in_jar)
+
+ return cookiejar
diff --git a/src/requests/exceptions.py b/src/requests/exceptions.py
index 9480a02a..83986b48 100644
--- a/src/requests/exceptions.py
+++ b/src/requests/exceptions.py
@@ -5,6 +5,7 @@ requests.exceptions
This module contains the set of Requests' exceptions.
"""
from urllib3.exceptions import HTTPError as BaseHTTPError
+
from .compat import JSONDecodeError as CompatJSONDecodeError
@@ -15,11 +16,10 @@ class RequestException(IOError):
def __init__(self, *args, **kwargs):
"""Initialize RequestException with `request` and `response` objects."""
- response = kwargs.pop('response', None)
+ response = kwargs.pop("response", None)
self.response = response
- self.request = kwargs.pop('request', None)
- if response is not None and not self.request and hasattr(response,
- 'request'):
+ self.request = kwargs.pop("request", None)
+ if response is not None and not self.request and hasattr(response, "request"):
self.request = self.response.request
super().__init__(*args, **kwargs)
@@ -136,6 +136,9 @@ class UnrewindableBodyError(RequestException):
"""Requests encountered an error when trying to rewind a body."""
+# Warnings
+
+
class RequestsWarning(Warning):
"""Base warning for Requests."""
diff --git a/src/requests/help.py b/src/requests/help.py
index 8057ee09..8fbcd656 100644
--- a/src/requests/help.py
+++ b/src/requests/help.py
@@ -1,19 +1,25 @@
"""Module containing bug report helper(s)."""
+
import json
import platform
import ssl
import sys
+
import idna
import urllib3
+
from . import __version__ as requests_version
+
try:
import charset_normalizer
except ImportError:
charset_normalizer = None
+
try:
import chardet
except ImportError:
chardet = None
+
try:
from urllib3.contrib import pyopenssl
except ImportError:
@@ -36,18 +42,93 @@ def _implementation():
doesn't work for Jython or IronPython. Future investigation should be done
to work out the correct shape of the code for those platforms.
"""
- pass
+ implementation = platform.python_implementation()
+
+ if implementation == "CPython":
+ implementation_version = platform.python_version()
+ elif implementation == "PyPy":
+ implementation_version = "{}.{}.{}".format(
+ sys.pypy_version_info.major,
+ sys.pypy_version_info.minor,
+ sys.pypy_version_info.micro,
+ )
+ if sys.pypy_version_info.releaselevel != "final":
+ implementation_version = "".join(
+ [implementation_version, sys.pypy_version_info.releaselevel]
+ )
+ elif implementation == "Jython":
+ implementation_version = platform.python_version() # Complete Guess
+ elif implementation == "IronPython":
+ implementation_version = platform.python_version() # Complete Guess
+ else:
+ implementation_version = "Unknown"
+
+ return {"name": implementation, "version": implementation_version}
def info():
"""Generate information for a bug report."""
- pass
+ try:
+ platform_info = {
+ "system": platform.system(),
+ "release": platform.release(),
+ }
+ except OSError:
+ platform_info = {
+ "system": "Unknown",
+ "release": "Unknown",
+ }
+
+ implementation_info = _implementation()
+ urllib3_info = {"version": urllib3.__version__}
+ charset_normalizer_info = {"version": None}
+ chardet_info = {"version": None}
+ if charset_normalizer:
+ charset_normalizer_info = {"version": charset_normalizer.__version__}
+ if chardet:
+ chardet_info = {"version": chardet.__version__}
+
+ pyopenssl_info = {
+ "version": None,
+ "openssl_version": "",
+ }
+ if OpenSSL:
+ pyopenssl_info = {
+ "version": OpenSSL.__version__,
+ "openssl_version": f"{OpenSSL.SSL.OPENSSL_VERSION_NUMBER:x}",
+ }
+ cryptography_info = {
+ "version": getattr(cryptography, "__version__", ""),
+ }
+ idna_info = {
+ "version": getattr(idna, "__version__", ""),
+ }
+
+ system_ssl = ssl.OPENSSL_VERSION_NUMBER
+ system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""}
+
+ return {
+ "platform": platform_info,
+ "implementation": implementation_info,
+ "system_ssl": system_ssl_info,
+ "using_pyopenssl": pyopenssl is not None,
+ "using_charset_normalizer": chardet is None,
+ "pyOpenSSL": pyopenssl_info,
+ "urllib3": urllib3_info,
+ "chardet": chardet_info,
+ "charset_normalizer": charset_normalizer_info,
+ "cryptography": cryptography_info,
+ "idna": idna_info,
+ "requests": {
+ "version": requests_version,
+ },
+ }
def main():
"""Pretty-print the bug information as JSON."""
- pass
+ print(json.dumps(info(), sort_keys=True, indent=2))
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/src/requests/hooks.py b/src/requests/hooks.py
index 8dfca96f..d181ba2e 100644
--- a/src/requests/hooks.py
+++ b/src/requests/hooks.py
@@ -9,9 +9,25 @@ Available hooks:
``response``:
The response generated from a Request.
"""
-HOOKS = ['response']
+HOOKS = ["response"]
+
+
+def default_hooks():
+ return {event: [] for event in HOOKS}
+
+
+# TODO: response is the only one
def dispatch_hook(key, hooks, hook_data, **kwargs):
"""Dispatches a hook dictionary on a given piece of data."""
- pass
+ hooks = hooks or {}
+ hooks = hooks.get(key)
+ if hooks:
+ if hasattr(hooks, "__call__"):
+ hooks = [hooks]
+ for hook in hooks:
+ _hook_data = hook(hook_data, **kwargs)
+ if _hook_data is not None:
+ hook_data = _hook_data
+ return hook_data
diff --git a/src/requests/models.py b/src/requests/models.py
index 9006e78e..8f56ca7d 100644
--- a/src/requests/models.py
+++ b/src/requests/models.py
@@ -4,20 +4,48 @@ requests.models
This module contains the primary objects that power Requests.
"""
+
import datetime
-import encodings.idna
+
+# Import encoding now, to avoid implicit import later.
+# Implicit import within threads may cause LookupError when standard library is in a ZIP,
+# such as in Embedded Python. See https://github.com/psf/requests/issues/3578.
+import encodings.idna # noqa: F401
from io import UnsupportedOperation
-from urllib3.exceptions import DecodeError, LocationParseError, ProtocolError, ReadTimeoutError, SSLError
+
+from urllib3.exceptions import (
+ DecodeError,
+ LocationParseError,
+ ProtocolError,
+ ReadTimeoutError,
+ SSLError,
+)
from urllib3.fields import RequestField
from urllib3.filepost import encode_multipart_formdata
from urllib3.util import parse_url
+
from ._internal_utils import to_native_string, unicode_is_ascii
from .auth import HTTPBasicAuth
-from .compat import Callable, JSONDecodeError, Mapping, basestring, builtin_str, chardet, cookielib
+from .compat import (
+ Callable,
+ JSONDecodeError,
+ Mapping,
+ basestring,
+ builtin_str,
+ chardet,
+ cookielib,
+)
from .compat import json as complexjson
from .compat import urlencode, urlsplit, urlunparse
from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header
-from .exceptions import ChunkedEncodingError, ConnectionError, ContentDecodingError, HTTPError, InvalidJSONError, InvalidURL
+from .exceptions import (
+ ChunkedEncodingError,
+ ConnectionError,
+ ContentDecodingError,
+ HTTPError,
+ InvalidJSONError,
+ InvalidURL,
+)
from .exceptions import JSONDecodeError as RequestsJSONDecodeError
from .exceptions import MissingSchema
from .exceptions import SSLError as RequestsSSLError
@@ -25,20 +53,55 @@ from .exceptions import StreamConsumedError
from .hooks import default_hooks
from .status_codes import codes
from .structures import CaseInsensitiveDict
-from .utils import check_header_validity, get_auth_from_url, guess_filename, guess_json_utf, iter_slices, parse_header_links, requote_uri, stream_decode_response_unicode, super_len, to_key_val_list
-REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.
- temporary_redirect, codes.permanent_redirect)
+from .utils import (
+ check_header_validity,
+ get_auth_from_url,
+ guess_filename,
+ guess_json_utf,
+ iter_slices,
+ parse_header_links,
+ requote_uri,
+ stream_decode_response_unicode,
+ super_len,
+ to_key_val_list,
+)
+
+#: The set of HTTP status codes that indicate an automatically
+#: processable redirect.
+REDIRECT_STATI = (
+ codes.moved, # 301
+ codes.found, # 302
+ codes.other, # 303
+ codes.temporary_redirect, # 307
+ codes.permanent_redirect, # 308
+)
+
DEFAULT_REDIRECT_LIMIT = 30
CONTENT_CHUNK_SIZE = 10 * 1024
ITER_CHUNK_SIZE = 512
class RequestEncodingMixin:
-
@property
def path_url(self):
"""Build the path URL to use."""
- pass
+
+ url = []
+
+ p = urlsplit(self.url)
+
+ path = p.path
+ if not path:
+ path = "/"
+
+ url.append(path)
+
+ query = p.query
+ if query:
+ url.append("?")
+ url.append(query)
+
+ return "".join(url)
@staticmethod
def _encode_params(data):
@@ -48,7 +111,27 @@ class RequestEncodingMixin:
2-tuples. Order is retained if data is a list of 2-tuples but arbitrary
if parameters are supplied as a dict.
"""
- pass
+
+ if isinstance(data, (str, bytes)):
+ return data
+ elif hasattr(data, "read"):
+ return data
+ elif hasattr(data, "__iter__"):
+ result = []
+ for k, vs in to_key_val_list(data):
+ if isinstance(vs, basestring) or not hasattr(vs, "__iter__"):
+ vs = [vs]
+ for v in vs:
+ if v is not None:
+ result.append(
+ (
+ k.encode("utf-8") if isinstance(k, str) else k,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
+ return urlencode(result, doseq=True)
+ else:
+ return data
@staticmethod
def _encode_files(files, data):
@@ -60,20 +143,88 @@ class RequestEncodingMixin:
The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)
or 4-tuples (filename, fileobj, contentype, custom_headers).
"""
- pass
+ if not files:
+ raise ValueError("Files must be provided.")
+ elif isinstance(data, basestring):
+ raise ValueError("Data must not be a string.")
+
+ new_fields = []
+ fields = to_key_val_list(data or {})
+ files = to_key_val_list(files or {})
+
+ for field, val in fields:
+ if isinstance(val, basestring) or not hasattr(val, "__iter__"):
+ val = [val]
+ for v in val:
+ if v is not None:
+ # Don't call str() on bytestrings: in Py3 it all goes wrong.
+ if not isinstance(v, bytes):
+ v = str(v)
+
+ new_fields.append(
+ (
+ field.decode("utf-8")
+ if isinstance(field, bytes)
+ else field,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
+
+ for k, v in files:
+ # support for explicit filename
+ ft = None
+ fh = None
+ if isinstance(v, (tuple, list)):
+ if len(v) == 2:
+ fn, fp = v
+ elif len(v) == 3:
+ fn, fp, ft = v
+ else:
+ fn, fp, ft, fh = v
+ else:
+ fn = guess_filename(v) or k
+ fp = v
+
+ if isinstance(fp, (str, bytes, bytearray)):
+ fdata = fp
+ elif hasattr(fp, "read"):
+ fdata = fp.read()
+ elif fp is None:
+ continue
+ else:
+ fdata = fp
+
+ rf = RequestField(name=k, data=fdata, filename=fn, headers=fh)
+ rf.make_multipart(content_type=ft)
+ new_fields.append(rf)
+
+ body, content_type = encode_multipart_formdata(new_fields)
+
+ return body, content_type
class RequestHooksMixin:
-
def register_hook(self, event, hook):
"""Properly register a hook."""
- pass
+
+ if event not in self.hooks:
+ raise ValueError(f'Unsupported event specified, with event name "{event}"')
+
+ if isinstance(hook, Callable):
+ self.hooks[event].append(hook)
+ elif hasattr(hook, "__iter__"):
+ self.hooks[event].extend(h for h in hook if isinstance(h, Callable))
def deregister_hook(self, event, hook):
"""Deregister a previously registered hook.
Returns True if the hook existed, False if not.
"""
- pass
+
+ try:
+ self.hooks[event].remove(hook)
+ return True
+ except ValueError:
+ return False
class Request(RequestHooksMixin):
@@ -104,17 +255,30 @@ class Request(RequestHooksMixin):
<PreparedRequest [GET]>
"""
- def __init__(self, method=None, url=None, headers=None, files=None,
- data=None, params=None, auth=None, cookies=None, hooks=None, json=None
- ):
+ def __init__(
+ self,
+ method=None,
+ url=None,
+ headers=None,
+ files=None,
+ data=None,
+ params=None,
+ auth=None,
+ cookies=None,
+ hooks=None,
+ json=None,
+ ):
+ # Default empty dicts for dict params.
data = [] if data is None else data
files = [] if files is None else files
headers = {} if headers is None else headers
params = {} if params is None else params
hooks = {} if hooks is None else hooks
+
self.hooks = default_hooks()
for k, v in list(hooks.items()):
self.register_hook(event=k, hook=v)
+
self.method = method
self.url = url
self.headers = headers
@@ -126,11 +290,24 @@ class Request(RequestHooksMixin):
self.cookies = cookies
def __repr__(self):
- return f'<Request [{self.method}]>'
+ return f"<Request [{self.method}]>"
def prepare(self):
"""Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it."""
- pass
+ p = PreparedRequest()
+ p.prepare(
+ method=self.method,
+ url=self.url,
+ headers=self.headers,
+ files=self.files,
+ data=self.data,
+ json=self.json,
+ params=self.params,
+ auth=self.auth,
+ cookies=self.cookies,
+ hooks=self.hooks,
+ )
+ return p
class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
@@ -155,45 +332,280 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
"""
def __init__(self):
+ #: HTTP verb to send to the server.
self.method = None
+ #: HTTP URL to send the request to.
self.url = None
+ #: dictionary of HTTP headers.
self.headers = None
+ # The `CookieJar` used to create the Cookie header will be stored here
+ # after prepare_cookies is called
self._cookies = None
+ #: request body to send to the server.
self.body = None
+ #: dictionary of callback hooks, for internal usage.
self.hooks = default_hooks()
+ #: integer denoting starting position of a readable file-like body.
self._body_position = None
- def prepare(self, method=None, url=None, headers=None, files=None, data
- =None, params=None, auth=None, cookies=None, hooks=None, json=None):
+ def prepare(
+ self,
+ method=None,
+ url=None,
+ headers=None,
+ files=None,
+ data=None,
+ params=None,
+ auth=None,
+ cookies=None,
+ hooks=None,
+ json=None,
+ ):
"""Prepares the entire request with the given parameters."""
- pass
+
+ self.prepare_method(method)
+ self.prepare_url(url, params)
+ self.prepare_headers(headers)
+ self.prepare_cookies(cookies)
+ self.prepare_body(data, files, json)
+ self.prepare_auth(auth, url)
+
+ # Note that prepare_auth must be last to enable authentication schemes
+ # such as OAuth to work on a fully prepared request.
+
+ # This MUST go after prepare_auth. Authenticators could add a hook
+ self.prepare_hooks(hooks)
def __repr__(self):
- return f'<PreparedRequest [{self.method}]>'
+ return f"<PreparedRequest [{self.method}]>"
+
+ def copy(self):
+ p = PreparedRequest()
+ p.method = self.method
+ p.url = self.url
+ p.headers = self.headers.copy() if self.headers is not None else None
+ p._cookies = _copy_cookie_jar(self._cookies)
+ p.body = self.body
+ p.hooks = self.hooks
+ p._body_position = self._body_position
+ return p
def prepare_method(self, method):
"""Prepares the given HTTP method."""
- pass
+ self.method = method
+ if self.method is not None:
+ self.method = to_native_string(self.method.upper())
+
+ @staticmethod
+ def _get_idna_encoded_host(host):
+ import idna
+
+ try:
+ host = idna.encode(host, uts46=True).decode("utf-8")
+ except idna.IDNAError:
+ raise UnicodeError
+ return host
def prepare_url(self, url, params):
"""Prepares the given HTTP URL."""
- pass
+ #: Accept objects that have string representations.
+ #: We're unable to blindly call unicode/str functions
+ #: as this will include the bytestring indicator (b'')
+ #: on python 3.x.
+ #: https://github.com/psf/requests/pull/2238
+ if isinstance(url, bytes):
+ url = url.decode("utf8")
+ else:
+ url = str(url)
+
+ # Remove leading whitespaces from url
+ url = url.lstrip()
+
+ # Don't do any URL preparation for non-HTTP schemes like `mailto`,
+ # `data` etc to work around exceptions from `url_parse`, which
+ # handles RFC 3986 only.
+ if ":" in url and not url.lower().startswith("http"):
+ self.url = url
+ return
+
+ # Support for unicode domain names and paths.
+ try:
+ scheme, auth, host, port, path, query, fragment = parse_url(url)
+ except LocationParseError as e:
+ raise InvalidURL(*e.args)
+
+ if not scheme:
+ raise MissingSchema(
+ f"Invalid URL {url!r}: No scheme supplied. "
+ f"Perhaps you meant https://{url}?"
+ )
+
+ if not host:
+ raise InvalidURL(f"Invalid URL {url!r}: No host supplied")
+
+ # In general, we want to try IDNA encoding the hostname if the string contains
+ # non-ASCII characters. This allows users to automatically get the correct IDNA
+ # behaviour. For strings containing only ASCII characters, we need to also verify
+ # it doesn't start with a wildcard (*), before allowing the unencoded hostname.
+ if not unicode_is_ascii(host):
+ try:
+ host = self._get_idna_encoded_host(host)
+ except UnicodeError:
+ raise InvalidURL("URL has an invalid label.")
+ elif host.startswith(("*", ".")):
+ raise InvalidURL("URL has an invalid label.")
+
+ # Carefully reconstruct the network location
+ netloc = auth or ""
+ if netloc:
+ netloc += "@"
+ netloc += host
+ if port:
+ netloc += f":{port}"
+
+ # Bare domains aren't valid URLs.
+ if not path:
+ path = "/"
+
+ if isinstance(params, (str, bytes)):
+ params = to_native_string(params)
+
+ enc_params = self._encode_params(params)
+ if enc_params:
+ if query:
+ query = f"{query}&{enc_params}"
+ else:
+ query = enc_params
+
+ url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment]))
+ self.url = url
def prepare_headers(self, headers):
"""Prepares the given HTTP headers."""
- pass
+
+ self.headers = CaseInsensitiveDict()
+ if headers:
+ for header in headers.items():
+ # Raise exception on invalid header value.
+ check_header_validity(header)
+ name, value = header
+ self.headers[to_native_string(name)] = value
def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""
- pass
+
+ # Check if file, fo, generator, iterator.
+ # If not, run through normal process.
+
+ # Nottin' on you.
+ body = None
+ content_type = None
+
+ if not data and json is not None:
+ # urllib3 requires a bytes-like body. Python 2's json.dumps
+ # provides this natively, but Python 3 gives a Unicode string.
+ content_type = "application/json"
+
+ try:
+ body = complexjson.dumps(json, allow_nan=False)
+ except ValueError as ve:
+ raise InvalidJSONError(ve, request=self)
+
+ if not isinstance(body, bytes):
+ body = body.encode("utf-8")
+
+ is_stream = all(
+ [
+ hasattr(data, "__iter__"),
+ not isinstance(data, (basestring, list, tuple, Mapping)),
+ ]
+ )
+
+ if is_stream:
+ try:
+ length = super_len(data)
+ except (TypeError, AttributeError, UnsupportedOperation):
+ length = None
+
+ body = data
+
+ if getattr(body, "tell", None) is not None:
+ # Record the current file position before reading.
+ # This will allow us to rewind a file in the event
+ # of a redirect.
+ try:
+ self._body_position = body.tell()
+ except OSError:
+ # This differentiates from None, allowing us to catch
+ # a failed `tell()` later when trying to rewind the body
+ self._body_position = object()
+
+ if files:
+ raise NotImplementedError(
+ "Streamed bodies and files are mutually exclusive."
+ )
+
+ if length:
+ self.headers["Content-Length"] = builtin_str(length)
+ else:
+ self.headers["Transfer-Encoding"] = "chunked"
+ else:
+ # Multi-part file uploads.
+ if files:
+ (body, content_type) = self._encode_files(files, data)
+ else:
+ if data:
+ body = self._encode_params(data)
+ if isinstance(data, basestring) or hasattr(data, "read"):
+ content_type = None
+ else:
+ content_type = "application/x-www-form-urlencoded"
+
+ self.prepare_content_length(body)
+
+ # Add content-type if it wasn't explicitly provided.
+ if content_type and ("content-type" not in self.headers):
+ self.headers["Content-Type"] = content_type
+
+ self.body = body
def prepare_content_length(self, body):
"""Prepare Content-Length header based on request method and body"""
- pass
+ if body is not None:
+ length = super_len(body)
+ if length:
+ # If length exists, set it. Otherwise, we fallback
+ # to Transfer-Encoding: chunked.
+ self.headers["Content-Length"] = builtin_str(length)
+ elif (
+ self.method not in ("GET", "HEAD")
+ and self.headers.get("Content-Length") is None
+ ):
+ # Set Content-Length to 0 for methods that can have a body
+ # but don't provide one. (i.e. not GET or HEAD)
+ self.headers["Content-Length"] = "0"
- def prepare_auth(self, auth, url=''):
+ def prepare_auth(self, auth, url=""):
"""Prepares the given HTTP auth data."""
- pass
+
+ # If no Auth is explicitly provided, extract it from the URL first.
+ if auth is None:
+ url_auth = get_auth_from_url(self.url)
+ auth = url_auth if any(url_auth) else None
+
+ if auth:
+ if isinstance(auth, tuple) and len(auth) == 2:
+ # special-case basic HTTP auth
+ auth = HTTPBasicAuth(*auth)
+
+ # Allow auth to make its changes.
+ r = auth(self)
+
+ # Update self to reflect the auth changes.
+ self.__dict__.update(r.__dict__)
+
+ # Recompute Content-Length
+ self.prepare_content_length(self.body)
def prepare_cookies(self, cookies):
"""Prepares the given HTTP cookie data.
@@ -206,33 +618,88 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
to ``prepare_cookies`` will have no actual effect, unless the "Cookie"
header is removed beforehand.
"""
- pass
+ if isinstance(cookies, cookielib.CookieJar):
+ self._cookies = cookies
+ else:
+ self._cookies = cookiejar_from_dict(cookies)
+
+ cookie_header = get_cookie_header(self._cookies, self)
+ if cookie_header is not None:
+ self.headers["Cookie"] = cookie_header
def prepare_hooks(self, hooks):
"""Prepares the given hooks."""
- pass
+ # hooks can be passed as None to the prepare method and to this
+ # method. To prevent iterating over None, simply use an empty list
+ # if hooks is False-y
+ hooks = hooks or []
+ for event in hooks:
+ self.register_hook(event, hooks[event])
class Response:
"""The :class:`Response <Response>` object, which contains a
server's response to an HTTP request.
"""
- __attrs__ = ['_content', 'status_code', 'headers', 'url', 'history',
- 'encoding', 'reason', 'cookies', 'elapsed', 'request']
+
+ __attrs__ = [
+ "_content",
+ "status_code",
+ "headers",
+ "url",
+ "history",
+ "encoding",
+ "reason",
+ "cookies",
+ "elapsed",
+ "request",
+ ]
def __init__(self):
self._content = False
self._content_consumed = False
self._next = None
+
+ #: Integer Code of responded HTTP Status, e.g. 404 or 200.
self.status_code = None
+
+ #: Case-insensitive Dictionary of Response Headers.
+ #: For example, ``headers['content-encoding']`` will return the
+ #: value of a ``'Content-Encoding'`` response header.
self.headers = CaseInsensitiveDict()
+
+ #: File-like object representation of response (for advanced usage).
+ #: Use of ``raw`` requires that ``stream=True`` be set on the request.
+ #: This requirement does not apply for use internally to Requests.
self.raw = None
+
+ #: Final URL location of Response.
self.url = None
+
+ #: Encoding to decode with when accessing r.text.
self.encoding = None
+
+ #: A list of :class:`Response <Response>` objects from
+ #: the history of the Request. Any redirect responses will end
+ #: up here. The list is sorted from the oldest to the most recent request.
self.history = []
+
+ #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK".
self.reason = None
+
+ #: A CookieJar of Cookies the server sent back.
self.cookies = cookiejar_from_dict({})
+
+ #: The amount of time elapsed between sending the request
+ #: and the arrival of the response (as a timedelta).
+ #: This property specifically measures the time taken between sending
+ #: the first byte of the request and finishing parsing the headers. It
+ #: is therefore unaffected by consuming the response content or the
+ #: value of the ``stream`` keyword argument.
self.elapsed = datetime.timedelta(0)
+
+ #: The :class:`PreparedRequest <PreparedRequest>` object to which this
+ #: is a response.
self.request = None
def __enter__(self):
@@ -242,18 +709,23 @@ class Response:
self.close()
def __getstate__(self):
+ # Consume everything; accessing the content attribute makes
+ # sure the content has been fully read.
if not self._content_consumed:
self.content
+
return {attr: getattr(self, attr, None) for attr in self.__attrs__}
def __setstate__(self, state):
for name, value in state.items():
setattr(self, name, value)
- setattr(self, '_content_consumed', True)
- setattr(self, 'raw', None)
+
+ # pickled objects do not have .raw
+ setattr(self, "_content_consumed", True)
+ setattr(self, "raw", None)
def __repr__(self):
- return f'<Response [{self.status_code}]>'
+ return f"<Response [{self.status_code}]>"
def __bool__(self):
"""Returns True if :attr:`status_code` is less than 400.
@@ -288,29 +760,41 @@ class Response:
the status code is between 200 and 400, this will return True. This
is **not** a check to see if the response code is ``200 OK``.
"""
- pass
+ try:
+ self.raise_for_status()
+ except HTTPError:
+ return False
+ return True
@property
def is_redirect(self):
"""True if this Response is a well-formed HTTP redirect that could have
been processed automatically (by :meth:`Session.resolve_redirects`).
"""
- pass
+ return "location" in self.headers and self.status_code in REDIRECT_STATI
@property
def is_permanent_redirect(self):
"""True if this Response one of the permanent versions of redirect."""
- pass
+ return "location" in self.headers and self.status_code in (
+ codes.moved_permanently,
+ codes.permanent_redirect,
+ )
@property
def next(self):
"""Returns a PreparedRequest for the next request in a redirect chain, if there is one."""
- pass
+ return self._next
@property
def apparent_encoding(self):
"""The apparent encoding, provided by the charset_normalizer or chardet libraries."""
- pass
+ if chardet is not None:
+ return chardet.detect(self.content)["encoding"]
+ else:
+ # If no character detection library is available, we'll fall back
+ # to a standard Python utf-8 str.
+ return "utf-8"
def iter_content(self, chunk_size=1, decode_unicode=False):
"""Iterates over the response data. When stream=True is set on the
@@ -328,22 +812,99 @@ class Response:
If decode_unicode is True, content will be decoded using the best
available encoding based on the response.
"""
- pass
- def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False,
- delimiter=None):
+ def generate():
+ # Special case for urllib3.
+ if hasattr(self.raw, "stream"):
+ try:
+ yield from self.raw.stream(chunk_size, decode_content=True)
+ except ProtocolError as e:
+ raise ChunkedEncodingError(e)
+ except DecodeError as e:
+ raise ContentDecodingError(e)
+ except ReadTimeoutError as e:
+ raise ConnectionError(e)
+ except SSLError as e:
+ raise RequestsSSLError(e)
+ else:
+ # Standard file-like object.
+ while True:
+ chunk = self.raw.read(chunk_size)
+ if not chunk:
+ break
+ yield chunk
+
+ self._content_consumed = True
+
+ if self._content_consumed and isinstance(self._content, bool):
+ raise StreamConsumedError()
+ elif chunk_size is not None and not isinstance(chunk_size, int):
+ raise TypeError(
+ f"chunk_size must be an int, it is instead a {type(chunk_size)}."
+ )
+ # simulate reading small chunks of the content
+ reused_chunks = iter_slices(self._content, chunk_size)
+
+ stream_chunks = generate()
+
+ chunks = reused_chunks if self._content_consumed else stream_chunks
+
+ if decode_unicode:
+ chunks = stream_decode_response_unicode(chunks, self)
+
+ return chunks
+
+ def iter_lines(
+ self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None
+ ):
"""Iterates over the response data, one line at a time. When
stream=True is set on the request, this avoids reading the
content at once into memory for large responses.
.. note:: This method is not reentrant safe.
"""
- pass
+
+ pending = None
+
+ for chunk in self.iter_content(
+ chunk_size=chunk_size, decode_unicode=decode_unicode
+ ):
+ if pending is not None:
+ chunk = pending + chunk
+
+ if delimiter:
+ lines = chunk.split(delimiter)
+ else:
+ lines = chunk.splitlines()
+
+ if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
+ pending = lines.pop()
+ else:
+ pending = None
+
+ yield from lines
+
+ if pending is not None:
+ yield pending
@property
def content(self):
"""Content of the response, in bytes."""
- pass
+
+ if self._content is False:
+ # Read the contents.
+ if self._content_consumed:
+ raise RuntimeError("The content for this response was already consumed")
+
+ if self.status_code == 0 or self.raw is None:
+ self._content = None
+ else:
+ self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b""
+
+ self._content_consumed = True
+ # don't need to release the connection; that's been handled by urllib3
+ # since we exhausted the data.
+ return self._content
@property
def text(self):
@@ -357,25 +918,110 @@ class Response:
non-HTTP knowledge to make a better guess at the encoding, you should
set ``r.encoding`` appropriately before accessing this property.
"""
- pass
+
+ # Try charset from content-type
+ content = None
+ encoding = self.encoding
+
+ if not self.content:
+ return ""
+
+ # Fallback to auto-detected encoding.
+ if self.encoding is None:
+ encoding = self.apparent_encoding
+
+ # Decode unicode from given encoding.
+ try:
+ content = str(self.content, encoding, errors="replace")
+ except (LookupError, TypeError):
+ # A LookupError is raised if the encoding was not found which could
+ # indicate a misspelling or similar mistake.
+ #
+ # A TypeError can be raised if encoding is None
+ #
+ # So we try blindly encoding.
+ content = str(self.content, errors="replace")
+
+ return content
def json(self, **kwargs):
- """Returns the json-encoded content of a response, if any.
+ r"""Returns the json-encoded content of a response, if any.
- :param \\*\\*kwargs: Optional arguments that ``json.loads`` takes.
+ :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
:raises requests.exceptions.JSONDecodeError: If the response body does not
contain valid json.
"""
- pass
+
+ if not self.encoding and self.content and len(self.content) > 3:
+ # No encoding set. JSON RFC 4627 section 3 states we should expect
+ # UTF-8, -16 or -32. Detect which one to use; If the detection or
+ # decoding fails, fall back to `self.text` (using charset_normalizer to make
+ # a best guess).
+ encoding = guess_json_utf(self.content)
+ if encoding is not None:
+ try:
+ return complexjson.loads(self.content.decode(encoding), **kwargs)
+ except UnicodeDecodeError:
+ # Wrong UTF codec detected; usually because it's not UTF-8
+ # but some other 8-bit codec. This is an RFC violation,
+ # and the server didn't bother to tell us what codec *was*
+ # used.
+ pass
+ except JSONDecodeError as e:
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
+
+ try:
+ return complexjson.loads(self.text, **kwargs)
+ except JSONDecodeError as e:
+ # Catch JSON-related errors and raise as requests.JSONDecodeError
+ # This aliases json.JSONDecodeError and simplejson.JSONDecodeError
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
@property
def links(self):
"""Returns the parsed header links of the response, if any."""
- pass
+
+ header = self.headers.get("link")
+
+ resolved_links = {}
+
+ if header:
+ links = parse_header_links(header)
+
+ for link in links:
+ key = link.get("rel") or link.get("url")
+ resolved_links[key] = link
+
+ return resolved_links
def raise_for_status(self):
"""Raises :class:`HTTPError`, if one occurred."""
- pass
+
+ http_error_msg = ""
+ if isinstance(self.reason, bytes):
+ # We attempt to decode utf-8 first because some servers
+ # choose to localize their reason strings. If the string
+ # isn't utf-8, we fall back to iso-8859-1 for all other
+ # encodings. (See PR #3538)
+ try:
+ reason = self.reason.decode("utf-8")
+ except UnicodeDecodeError:
+ reason = self.reason.decode("iso-8859-1")
+ else:
+ reason = self.reason
+
+ if 400 <= self.status_code < 500:
+ http_error_msg = (
+ f"{self.status_code} Client Error: {reason} for url: {self.url}"
+ )
+
+ elif 500 <= self.status_code < 600:
+ http_error_msg = (
+ f"{self.status_code} Server Error: {reason} for url: {self.url}"
+ )
+
+ if http_error_msg:
+ raise HTTPError(http_error_msg, response=self)
def close(self):
"""Releases the connection back to the pool. Once this method has been
@@ -383,4 +1029,9 @@ class Response:
*Note: Should not normally need to be called explicitly.*
"""
- pass
+ if not self._content_consumed:
+ self.raw.close()
+
+ release_conn = getattr(self.raw, "release_conn", None)
+ if release_conn is not None:
+ release_conn()
diff --git a/src/requests/packages.py b/src/requests/packages.py
index 959abfb2..5ab3d8e2 100644
--- a/src/requests/packages.py
+++ b/src/requests/packages.py
@@ -1,15 +1,23 @@
import sys
+
from .compat import chardet
-for package in ('urllib3', 'idna'):
+
+# This code exists for backwards compatibility reasons.
+# I don't like it either. Just look the other way. :)
+
+for package in ("urllib3", "idna"):
locals()[package] = __import__(package)
+ # This traversal is apparently necessary such that the identities are
+ # preserved (requests.packages.urllib3.* is urllib3.*)
for mod in list(sys.modules):
- if mod == package or mod.startswith(f'{package}.'):
- sys.modules[f'requests.packages.{mod}'] = sys.modules[mod]
+ if mod == package or mod.startswith(f"{package}."):
+ sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
+
if chardet is not None:
target = chardet.__name__
for mod in list(sys.modules):
- if mod == target or mod.startswith(f'{target}.'):
+ if mod == target or mod.startswith(f"{target}."):
imported_mod = sys.modules[mod]
- sys.modules[f'requests.packages.{mod}'] = imported_mod
- mod = mod.replace(target, 'chardet')
- sys.modules[f'requests.packages.{mod}'] = imported_mod
+ sys.modules[f"requests.packages.{mod}"] = imported_mod
+ mod = mod.replace(target, "chardet")
+ sys.modules[f"requests.packages.{mod}"] = imported_mod
diff --git a/src/requests/sessions.py b/src/requests/sessions.py
index b286e93c..b387bc36 100644
--- a/src/requests/sessions.py
+++ b/src/requests/sessions.py
@@ -10,18 +10,49 @@ import sys
import time
from collections import OrderedDict
from datetime import timedelta
+
from ._internal_utils import to_native_string
from .adapters import HTTPAdapter
from .auth import _basic_auth_str
from .compat import Mapping, cookielib, urljoin, urlparse
-from .cookies import RequestsCookieJar, cookiejar_from_dict, extract_cookies_to_jar, merge_cookies
-from .exceptions import ChunkedEncodingError, ContentDecodingError, InvalidSchema, TooManyRedirects
+from .cookies import (
+ RequestsCookieJar,
+ cookiejar_from_dict,
+ extract_cookies_to_jar,
+ merge_cookies,
+)
+from .exceptions import (
+ ChunkedEncodingError,
+ ContentDecodingError,
+ InvalidSchema,
+ TooManyRedirects,
+)
from .hooks import default_hooks, dispatch_hook
-from .models import DEFAULT_REDIRECT_LIMIT, REDIRECT_STATI, PreparedRequest, Request
+
+# formerly defined here, reexposed here for backward compatibility
+from .models import ( # noqa: F401
+ DEFAULT_REDIRECT_LIMIT,
+ REDIRECT_STATI,
+ PreparedRequest,
+ Request,
+)
from .status_codes import codes
from .structures import CaseInsensitiveDict
-from .utils import DEFAULT_PORTS, default_headers, get_auth_from_url, get_environ_proxies, get_netrc_auth, requote_uri, resolve_proxies, rewind_body, should_bypass_proxies, to_key_val_list
-if sys.platform == 'win32':
+from .utils import ( # noqa: F401
+ DEFAULT_PORTS,
+ default_headers,
+ get_auth_from_url,
+ get_environ_proxies,
+ get_netrc_auth,
+ requote_uri,
+ resolve_proxies,
+ rewind_body,
+ should_bypass_proxies,
+ to_key_val_list,
+)
+
+# Preferred clock, based on which one is more accurate on a given system.
+if sys.platform == "win32":
preferred_clock = time.perf_counter
else:
preferred_clock = time.time
@@ -32,7 +63,29 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
the explicit setting on that request, and the setting in the session. If a
setting is a dictionary, they will be merged together using `dict_class`
"""
- pass
+
+ if session_setting is None:
+ return request_setting
+
+ if request_setting is None:
+ return session_setting
+
+ # Bypass if not a dictionary (e.g. verify)
+ if not (
+ isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping)
+ ):
+ return request_setting
+
+ merged_setting = dict_class(to_key_val_list(session_setting))
+ merged_setting.update(to_key_val_list(request_setting))
+
+ # Remove keys that are set to None. Extract keys first to avoid altering
+ # the dictionary during iteration.
+ none_keys = [k for (k, v) in merged_setting.items() if v is None]
+ for key in none_keys:
+ del merged_setting[key]
+
+ return merged_setting
def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
@@ -41,31 +94,210 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
This is necessary because when request_hooks == {'response': []}, the
merge breaks Session hooks entirely.
"""
- pass
+ if session_hooks is None or session_hooks.get("response") == []:
+ return request_hooks
+ if request_hooks is None or request_hooks.get("response") == []:
+ return session_hooks
-class SessionRedirectMixin:
+ return merge_setting(request_hooks, session_hooks, dict_class)
+
+class SessionRedirectMixin:
def get_redirect_target(self, resp):
"""Receives a Response. Returns a redirect URI or ``None``"""
- pass
+ # Due to the nature of how requests processes redirects this method will
+ # be called at least once upon the original response and at least twice
+ # on each subsequent redirect response (if any).
+ # If a custom mixin is used to handle this logic, it may be advantageous
+ # to cache the redirect location onto the response object as a private
+ # attribute.
+ if resp.is_redirect:
+ location = resp.headers["location"]
+ # Currently the underlying http module on py3 decode headers
+ # in latin1, but empirical evidence suggests that latin1 is very
+ # rarely used with non-ASCII characters in HTTP headers.
+ # It is more likely to get UTF8 header rather than latin1.
+ # This causes incorrect handling of UTF8 encoded location headers.
+ # To solve this, we re-encode the location in latin1.
+ location = location.encode("latin1")
+ return to_native_string(location, "utf8")
+ return None
def should_strip_auth(self, old_url, new_url):
"""Decide whether Authorization header should be removed when redirecting"""
- pass
-
- def resolve_redirects(self, resp, req, stream=False, timeout=None,
- verify=True, cert=None, proxies=None, yield_requests=False, **
- adapter_kwargs):
+ old_parsed = urlparse(old_url)
+ new_parsed = urlparse(new_url)
+ if old_parsed.hostname != new_parsed.hostname:
+ return True
+ # Special case: allow http -> https redirect when using the standard
+ # ports. This isn't specified by RFC 7235, but is kept to avoid
+ # breaking backwards compatibility with older versions of requests
+ # that allowed any redirects on the same host.
+ if (
+ old_parsed.scheme == "http"
+ and old_parsed.port in (80, None)
+ and new_parsed.scheme == "https"
+ and new_parsed.port in (443, None)
+ ):
+ return False
+
+ # Handle default port usage corresponding to scheme.
+ changed_port = old_parsed.port != new_parsed.port
+ changed_scheme = old_parsed.scheme != new_parsed.scheme
+ default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None)
+ if (
+ not changed_scheme
+ and old_parsed.port in default_port
+ and new_parsed.port in default_port
+ ):
+ return False
+
+ # Standard case: root URI must match
+ return changed_port or changed_scheme
+
+ def resolve_redirects(
+ self,
+ resp,
+ req,
+ stream=False,
+ timeout=None,
+ verify=True,
+ cert=None,
+ proxies=None,
+ yield_requests=False,
+ **adapter_kwargs,
+ ):
"""Receives a Response. Returns a generator of Responses or Requests."""
- pass
+
+ hist = [] # keep track of history
+
+ url = self.get_redirect_target(resp)
+ previous_fragment = urlparse(req.url).fragment
+ while url:
+ prepared_request = req.copy()
+
+ # Update history and keep track of redirects.
+ # resp.history must ignore the original request in this loop
+ hist.append(resp)
+ resp.history = hist[1:]
+
+ try:
+ resp.content # Consume socket so it can be released
+ except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
+ resp.raw.read(decode_content=False)
+
+ if len(resp.history) >= self.max_redirects:
+ raise TooManyRedirects(
+ f"Exceeded {self.max_redirects} redirects.", response=resp
+ )
+
+ # Release the connection back into the pool.
+ resp.close()
+
+ # Handle redirection without scheme (see: RFC 1808 Section 4)
+ if url.startswith("//"):
+ parsed_rurl = urlparse(resp.url)
+ url = ":".join([to_native_string(parsed_rurl.scheme), url])
+
+ # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2)
+ parsed = urlparse(url)
+ if parsed.fragment == "" and previous_fragment:
+ parsed = parsed._replace(fragment=previous_fragment)
+ elif parsed.fragment:
+ previous_fragment = parsed.fragment
+ url = parsed.geturl()
+
+ # Facilitate relative 'location' headers, as allowed by RFC 7231.
+ # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
+ # Compliant with RFC3986, we percent encode the url.
+ if not parsed.netloc:
+ url = urljoin(resp.url, requote_uri(url))
+ else:
+ url = requote_uri(url)
+
+ prepared_request.url = to_native_string(url)
+
+ self.rebuild_method(prepared_request, resp)
+
+ # https://github.com/psf/requests/issues/1084
+ if resp.status_code not in (
+ codes.temporary_redirect,
+ codes.permanent_redirect,
+ ):
+ # https://github.com/psf/requests/issues/3490
+ purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding")
+ for header in purged_headers:
+ prepared_request.headers.pop(header, None)
+ prepared_request.body = None
+
+ headers = prepared_request.headers
+ headers.pop("Cookie", None)
+
+ # Extract any cookies sent on the response to the cookiejar
+ # in the new request. Because we've mutated our copied prepared
+ # request, use the old one that we haven't yet touched.
+ extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
+ merge_cookies(prepared_request._cookies, self.cookies)
+ prepared_request.prepare_cookies(prepared_request._cookies)
+
+ # Rebuild auth and proxy information.
+ proxies = self.rebuild_proxies(prepared_request, proxies)
+ self.rebuild_auth(prepared_request, resp)
+
+ # A failed tell() sets `_body_position` to `object()`. This non-None
+ # value ensures `rewindable` will be True, allowing us to raise an
+ # UnrewindableBodyError, instead of hanging the connection.
+ rewindable = prepared_request._body_position is not None and (
+ "Content-Length" in headers or "Transfer-Encoding" in headers
+ )
+
+ # Attempt to rewind consumed file-like object.
+ if rewindable:
+ rewind_body(prepared_request)
+
+ # Override the original request.
+ req = prepared_request
+
+ if yield_requests:
+ yield req
+ else:
+ resp = self.send(
+ req,
+ stream=stream,
+ timeout=timeout,
+ verify=verify,
+ cert=cert,
+ proxies=proxies,
+ allow_redirects=False,
+ **adapter_kwargs,
+ )
+
+ extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
+
+ # extract redirect url, if any, for the next loop
+ url = self.get_redirect_target(resp)
+ yield resp
def rebuild_auth(self, prepared_request, response):
"""When being redirected we may want to strip authentication from the
request to avoid leaking credentials. This method intelligently removes
and reapplies authentication where possible to avoid credential loss.
"""
- pass
+ headers = prepared_request.headers
+ url = prepared_request.url
+
+ if "Authorization" in headers and self.should_strip_auth(
+ response.request.url, url
+ ):
+ # If we get redirected to a new host, we should strip out any
+ # authentication headers.
+ del headers["Authorization"]
+
+ # .netrc might have more auth for us on our new host.
+ new_auth = get_netrc_auth(url) if self.trust_env else None
+ if new_auth is not None:
+ prepared_request.prepare_auth(new_auth)
def rebuild_proxies(self, prepared_request, proxies):
"""This method re-evaluates the proxy configuration by considering the
@@ -79,13 +311,46 @@ class SessionRedirectMixin:
:rtype: dict
"""
- pass
+ headers = prepared_request.headers
+ scheme = urlparse(prepared_request.url).scheme
+ new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env)
+
+ if "Proxy-Authorization" in headers:
+ del headers["Proxy-Authorization"]
+
+ try:
+ username, password = get_auth_from_url(new_proxies[scheme])
+ except KeyError:
+ username, password = None, None
+
+ # urllib3 handles proxy authorization for us in the standard adapter.
+ # Avoid appending this to TLS tunneled requests where it may be leaked.
+ if not scheme.startswith("https") and username and password:
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
+
+ return new_proxies
def rebuild_method(self, prepared_request, response):
"""When being redirected we may want to change the method of the request
based on certain specs or browser behavior.
"""
- pass
+ method = prepared_request.method
+
+ # https://tools.ietf.org/html/rfc7231#section-6.4.4
+ if response.status_code == codes.see_other and method != "HEAD":
+ method = "GET"
+
+ # Do what the browsers do, despite standards...
+ # First, turn 302s into GETs.
+ if response.status_code == codes.found and method != "HEAD":
+ method = "GET"
+
+ # Second, if a POST is responded to with a 301, turn it into a GET.
+ # This bizarre behaviour is explained in Issue 1704.
+ if response.status_code == codes.moved and method == "POST":
+ method = "GET"
+
+ prepared_request.method = method
class Session(SessionRedirectMixin):
@@ -106,24 +371,82 @@ class Session(SessionRedirectMixin):
... s.get('https://httpbin.org/get')
<Response [200]>
"""
- __attrs__ = ['headers', 'cookies', 'auth', 'proxies', 'hooks', 'params',
- 'verify', 'cert', 'adapters', 'stream', 'trust_env', 'max_redirects']
+
+ __attrs__ = [
+ "headers",
+ "cookies",
+ "auth",
+ "proxies",
+ "hooks",
+ "params",
+ "verify",
+ "cert",
+ "adapters",
+ "stream",
+ "trust_env",
+ "max_redirects",
+ ]
def __init__(self):
+ #: A case-insensitive dictionary of headers to be sent on each
+ #: :class:`Request <Request>` sent from this
+ #: :class:`Session <Session>`.
self.headers = default_headers()
+
+ #: Default Authentication tuple or object to attach to
+ #: :class:`Request <Request>`.
self.auth = None
+
+ #: Dictionary mapping protocol or protocol and host to the URL of the proxy
+ #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
+ #: be used on each :class:`Request <Request>`.
self.proxies = {}
+
+ #: Event-handling hooks.
self.hooks = default_hooks()
+
+ #: Dictionary of querystring data to attach to each
+ #: :class:`Request <Request>`. The dictionary values may be lists for
+ #: representing multivalued query parameters.
self.params = {}
+
+ #: Stream response content default.
self.stream = False
+
+ #: SSL Verification default.
+ #: Defaults to `True`, requiring requests to verify the TLS certificate at the
+ #: remote end.
+ #: If verify is set to `False`, requests will accept any TLS certificate
+ #: presented by the server, and will ignore hostname mismatches and/or
+ #: expired certificates, which will make your application vulnerable to
+ #: man-in-the-middle (MitM) attacks.
+ #: Only set this to `False` for testing.
self.verify = True
+
+ #: SSL client certificate default, if String, path to ssl client
+ #: cert file (.pem). If Tuple, ('cert', 'key') pair.
self.cert = None
+
+ #: Maximum number of redirects allowed. If the request exceeds this
+ #: limit, a :class:`TooManyRedirects` exception is raised.
+ #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
+ #: 30.
self.max_redirects = DEFAULT_REDIRECT_LIMIT
+
+ #: Trust environment settings for proxy configuration, default
+ #: authentication and similar.
self.trust_env = True
+
+ #: A CookieJar containing all currently outstanding cookies set on this
+ #: session. By default it is a
+ #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
+ #: may be any other ``cookielib.CookieJar`` compatible object.
self.cookies = cookiejar_from_dict({})
+
+ # Default connection adapters.
self.adapters = OrderedDict()
- self.mount('https://', HTTPAdapter())
- self.mount('http://', HTTPAdapter())
+ self.mount("https://", HTTPAdapter())
+ self.mount("http://", HTTPAdapter())
def __enter__(self):
return self
@@ -141,12 +464,58 @@ class Session(SessionRedirectMixin):
session's settings.
:rtype: requests.PreparedRequest
"""
- pass
-
- def request(self, method, url, params=None, data=None, headers=None,
- cookies=None, files=None, auth=None, timeout=None, allow_redirects=
- True, proxies=None, hooks=None, stream=None, verify=None, cert=None,
- json=None):
+ cookies = request.cookies or {}
+
+ # Bootstrap CookieJar.
+ if not isinstance(cookies, cookielib.CookieJar):
+ cookies = cookiejar_from_dict(cookies)
+
+ # Merge with session cookies
+ merged_cookies = merge_cookies(
+ merge_cookies(RequestsCookieJar(), self.cookies), cookies
+ )
+
+ # Set environment's basic authentication if not explicitly set.
+ auth = request.auth
+ if self.trust_env and not auth and not self.auth:
+ auth = get_netrc_auth(request.url)
+
+ p = PreparedRequest()
+ p.prepare(
+ method=request.method.upper(),
+ url=request.url,
+ files=request.files,
+ data=request.data,
+ json=request.json,
+ headers=merge_setting(
+ request.headers, self.headers, dict_class=CaseInsensitiveDict
+ ),
+ params=merge_setting(request.params, self.params),
+ auth=merge_setting(auth, self.auth),
+ cookies=merged_cookies,
+ hooks=merge_hooks(request.hooks, self.hooks),
+ )
+ return p
+
+ def request(
+ self,
+ method,
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ files=None,
+ auth=None,
+ timeout=None,
+ allow_redirects=True,
+ proxies=None,
+ hooks=None,
+ stream=None,
+ verify=None,
+ cert=None,
+ json=None,
+ ):
"""Constructs a :class:`Request <Request>`, prepares it and sends it.
Returns :class:`Response <Response>` object.
@@ -190,84 +559,193 @@ class Session(SessionRedirectMixin):
If Tuple, ('cert', 'key') pair.
:rtype: requests.Response
"""
- pass
+ # Create the Request.
+ req = Request(
+ method=method.upper(),
+ url=url,
+ headers=headers,
+ files=files,
+ data=data or {},
+ json=json,
+ params=params or {},
+ auth=auth,
+ cookies=cookies,
+ hooks=hooks,
+ )
+ prep = self.prepare_request(req)
+
+ proxies = proxies or {}
+
+ settings = self.merge_environment_settings(
+ prep.url, proxies, stream, verify, cert
+ )
+
+ # Send the request.
+ send_kwargs = {
+ "timeout": timeout,
+ "allow_redirects": allow_redirects,
+ }
+ send_kwargs.update(settings)
+ resp = self.send(prep, **send_kwargs)
+
+ return resp
def get(self, url, **kwargs):
- """Sends a GET request. Returns :class:`Response` object.
+ r"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("GET", url, **kwargs)
def options(self, url, **kwargs):
- """Sends a OPTIONS request. Returns :class:`Response` object.
+ r"""Sends a OPTIONS request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("OPTIONS", url, **kwargs)
def head(self, url, **kwargs):
- """Sends a HEAD request. Returns :class:`Response` object.
+ r"""Sends a HEAD request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ kwargs.setdefault("allow_redirects", False)
+ return self.request("HEAD", url, **kwargs)
def post(self, url, data=None, json=None, **kwargs):
- """Sends a POST request. Returns :class:`Response` object.
+ r"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ return self.request("POST", url, data=data, json=json, **kwargs)
def put(self, url, data=None, **kwargs):
- """Sends a PUT request. Returns :class:`Response` object.
+ r"""Sends a PUT request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ return self.request("PUT", url, data=data, **kwargs)
def patch(self, url, data=None, **kwargs):
- """Sends a PATCH request. Returns :class:`Response` object.
+ r"""Sends a PATCH request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ return self.request("PATCH", url, data=data, **kwargs)
def delete(self, url, **kwargs):
- """Sends a DELETE request. Returns :class:`Response` object.
+ r"""Sends a DELETE request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
- :param \\*\\*kwargs: Optional arguments that ``request`` takes.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
- pass
+
+ return self.request("DELETE", url, **kwargs)
def send(self, request, **kwargs):
"""Send a given PreparedRequest.
:rtype: requests.Response
"""
- pass
+ # Set defaults that the hooks can utilize to ensure they always have
+ # the correct parameters to reproduce the previous request.
+ kwargs.setdefault("stream", self.stream)
+ kwargs.setdefault("verify", self.verify)
+ kwargs.setdefault("cert", self.cert)
+ if "proxies" not in kwargs:
+ kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env)
+
+ # It's possible that users might accidentally send a Request object.
+ # Guard against that specific failure case.
+ if isinstance(request, Request):
+ raise ValueError("You can only send PreparedRequests.")
+
+ # Set up variables needed for resolve_redirects and dispatching of hooks
+ allow_redirects = kwargs.pop("allow_redirects", True)
+ stream = kwargs.get("stream")
+ hooks = request.hooks
+
+ # Get the appropriate adapter to use
+ adapter = self.get_adapter(url=request.url)
+
+ # Start time (approximately) of the request
+ start = preferred_clock()
+
+ # Send the request
+ r = adapter.send(request, **kwargs)
+
+ # Total elapsed time of the request (approximately)
+ elapsed = preferred_clock() - start
+ r.elapsed = timedelta(seconds=elapsed)
+
+ # Response manipulation hooks
+ r = dispatch_hook("response", hooks, r, **kwargs)
+
+ # Persist cookies
+ if r.history:
+ # If the hooks create history then we want those cookies too
+ for resp in r.history:
+ extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
+
+ extract_cookies_to_jar(self.cookies, request, r.raw)
+
+ # Resolve redirects if allowed.
+ if allow_redirects:
+ # Redirect resolving generator.
+ gen = self.resolve_redirects(r, request, **kwargs)
+ history = [resp for resp in gen]
+ else:
+ history = []
+
+ # Shuffle things around if there's history.
+ if history:
+ # Insert the first (original) request at the start
+ history.insert(0, r)
+ # Get the last request made
+ r = history.pop()
+ r.history = history
+
+ # If redirects aren't being followed, store the response on the Request for Response.next().
+ if not allow_redirects:
+ try:
+ r._next = next(
+ self.resolve_redirects(r, request, yield_requests=True, **kwargs)
+ )
+ except StopIteration:
+ pass
+
+ if not stream:
+ r.content
+
+ return r
def merge_environment_settings(self, url, proxies, stream, verify, cert):
"""
@@ -275,7 +753,30 @@ class Session(SessionRedirectMixin):
:rtype: dict
"""
- pass
+ # Gather clues from the surrounding environment.
+ if self.trust_env:
+ # Set environment's proxies.
+ no_proxy = proxies.get("no_proxy") if proxies is not None else None
+ env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
+ for k, v in env_proxies.items():
+ proxies.setdefault(k, v)
+
+ # Look for requests environment configuration
+ # and be compatible with cURL.
+ if verify is True or verify is None:
+ verify = (
+ os.environ.get("REQUESTS_CA_BUNDLE")
+ or os.environ.get("CURL_CA_BUNDLE")
+ or verify
+ )
+
+ # Merge all the kwargs.
+ proxies = merge_setting(proxies, self.proxies)
+ stream = merge_setting(stream, self.stream)
+ verify = merge_setting(verify, self.verify)
+ cert = merge_setting(cert, self.cert)
+
+ return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
def get_adapter(self, url):
"""
@@ -283,18 +784,28 @@ class Session(SessionRedirectMixin):
:rtype: requests.adapters.BaseAdapter
"""
- pass
+ for prefix, adapter in self.adapters.items():
+ if url.lower().startswith(prefix.lower()):
+ return adapter
+
+ # Nothing matches :-/
+ raise InvalidSchema(f"No connection adapters were found for {url!r}")
def close(self):
"""Closes all adapters and as such the session"""
- pass
+ for v in self.adapters.values():
+ v.close()
def mount(self, prefix, adapter):
"""Registers a connection adapter to a prefix.
Adapters are sorted in descending order by prefix length.
"""
- pass
+ self.adapters[prefix] = adapter
+ keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
+
+ for key in keys_to_move:
+ self.adapters[key] = self.adapters.pop(key)
def __getstate__(self):
state = {attr: getattr(self, attr, None) for attr in self.__attrs__}
@@ -317,4 +828,4 @@ def session():
:rtype: Session
"""
- pass
+ return Session()
diff --git a/src/requests/status_codes.py b/src/requests/status_codes.py
index 9ae0edc1..c7945a2f 100644
--- a/src/requests/status_codes.py
+++ b/src/requests/status_codes.py
@@ -1,4 +1,4 @@
-"""
+r"""
The ``codes`` object defines a mapping from common names for HTTP statuses
to their numerical codes, accessible either as attributes or as dictionary
items.
@@ -10,58 +10,119 @@ Example::
307
>>> requests.codes.teapot
418
- >>> requests.codes['\\o/']
+ >>> requests.codes['\o/']
200
Some codes have multiple names, and both upper- and lower-case versions of
the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
``codes.okay`` all correspond to the HTTP status code 200.
"""
+
from .structures import LookupDict
-_codes = {(100): ('continue',), (101): ('switching_protocols',), (102): (
- 'processing', 'early-hints'), (103): ('checkpoint',), (122): (
- 'uri_too_long', 'request_uri_too_long'), (200): ('ok', 'okay', 'all_ok',
- 'all_okay', 'all_good', '\\o/', '✓'), (201): ('created',), (202): (
- 'accepted',), (203): ('non_authoritative_info',
- 'non_authoritative_information'), (204): ('no_content',), (205): (
- 'reset_content', 'reset'), (206): ('partial_content', 'partial'), (207):
- ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'), (
- 208): ('already_reported',), (226): ('im_used',), (300): (
- 'multiple_choices',), (301): ('moved_permanently', 'moved', '\\o-'), (
- 302): ('found',), (303): ('see_other', 'other'), (304): ('not_modified'
- ,), (305): ('use_proxy',), (306): ('switch_proxy',), (307): (
- 'temporary_redirect', 'temporary_moved', 'temporary'), (308): (
- 'permanent_redirect', 'resume_incomplete', 'resume'), (400): (
- 'bad_request', 'bad'), (401): ('unauthorized',), (402): (
- 'payment_required', 'payment'), (403): ('forbidden',), (404): (
- 'not_found', '-o-'), (405): ('method_not_allowed', 'not_allowed'), (406
- ): ('not_acceptable',), (407): ('proxy_authentication_required',
- 'proxy_auth', 'proxy_authentication'), (408): ('request_timeout',
- 'timeout'), (409): ('conflict',), (410): ('gone',), (411): (
- 'length_required',), (412): ('precondition_failed', 'precondition'), (
- 413): ('request_entity_too_large', 'content_too_large'), (414): (
- 'request_uri_too_large', 'uri_too_long'), (415): (
- 'unsupported_media_type', 'unsupported_media', 'media_type'), (416): (
- 'requested_range_not_satisfiable', 'requested_range',
- 'range_not_satisfiable'), (417): ('expectation_failed',), (418): (
- 'im_a_teapot', 'teapot', 'i_am_a_teapot'), (421): (
- 'misdirected_request',), (422): ('unprocessable_entity',
- 'unprocessable', 'unprocessable_content'), (423): ('locked',), (424): (
- 'failed_dependency', 'dependency'), (425): ('unordered_collection',
- 'unordered', 'too_early'), (426): ('upgrade_required', 'upgrade'), (428
- ): ('precondition_required', 'precondition'), (429): (
- 'too_many_requests', 'too_many'), (431): ('header_fields_too_large',
- 'fields_too_large'), (444): ('no_response', 'none'), (449): (
- 'retry_with', 'retry'), (450): ('blocked_by_windows_parental_controls',
- 'parental_controls'), (451): ('unavailable_for_legal_reasons',
- 'legal_reasons'), (499): ('client_closed_request',), (500): (
- 'internal_server_error', 'server_error', '/o\\', '✗'), (501): (
- 'not_implemented',), (502): ('bad_gateway',), (503): (
- 'service_unavailable', 'unavailable'), (504): ('gateway_timeout',), (
- 505): ('http_version_not_supported', 'http_version'), (506): (
- 'variant_also_negotiates',), (507): ('insufficient_storage',), (509): (
- 'bandwidth_limit_exceeded', 'bandwidth'), (510): ('not_extended',), (
- 511): ('network_authentication_required', 'network_auth',
- 'network_authentication')}
-codes = LookupDict(name='status_codes')
+
+_codes = {
+ # Informational.
+ 100: ("continue",),
+ 101: ("switching_protocols",),
+ 102: ("processing", "early-hints"),
+ 103: ("checkpoint",),
+ 122: ("uri_too_long", "request_uri_too_long"),
+ 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
+ 201: ("created",),
+ 202: ("accepted",),
+ 203: ("non_authoritative_info", "non_authoritative_information"),
+ 204: ("no_content",),
+ 205: ("reset_content", "reset"),
+ 206: ("partial_content", "partial"),
+ 207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"),
+ 208: ("already_reported",),
+ 226: ("im_used",),
+ # Redirection.
+ 300: ("multiple_choices",),
+ 301: ("moved_permanently", "moved", "\\o-"),
+ 302: ("found",),
+ 303: ("see_other", "other"),
+ 304: ("not_modified",),
+ 305: ("use_proxy",),
+ 306: ("switch_proxy",),
+ 307: ("temporary_redirect", "temporary_moved", "temporary"),
+ 308: (
+ "permanent_redirect",
+ "resume_incomplete",
+ "resume",
+ ), # "resume" and "resume_incomplete" to be removed in 3.0
+ # Client Error.
+ 400: ("bad_request", "bad"),
+ 401: ("unauthorized",),
+ 402: ("payment_required", "payment"),
+ 403: ("forbidden",),
+ 404: ("not_found", "-o-"),
+ 405: ("method_not_allowed", "not_allowed"),
+ 406: ("not_acceptable",),
+ 407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"),
+ 408: ("request_timeout", "timeout"),
+ 409: ("conflict",),
+ 410: ("gone",),
+ 411: ("length_required",),
+ 412: ("precondition_failed", "precondition"),
+ 413: ("request_entity_too_large", "content_too_large"),
+ 414: ("request_uri_too_large", "uri_too_long"),
+ 415: ("unsupported_media_type", "unsupported_media", "media_type"),
+ 416: (
+ "requested_range_not_satisfiable",
+ "requested_range",
+ "range_not_satisfiable",
+ ),
+ 417: ("expectation_failed",),
+ 418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
+ 421: ("misdirected_request",),
+ 422: ("unprocessable_entity", "unprocessable", "unprocessable_content"),
+ 423: ("locked",),
+ 424: ("failed_dependency", "dependency"),
+ 425: ("unordered_collection", "unordered", "too_early"),
+ 426: ("upgrade_required", "upgrade"),
+ 428: ("precondition_required", "precondition"),
+ 429: ("too_many_requests", "too_many"),
+ 431: ("header_fields_too_large", "fields_too_large"),
+ 444: ("no_response", "none"),
+ 449: ("retry_with", "retry"),
+ 450: ("blocked_by_windows_parental_controls", "parental_controls"),
+ 451: ("unavailable_for_legal_reasons", "legal_reasons"),
+ 499: ("client_closed_request",),
+ # Server Error.
+ 500: ("internal_server_error", "server_error", "/o\\", "✗"),
+ 501: ("not_implemented",),
+ 502: ("bad_gateway",),
+ 503: ("service_unavailable", "unavailable"),
+ 504: ("gateway_timeout",),
+ 505: ("http_version_not_supported", "http_version"),
+ 506: ("variant_also_negotiates",),
+ 507: ("insufficient_storage",),
+ 509: ("bandwidth_limit_exceeded", "bandwidth"),
+ 510: ("not_extended",),
+ 511: ("network_authentication_required", "network_auth", "network_authentication"),
+}
+
+codes = LookupDict(name="status_codes")
+
+
+def _init():
+ for code, titles in _codes.items():
+ for title in titles:
+ setattr(codes, title, code)
+ if not title.startswith(("\\", "/")):
+ setattr(codes, title.upper(), code)
+
+ def doc(code):
+ names = ", ".join(f"``{n}``" for n in _codes[code])
+ return "* %d: %s" % (code, names)
+
+ global __doc__
+ __doc__ = (
+ __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes))
+ if __doc__ is not None
+ else None
+ )
+
+
_init()
diff --git a/src/requests/structures.py b/src/requests/structures.py
index b389ca26..188e13e4 100644
--- a/src/requests/structures.py
+++ b/src/requests/structures.py
@@ -4,7 +4,9 @@ requests.structures
Data structures that power Requests.
"""
+
from collections import OrderedDict
+
from .compat import Mapping, MutableMapping
@@ -42,7 +44,9 @@ class CaseInsensitiveDict(MutableMapping):
self.update(data, **kwargs)
def __setitem__(self, key, value):
- self._store[key.lower()] = key, value
+ # Use the lowercased key for lookups, but store the actual
+ # key alongside the value.
+ self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
@@ -58,15 +62,20 @@ class CaseInsensitiveDict(MutableMapping):
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
- pass
+ return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
def __eq__(self, other):
if isinstance(other, Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
+ # Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
+ # Copy is required
+ def copy(self):
+ return CaseInsensitiveDict(self._store.values())
+
def __repr__(self):
return str(dict(self.items()))
@@ -82,4 +91,9 @@ class LookupDict(dict):
return f"<lookup '{self.name}'>"
def __getitem__(self, key):
+ # We allow fall-through here, so values default to None
+
return self.__dict__.get(key, None)
+
+ def get(self, key, default=None):
+ return self.__dict__.get(key, default)
diff --git a/src/requests/utils.py b/src/requests/utils.py
index 504e22fc..ae6c42f6 100644
--- a/src/requests/utils.py
+++ b/src/requests/utils.py
@@ -5,6 +5,7 @@ requests.utils
This module provides utility functions that are used within Requests
that are also useful for external consumption.
"""
+
import codecs
import contextlib
import io
@@ -17,45 +18,251 @@ import tempfile
import warnings
import zipfile
from collections import OrderedDict
+
from urllib3.util import make_headers, parse_url
+
from . import certs
from .__version__ import __version__
-from ._internal_utils import _HEADER_VALIDATORS_BYTE, _HEADER_VALIDATORS_STR, HEADER_VALIDATORS, to_native_string
-from .compat import Mapping, basestring, bytes, getproxies, getproxies_environment, integer_types
+
+# to_native_string is unused here, but imported here for backwards compatibility
+from ._internal_utils import ( # noqa: F401
+ _HEADER_VALIDATORS_BYTE,
+ _HEADER_VALIDATORS_STR,
+ HEADER_VALIDATORS,
+ to_native_string,
+)
+from .compat import (
+ Mapping,
+ basestring,
+ bytes,
+ getproxies,
+ getproxies_environment,
+ integer_types,
+)
from .compat import parse_http_list as _parse_list_header
-from .compat import proxy_bypass, proxy_bypass_environment, quote, str, unquote, urlparse, urlunparse
+from .compat import (
+ proxy_bypass,
+ proxy_bypass_environment,
+ quote,
+ str,
+ unquote,
+ urlparse,
+ urlunparse,
+)
from .cookies import cookiejar_from_dict
-from .exceptions import FileModeWarning, InvalidHeader, InvalidURL, UnrewindableBodyError
+from .exceptions import (
+ FileModeWarning,
+ InvalidHeader,
+ InvalidURL,
+ UnrewindableBodyError,
+)
from .structures import CaseInsensitiveDict
-NETRC_FILES = '.netrc', '_netrc'
+
+NETRC_FILES = (".netrc", "_netrc")
+
DEFAULT_CA_BUNDLE_PATH = certs.where()
-DEFAULT_PORTS = {'http': 80, 'https': 443}
-DEFAULT_ACCEPT_ENCODING = ', '.join(re.split(',\\s*', make_headers(
- accept_encoding=True)['accept-encoding']))
-if sys.platform == 'win32':
- def proxy_bypass(host):
+DEFAULT_PORTS = {"http": 80, "https": 443}
+
+# Ensure that ', ' is used to preserve previous delimiter behavior.
+DEFAULT_ACCEPT_ENCODING = ", ".join(
+ re.split(r",\s*", make_headers(accept_encoding=True)["accept-encoding"])
+)
+
+
+if sys.platform == "win32":
+ # provide a proxy_bypass version on Windows without DNS lookups
+
+ def proxy_bypass_registry(host):
+ try:
+ import winreg
+ except ImportError:
+ return False
+
+ try:
+ internetSettings = winreg.OpenKey(
+ winreg.HKEY_CURRENT_USER,
+ r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
+ )
+ # ProxyEnable could be REG_SZ or REG_DWORD, normalizing it
+ proxyEnable = int(winreg.QueryValueEx(internetSettings, "ProxyEnable")[0])
+ # ProxyOverride is almost always a string
+ proxyOverride = winreg.QueryValueEx(internetSettings, "ProxyOverride")[0]
+ except (OSError, ValueError):
+ return False
+ if not proxyEnable or not proxyOverride:
+ return False
+
+ # make a check value list from the registry entry: replace the
+ # '<local>' string by the localhost entry and the corresponding
+ # canonical entry.
+ proxyOverride = proxyOverride.split(";")
+ # filter out empty strings to avoid re.match return true in the following code.
+ proxyOverride = filter(None, proxyOverride)
+ # now check if we match one of the registry values.
+ for test in proxyOverride:
+ if test == "<local>":
+ if "." not in host:
+ return True
+ test = test.replace(".", r"\.") # mask dots
+ test = test.replace("*", r".*") # change glob sequence
+ test = test.replace("?", r".") # change glob char
+ if re.match(test, host, re.I):
+ return True
+ return False
+
+ def proxy_bypass(host): # noqa
"""Return True, if the host should be bypassed.
Checks proxy settings gathered from the environment, if specified,
or the registry.
"""
- pass
+ if getproxies_environment():
+ return proxy_bypass_environment(host)
+ else:
+ return proxy_bypass_registry(host)
def dict_to_sequence(d):
"""Returns an internal sequence dictionary update."""
- pass
+
+ if hasattr(d, "items"):
+ d = d.items()
+
+ return d
+
+
+def super_len(o):
+ total_length = None
+ current_position = 0
+
+ if isinstance(o, str):
+ o = o.encode("utf-8")
+
+ if hasattr(o, "__len__"):
+ total_length = len(o)
+
+ elif hasattr(o, "len"):
+ total_length = o.len
+
+ elif hasattr(o, "fileno"):
+ try:
+ fileno = o.fileno()
+ except (io.UnsupportedOperation, AttributeError):
+ # AttributeError is a surprising exception, seeing as how we've just checked
+ # that `hasattr(o, 'fileno')`. It happens for objects obtained via
+ # `Tarfile.extractfile()`, per issue 5229.
+ pass
+ else:
+ total_length = os.fstat(fileno).st_size
+
+ # Having used fstat to determine the file length, we need to
+ # confirm that this file was opened up in binary mode.
+ if "b" not in o.mode:
+ warnings.warn(
+ (
+ "Requests has determined the content-length for this "
+ "request using the binary size of the file: however, the "
+ "file has been opened in text mode (i.e. without the 'b' "
+ "flag in the mode). This may lead to an incorrect "
+ "content-length. In Requests 3.0, support will be removed "
+ "for files in text mode."
+ ),
+ FileModeWarning,
+ )
+
+ if hasattr(o, "tell"):
+ try:
+ current_position = o.tell()
+ except OSError:
+ # This can happen in some weird situations, such as when the file
+ # is actually a special file descriptor like stdin. In this
+ # instance, we don't know what the length is, so set it to zero and
+ # let requests chunk it instead.
+ if total_length is not None:
+ current_position = total_length
+ else:
+ if hasattr(o, "seek") and total_length is None:
+ # StringIO and BytesIO have seek but no usable fileno
+ try:
+ # seek to end of file
+ o.seek(0, 2)
+ total_length = o.tell()
+
+ # seek back to current position to support
+ # partially read file-like objects
+ o.seek(current_position or 0)
+ except OSError:
+ total_length = 0
+
+ if total_length is None:
+ total_length = 0
+
+ return max(0, total_length - current_position)
def get_netrc_auth(url, raise_errors=False):
"""Returns the Requests tuple auth for a given url from netrc."""
- pass
+
+ netrc_file = os.environ.get("NETRC")
+ if netrc_file is not None:
+ netrc_locations = (netrc_file,)
+ else:
+ netrc_locations = (f"~/{f}" for f in NETRC_FILES)
+
+ try:
+ from netrc import NetrcParseError, netrc
+
+ netrc_path = None
+
+ for f in netrc_locations:
+ try:
+ loc = os.path.expanduser(f)
+ except KeyError:
+ # os.path.expanduser can fail when $HOME is undefined and
+ # getpwuid fails. See https://bugs.python.org/issue20164 &
+ # https://github.com/psf/requests/issues/1846
+ return
+
+ if os.path.exists(loc):
+ netrc_path = loc
+ break
+
+ # Abort early if there isn't one.
+ if netrc_path is None:
+ return
+
+ ri = urlparse(url)
+
+ # Strip port numbers from netloc. This weird `if...encode`` dance is
+ # used for Python 3.2, which doesn't support unicode literals.
+ splitstr = b":"
+ if isinstance(url, str):
+ splitstr = splitstr.decode("ascii")
+ host = ri.netloc.split(splitstr)[0]
+
+ try:
+ _netrc = netrc(netrc_path).authenticators(host)
+ if _netrc:
+ # Return with login / password
+ login_i = 0 if _netrc[0] else 1
+ return (_netrc[login_i], _netrc[2])
+ except (NetrcParseError, OSError):
+ # If there was a parsing error or a permissions issue reading the file,
+ # we'll just skip netrc auth unless explicitly asked to raise errors.
+ if raise_errors:
+ raise
+
+ # App Engine hackiness.
+ except (ImportError, AttributeError):
+ pass
def guess_filename(obj):
"""Tries to guess the filename of the given object."""
- pass
+ name = getattr(obj, "name", None)
+ if name and isinstance(name, basestring) and name[0] != "<" and name[-1] != ">":
+ return os.path.basename(name)
def extract_zipped_paths(path):
@@ -63,13 +270,49 @@ def extract_zipped_paths(path):
archive with the location of an extracted copy of the target, or else
just return the provided path unchanged.
"""
- pass
+ if os.path.exists(path):
+ # this is already a valid path, no need to do anything further
+ return path
+
+ # find the first valid part of the provided path and treat that as a zip archive
+ # assume the rest of the path is the name of a member in the archive
+ archive, member = os.path.split(path)
+ while archive and not os.path.exists(archive):
+ archive, prefix = os.path.split(archive)
+ if not prefix:
+ # If we don't check for an empty prefix after the split (in other words, archive remains unchanged after the split),
+ # we _can_ end up in an infinite loop on a rare corner case affecting a small number of users
+ break
+ member = "/".join([prefix, member])
+
+ if not zipfile.is_zipfile(archive):
+ return path
+
+ zip_file = zipfile.ZipFile(archive)
+ if member not in zip_file.namelist():
+ return path
+
+ # we have a valid zip archive and a valid member of that archive
+ tmp = tempfile.gettempdir()
+ extracted_path = os.path.join(tmp, member.split("/")[-1])
+ if not os.path.exists(extracted_path):
+ # use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition
+ with atomic_open(extracted_path) as file_handler:
+ file_handler.write(zip_file.read(member))
+ return extracted_path
@contextlib.contextmanager
def atomic_open(filename):
"""Write a file to the disk in an atomic fashion"""
- pass
+ tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename))
+ try:
+ with os.fdopen(tmp_descriptor, "wb") as tmp_handler:
+ yield tmp_handler
+ os.replace(tmp_name, filename)
+ except BaseException:
+ os.remove(tmp_name)
+ raise
def from_key_val_list(value):
@@ -90,7 +333,13 @@ def from_key_val_list(value):
:rtype: OrderedDict
"""
- pass
+ if value is None:
+ return None
+
+ if isinstance(value, (str, bytes, bool, int)):
+ raise ValueError("cannot encode objects that are not 2-tuples")
+
+ return OrderedDict(value)
def to_key_val_list(value):
@@ -110,9 +359,19 @@ def to_key_val_list(value):
:rtype: list
"""
- pass
+ if value is None:
+ return None
+
+ if isinstance(value, (str, bytes, bool, int)):
+ raise ValueError("cannot encode objects that are not 2-tuples")
+
+ if isinstance(value, Mapping):
+ value = value.items()
+ return list(value)
+
+# From mitsuhiko/werkzeug (used with permission).
def parse_list_header(value):
"""Parse lists as described by RFC 2068 Section 2.
@@ -136,9 +395,15 @@ def parse_list_header(value):
:return: :class:`list`
:rtype: list
"""
- pass
+ result = []
+ for item in _parse_list_header(value):
+ if item[:1] == item[-1:] == '"':
+ item = unquote_header_value(item[1:-1])
+ result.append(item)
+ return result
+# From mitsuhiko/werkzeug (used with permission).
def parse_dict_header(value):
"""Parse lists of key, value pairs as described by RFC 2068 Section 2 and
convert them into a python dict:
@@ -161,18 +426,42 @@ def parse_dict_header(value):
:return: :class:`dict`
:rtype: dict
"""
- pass
-
-
+ result = {}
+ for item in _parse_list_header(value):
+ if "=" not in item:
+ result[item] = None
+ continue
+ name, value = item.split("=", 1)
+ if value[:1] == value[-1:] == '"':
+ value = unquote_header_value(value[1:-1])
+ result[name] = value
+ return result
+
+
+# From mitsuhiko/werkzeug (used with permission).
def unquote_header_value(value, is_filename=False):
- """Unquotes a header value. (Reversal of :func:`quote_header_value`).
+ r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
This does not use the real unquoting but what browsers are actually
using for quoting.
:param value: the header value to unquote.
:rtype: str
"""
- pass
+ if value and value[0] == value[-1] == '"':
+ # this is not the real unquoting, but fixing this so that the
+ # RFC is met will result in bugs with internet explorer and
+ # probably some other browsers as well. IE for example is
+ # uploading files with "C:\foo\bar.txt" as filename
+ value = value[1:-1]
+
+ # if this is a filename and the starting characters look like
+ # a UNC path, then just return the value without quotes. Using the
+ # replace sequence below on a UNC path has the effect of turning
+ # the leading double slash into a single slash and then
+ # _fix_ie_filename() doesn't work correctly. See #458.
+ if not is_filename or value[:2] != "\\\\":
+ return value.replace("\\\\", "\\").replace('\\"', '"')
+ return value
def dict_from_cookiejar(cj):
@@ -181,7 +470,9 @@ def dict_from_cookiejar(cj):
:param cj: CookieJar object to extract cookies from.
:rtype: dict
"""
- pass
+
+ cookie_dict = {cookie.name: cookie.value for cookie in cj}
+ return cookie_dict
def add_dict_to_cookiejar(cj, cookie_dict):
@@ -191,7 +482,8 @@ def add_dict_to_cookiejar(cj, cookie_dict):
:param cookie_dict: Dict of key/values to insert into CookieJar.
:rtype: CookieJar
"""
- pass
+
+ return cookiejar_from_dict(cookie_dict, cj)
def get_encodings_from_content(content):
@@ -199,7 +491,24 @@ def get_encodings_from_content(content):
:param content: bytestring to extract encodings from.
"""
- pass
+ warnings.warn(
+ (
+ "In requests 3.0, get_encodings_from_content will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
+
+ charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I)
+ pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I)
+ xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]')
+
+ return (
+ charset_re.findall(content)
+ + pragma_re.findall(content)
+ + xml_re.findall(content)
+ )
def _parse_content_type_header(header):
@@ -209,7 +518,22 @@ def _parse_content_type_header(header):
:return: tuple containing content type and dictionary of
parameters
"""
- pass
+
+ tokens = header.split(";")
+ content_type, params = tokens[0].strip(), tokens[1:]
+ params_dict = {}
+ items_to_strip = "\"' "
+
+ for param in params:
+ param = param.strip()
+ if param:
+ key, value = param, True
+ index_of_equals = param.find("=")
+ if index_of_equals != -1:
+ key = param[:index_of_equals].strip(items_to_strip)
+ value = param[index_of_equals + 1 :].strip(items_to_strip)
+ params_dict[key.lower()] = value
+ return content_type, params_dict
def get_encoding_from_headers(headers):
@@ -218,17 +542,50 @@ def get_encoding_from_headers(headers):
:param headers: dictionary to extract encoding from.
:rtype: str
"""
- pass
+
+ content_type = headers.get("content-type")
+
+ if not content_type:
+ return None
+
+ content_type, params = _parse_content_type_header(content_type)
+
+ if "charset" in params:
+ return params["charset"].strip("'\"")
+
+ if "text" in content_type:
+ return "ISO-8859-1"
+
+ if "application/json" in content_type:
+ # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset
+ return "utf-8"
def stream_decode_response_unicode(iterator, r):
"""Stream decodes an iterator."""
- pass
+
+ if r.encoding is None:
+ yield from iterator
+ return
+
+ decoder = codecs.getincrementaldecoder(r.encoding)(errors="replace")
+ for chunk in iterator:
+ rv = decoder.decode(chunk)
+ if rv:
+ yield rv
+ rv = decoder.decode(b"", final=True)
+ if rv:
+ yield rv
def iter_slices(string, slice_length):
"""Iterate over slices of a string."""
- pass
+ pos = 0
+ if slice_length is None or slice_length <= 0:
+ slice_length = len(string)
+ while pos < len(string):
+ yield string[pos : pos + slice_length]
+ pos += slice_length
def get_unicode_from_response(r):
@@ -243,11 +600,37 @@ def get_unicode_from_response(r):
:rtype: str
"""
- pass
-
-
+ warnings.warn(
+ (
+ "In requests 3.0, get_unicode_from_response will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
+
+ tried_encodings = []
+
+ # Try charset from content-type
+ encoding = get_encoding_from_headers(r.headers)
+
+ if encoding:
+ try:
+ return str(r.content, encoding)
+ except UnicodeError:
+ tried_encodings.append(encoding)
+
+ # Fall back:
+ try:
+ return str(r.content, encoding, errors="replace")
+ except TypeError:
+ return r.content
+
+
+# The unreserved URI characters (RFC 3986)
UNRESERVED_SET = frozenset(
- 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + '0123456789-._~')
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~"
+)
def unquote_unreserved(uri):
@@ -256,7 +639,22 @@ def unquote_unreserved(uri):
:rtype: str
"""
- pass
+ parts = uri.split("%")
+ for i in range(1, len(parts)):
+ h = parts[i][0:2]
+ if len(h) == 2 and h.isalnum():
+ try:
+ c = chr(int(h, 16))
+ except ValueError:
+ raise InvalidURL(f"Invalid percent-escape sequence: '{h}'")
+
+ if c in UNRESERVED_SET:
+ parts[i] = c + parts[i][2:]
+ else:
+ parts[i] = f"%{parts[i]}"
+ else:
+ parts[i] = f"%{parts[i]}"
+ return "".join(parts)
def requote_uri(uri):
@@ -267,7 +665,18 @@ def requote_uri(uri):
:rtype: str
"""
- pass
+ safe_with_percent = "!#$%&'()*+,/:;=?@[]~"
+ safe_without_percent = "!#$&'()*+,/:;=?@[]~"
+ try:
+ # Unquote only the unreserved characters
+ # Then quote only illegal characters (do not quote reserved,
+ # unreserved, or '%')
+ return quote(unquote_unreserved(uri), safe=safe_with_percent)
+ except InvalidURL:
+ # We couldn't unquote the given URI, so let's try quoting it, but
+ # there may be unquoted '%'s in the URI. We need to make sure they're
+ # properly quoted so they do not cause issues elsewhere.
+ return quote(uri, safe=safe_without_percent)
def address_in_network(ip, net):
@@ -278,7 +687,11 @@ def address_in_network(ip, net):
:rtype: bool
"""
- pass
+ ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0]
+ netaddr, bits = net.split("/")
+ netmask = struct.unpack("=L", socket.inet_aton(dotted_netmask(int(bits))))[0]
+ network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask
+ return (ipaddr & netmask) == (network & netmask)
def dotted_netmask(mask):
@@ -288,14 +701,19 @@ def dotted_netmask(mask):
:rtype: str
"""
- pass
+ bits = 0xFFFFFFFF ^ (1 << 32 - mask) - 1
+ return socket.inet_ntoa(struct.pack(">I", bits))
def is_ipv4_address(string_ip):
"""
:rtype: bool
"""
- pass
+ try:
+ socket.inet_aton(string_ip)
+ except OSError:
+ return False
+ return True
def is_valid_cidr(string_network):
@@ -304,7 +722,22 @@ def is_valid_cidr(string_network):
:rtype: bool
"""
- pass
+ if string_network.count("/") == 1:
+ try:
+ mask = int(string_network.split("/")[1])
+ except ValueError:
+ return False
+
+ if mask < 1 or mask > 32:
+ return False
+
+ try:
+ socket.inet_aton(string_network.split("/")[0])
+ except OSError:
+ return False
+ else:
+ return False
+ return True
@contextlib.contextmanager
@@ -315,7 +748,18 @@ def set_environ(env_name, value):
the environment variable 'env_name'.
If 'value' is None, do nothing"""
- pass
+ value_changed = value is not None
+ if value_changed:
+ old_value = os.environ.get(env_name)
+ os.environ[env_name] = value
+ try:
+ yield
+ finally:
+ if value_changed:
+ if old_value is None:
+ del os.environ[env_name]
+ else:
+ os.environ[env_name] = old_value
def should_bypass_proxies(url, no_proxy):
@@ -324,7 +768,59 @@ def should_bypass_proxies(url, no_proxy):
:rtype: bool
"""
- pass
+
+ # Prioritize lowercase environment variables over uppercase
+ # to keep a consistent behaviour with other http projects (curl, wget).
+ def get_proxy(key):
+ return os.environ.get(key) or os.environ.get(key.upper())
+
+ # First check whether no_proxy is defined. If it is, check that the URL
+ # we're getting isn't in the no_proxy list.
+ no_proxy_arg = no_proxy
+ if no_proxy is None:
+ no_proxy = get_proxy("no_proxy")
+ parsed = urlparse(url)
+
+ if parsed.hostname is None:
+ # URLs don't always have hostnames, e.g. file:/// urls.
+ return True
+
+ if no_proxy:
+ # We need to check whether we match here. We need to see if we match
+ # the end of the hostname, both with and without the port.
+ no_proxy = (host for host in no_proxy.replace(" ", "").split(",") if host)
+
+ if is_ipv4_address(parsed.hostname):
+ for proxy_ip in no_proxy:
+ if is_valid_cidr(proxy_ip):
+ if address_in_network(parsed.hostname, proxy_ip):
+ return True
+ elif parsed.hostname == proxy_ip:
+ # If no_proxy ip was defined in plain IP notation instead of cidr notation &
+ # matches the IP of the index
+ return True
+ else:
+ host_with_port = parsed.hostname
+ if parsed.port:
+ host_with_port += f":{parsed.port}"
+
+ for host in no_proxy:
+ if parsed.hostname.endswith(host) or host_with_port.endswith(host):
+ # The URL does match something in no_proxy, so we don't want
+ # to apply the proxies on this URL.
+ return True
+
+ with set_environ("no_proxy", no_proxy_arg):
+ # parsed.hostname can be `None` in cases such as a file URI.
+ try:
+ bypass = proxy_bypass(parsed.hostname)
+ except (TypeError, socket.gaierror):
+ bypass = False
+
+ if bypass:
+ return True
+
+ return False
def get_environ_proxies(url, no_proxy=None):
@@ -333,7 +829,10 @@ def get_environ_proxies(url, no_proxy=None):
:rtype: dict
"""
- pass
+ if should_bypass_proxies(url, no_proxy=no_proxy):
+ return {}
+ else:
+ return getproxies()
def select_proxy(url, proxies):
@@ -342,7 +841,24 @@ def select_proxy(url, proxies):
:param url: The url being for the request
:param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
"""
- pass
+ proxies = proxies or {}
+ urlparts = urlparse(url)
+ if urlparts.hostname is None:
+ return proxies.get(urlparts.scheme, proxies.get("all"))
+
+ proxy_keys = [
+ urlparts.scheme + "://" + urlparts.hostname,
+ urlparts.scheme,
+ "all://" + urlparts.hostname,
+ "all",
+ ]
+ proxy = None
+ for proxy_key in proxy_keys:
+ if proxy_key in proxies:
+ proxy = proxies[proxy_key]
+ break
+
+ return proxy
def resolve_proxies(request, proxies, trust_env=True):
@@ -356,23 +872,43 @@ def resolve_proxies(request, proxies, trust_env=True):
:rtype: dict
"""
- pass
+ proxies = proxies if proxies is not None else {}
+ url = request.url
+ scheme = urlparse(url).scheme
+ no_proxy = proxies.get("no_proxy")
+ new_proxies = proxies.copy()
+ if trust_env and not should_bypass_proxies(url, no_proxy=no_proxy):
+ environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)
-def default_user_agent(name='python-requests'):
+ proxy = environ_proxies.get(scheme, environ_proxies.get("all"))
+
+ if proxy:
+ new_proxies.setdefault(scheme, proxy)
+ return new_proxies
+
+
+def default_user_agent(name="python-requests"):
"""
Return a string representing the default user agent.
:rtype: str
"""
- pass
+ return f"{name}/{__version__}"
def default_headers():
"""
:rtype: requests.structures.CaseInsensitiveDict
"""
- pass
+ return CaseInsensitiveDict(
+ {
+ "User-Agent": default_user_agent(),
+ "Accept-Encoding": DEFAULT_ACCEPT_ENCODING,
+ "Accept": "*/*",
+ "Connection": "keep-alive",
+ }
+ )
def parse_header_links(value):
@@ -382,10 +918,38 @@ def parse_header_links(value):
:rtype: list
"""
- pass
+ links = []
+
+ replace_chars = " '\""
+
+ value = value.strip(replace_chars)
+ if not value:
+ return links
+
+ for val in re.split(", *<", value):
+ try:
+ url, params = val.split(";", 1)
+ except ValueError:
+ url, params = val, ""
+
+ link = {"url": url.strip("<> '\"")}
-_null = '\x00'.encode('ascii')
+ for param in params.split(";"):
+ try:
+ key, value = param.split("=")
+ except ValueError:
+ break
+
+ link[key.strip(replace_chars)] = value.strip(replace_chars)
+
+ links.append(link)
+
+ return links
+
+
+# Null bytes; no need to recreate these on each call to guess_json_utf
+_null = "\x00".encode("ascii") # encoding to ASCII for Python 3
_null2 = _null * 2
_null3 = _null * 3
@@ -394,7 +958,32 @@ def guess_json_utf(data):
"""
:rtype: str
"""
- pass
+ # JSON always starts with two ASCII characters, so detection is as
+ # easy as counting the nulls and from their location and count
+ # determine the encoding. Also detect a BOM, if present.
+ sample = data[:4]
+ if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE):
+ return "utf-32" # BOM included
+ if sample[:3] == codecs.BOM_UTF8:
+ return "utf-8-sig" # BOM included, MS style (discouraged)
+ if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE):
+ return "utf-16" # BOM included
+ nullcount = sample.count(_null)
+ if nullcount == 0:
+ return "utf-8"
+ if nullcount == 2:
+ if sample[::2] == _null2: # 1st and 3rd are null
+ return "utf-16-be"
+ if sample[1::2] == _null2: # 2nd and 4th are null
+ return "utf-16-le"
+ # Did not detect 2 valid UTF-16 ascii-range characters
+ if nullcount == 3:
+ if sample[:3] == _null3:
+ return "utf-32-be"
+ if sample[1:] == _null3:
+ return "utf-32-le"
+ # Did not detect a valid UTF-32 ascii-range character
+ return None
def prepend_scheme_if_needed(url, new_scheme):
@@ -403,7 +992,27 @@ def prepend_scheme_if_needed(url, new_scheme):
:rtype: str
"""
- pass
+ parsed = parse_url(url)
+ scheme, auth, host, port, path, query, fragment = parsed
+
+ # A defect in urlparse determines that there isn't a netloc present in some
+ # urls. We previously assumed parsing was overly cautious, and swapped the
+ # netloc and path. Due to a lack of tests on the original defect, this is
+ # maintained with parse_url for backwards compatibility.
+ netloc = parsed.netloc
+ if not netloc:
+ netloc, path = path, netloc
+
+ if auth:
+ # parse_url doesn't provide the netloc with auth
+ # so we'll add it ourselves.
+ netloc = "@".join([auth, netloc])
+ if scheme is None:
+ scheme = new_scheme
+ if path is None:
+ path = ""
+
+ return urlunparse((scheme, netloc, path, "", query, fragment))
def get_auth_from_url(url):
@@ -412,7 +1021,14 @@ def get_auth_from_url(url):
:rtype: (str,str)
"""
- pass
+ parsed = urlparse(url)
+
+ try:
+ auth = (unquote(parsed.username), unquote(parsed.password))
+ except (AttributeError, TypeError):
+ auth = ("", "")
+
+ return auth
def check_header_validity(header):
@@ -421,7 +1037,28 @@ def check_header_validity(header):
:param header: tuple, in the format (name, value).
"""
- pass
+ name, value = header
+ _validate_header_part(header, name, 0)
+ _validate_header_part(header, value, 1)
+
+
+def _validate_header_part(header, header_part, header_validator_index):
+ if isinstance(header_part, str):
+ validator = _HEADER_VALIDATORS_STR[header_validator_index]
+ elif isinstance(header_part, bytes):
+ validator = _HEADER_VALIDATORS_BYTE[header_validator_index]
+ else:
+ raise InvalidHeader(
+ f"Header part ({header_part!r}) from {header} "
+ f"must be of type str or bytes, not {type(header_part)}"
+ )
+
+ if not validator.match(header_part):
+ header_kind = "name" if header_validator_index == 0 else "value"
+ raise InvalidHeader(
+ f"Invalid leading whitespace, reserved character(s), or return "
+ f"character(s) in header {header_kind}: {header_part!r}"
+ )
def urldefragauth(url):
@@ -430,11 +1067,30 @@ def urldefragauth(url):
:rtype: str
"""
- pass
+ scheme, netloc, path, params, query, fragment = urlparse(url)
+
+ # see func:`prepend_scheme_if_needed`
+ if not netloc:
+ netloc, path = path, netloc
+
+ netloc = netloc.rsplit("@", 1)[-1]
+
+ return urlunparse((scheme, netloc, path, params, query, ""))
def rewind_body(prepared_request):
"""Move file pointer back to its recorded starting position
so it can be read again on redirect.
"""
- pass
+ body_seek = getattr(prepared_request.body, "seek", None)
+ if body_seek is not None and isinstance(
+ prepared_request._body_position, integer_types
+ ):
+ try:
+ body_seek(prepared_request._body_position)
+ except OSError:
+ raise UnrewindableBodyError(
+ "An error occurred when rewinding request body for redirect."
+ )
+ else:
+ raise UnrewindableBodyError("Unable to rewind request body for redirect.")