jpayne@7: """ jpayne@7: requests.sessions jpayne@7: ~~~~~~~~~~~~~~~~~ jpayne@7: jpayne@7: This module provides a Session object to manage and persist settings across jpayne@7: requests (cookies, auth, proxies). jpayne@7: """ jpayne@7: import os jpayne@7: import sys jpayne@7: import time jpayne@7: from collections import OrderedDict jpayne@7: from datetime import timedelta jpayne@7: jpayne@7: from ._internal_utils import to_native_string jpayne@7: from .adapters import HTTPAdapter jpayne@7: from .auth import _basic_auth_str jpayne@7: from .compat import Mapping, cookielib, urljoin, urlparse jpayne@7: from .cookies import ( jpayne@7: RequestsCookieJar, jpayne@7: cookiejar_from_dict, jpayne@7: extract_cookies_to_jar, jpayne@7: merge_cookies, jpayne@7: ) jpayne@7: from .exceptions import ( jpayne@7: ChunkedEncodingError, jpayne@7: ContentDecodingError, jpayne@7: InvalidSchema, jpayne@7: TooManyRedirects, jpayne@7: ) jpayne@7: from .hooks import default_hooks, dispatch_hook jpayne@7: jpayne@7: # formerly defined here, reexposed here for backward compatibility jpayne@7: from .models import ( # noqa: F401 jpayne@7: DEFAULT_REDIRECT_LIMIT, jpayne@7: REDIRECT_STATI, jpayne@7: PreparedRequest, jpayne@7: Request, jpayne@7: ) jpayne@7: from .status_codes import codes jpayne@7: from .structures import CaseInsensitiveDict jpayne@7: from .utils import ( # noqa: F401 jpayne@7: DEFAULT_PORTS, jpayne@7: default_headers, jpayne@7: get_auth_from_url, jpayne@7: get_environ_proxies, jpayne@7: get_netrc_auth, jpayne@7: requote_uri, jpayne@7: resolve_proxies, jpayne@7: rewind_body, jpayne@7: should_bypass_proxies, jpayne@7: to_key_val_list, jpayne@7: ) jpayne@7: jpayne@7: # Preferred clock, based on which one is more accurate on a given system. jpayne@7: if sys.platform == "win32": jpayne@7: preferred_clock = time.perf_counter jpayne@7: else: jpayne@7: preferred_clock = time.time jpayne@7: jpayne@7: jpayne@7: def merge_setting(request_setting, session_setting, dict_class=OrderedDict): jpayne@7: """Determines appropriate setting for a given request, taking into account jpayne@7: the explicit setting on that request, and the setting in the session. If a jpayne@7: setting is a dictionary, they will be merged together using `dict_class` jpayne@7: """ jpayne@7: jpayne@7: if session_setting is None: jpayne@7: return request_setting jpayne@7: jpayne@7: if request_setting is None: jpayne@7: return session_setting jpayne@7: jpayne@7: # Bypass if not a dictionary (e.g. verify) jpayne@7: if not ( jpayne@7: isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping) jpayne@7: ): jpayne@7: return request_setting jpayne@7: jpayne@7: merged_setting = dict_class(to_key_val_list(session_setting)) jpayne@7: merged_setting.update(to_key_val_list(request_setting)) jpayne@7: jpayne@7: # Remove keys that are set to None. Extract keys first to avoid altering jpayne@7: # the dictionary during iteration. jpayne@7: none_keys = [k for (k, v) in merged_setting.items() if v is None] jpayne@7: for key in none_keys: jpayne@7: del merged_setting[key] jpayne@7: jpayne@7: return merged_setting jpayne@7: jpayne@7: jpayne@7: def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): jpayne@7: """Properly merges both requests and session hooks. jpayne@7: jpayne@7: This is necessary because when request_hooks == {'response': []}, the jpayne@7: merge breaks Session hooks entirely. jpayne@7: """ jpayne@7: if session_hooks is None or session_hooks.get("response") == []: jpayne@7: return request_hooks jpayne@7: jpayne@7: if request_hooks is None or request_hooks.get("response") == []: jpayne@7: return session_hooks jpayne@7: jpayne@7: return merge_setting(request_hooks, session_hooks, dict_class) jpayne@7: jpayne@7: jpayne@7: class SessionRedirectMixin: jpayne@7: def get_redirect_target(self, resp): jpayne@7: """Receives a Response. Returns a redirect URI or ``None``""" jpayne@7: # Due to the nature of how requests processes redirects this method will jpayne@7: # be called at least once upon the original response and at least twice jpayne@7: # on each subsequent redirect response (if any). jpayne@7: # If a custom mixin is used to handle this logic, it may be advantageous jpayne@7: # to cache the redirect location onto the response object as a private jpayne@7: # attribute. jpayne@7: if resp.is_redirect: jpayne@7: location = resp.headers["location"] jpayne@7: # Currently the underlying http module on py3 decode headers jpayne@7: # in latin1, but empirical evidence suggests that latin1 is very jpayne@7: # rarely used with non-ASCII characters in HTTP headers. jpayne@7: # It is more likely to get UTF8 header rather than latin1. jpayne@7: # This causes incorrect handling of UTF8 encoded location headers. jpayne@7: # To solve this, we re-encode the location in latin1. jpayne@7: location = location.encode("latin1") jpayne@7: return to_native_string(location, "utf8") jpayne@7: return None jpayne@7: jpayne@7: def should_strip_auth(self, old_url, new_url): jpayne@7: """Decide whether Authorization header should be removed when redirecting""" jpayne@7: old_parsed = urlparse(old_url) jpayne@7: new_parsed = urlparse(new_url) jpayne@7: if old_parsed.hostname != new_parsed.hostname: jpayne@7: return True jpayne@7: # Special case: allow http -> https redirect when using the standard jpayne@7: # ports. This isn't specified by RFC 7235, but is kept to avoid jpayne@7: # breaking backwards compatibility with older versions of requests jpayne@7: # that allowed any redirects on the same host. jpayne@7: if ( jpayne@7: old_parsed.scheme == "http" jpayne@7: and old_parsed.port in (80, None) jpayne@7: and new_parsed.scheme == "https" jpayne@7: and new_parsed.port in (443, None) jpayne@7: ): jpayne@7: return False jpayne@7: jpayne@7: # Handle default port usage corresponding to scheme. jpayne@7: changed_port = old_parsed.port != new_parsed.port jpayne@7: changed_scheme = old_parsed.scheme != new_parsed.scheme jpayne@7: default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None) jpayne@7: if ( jpayne@7: not changed_scheme jpayne@7: and old_parsed.port in default_port jpayne@7: and new_parsed.port in default_port jpayne@7: ): jpayne@7: return False jpayne@7: jpayne@7: # Standard case: root URI must match jpayne@7: return changed_port or changed_scheme jpayne@7: jpayne@7: def resolve_redirects( jpayne@7: self, jpayne@7: resp, jpayne@7: req, jpayne@7: stream=False, jpayne@7: timeout=None, jpayne@7: verify=True, jpayne@7: cert=None, jpayne@7: proxies=None, jpayne@7: yield_requests=False, jpayne@7: **adapter_kwargs, jpayne@7: ): jpayne@7: """Receives a Response. Returns a generator of Responses or Requests.""" jpayne@7: jpayne@7: hist = [] # keep track of history jpayne@7: jpayne@7: url = self.get_redirect_target(resp) jpayne@7: previous_fragment = urlparse(req.url).fragment jpayne@7: while url: jpayne@7: prepared_request = req.copy() jpayne@7: jpayne@7: # Update history and keep track of redirects. jpayne@7: # resp.history must ignore the original request in this loop jpayne@7: hist.append(resp) jpayne@7: resp.history = hist[1:] jpayne@7: jpayne@7: try: jpayne@7: resp.content # Consume socket so it can be released jpayne@7: except (ChunkedEncodingError, ContentDecodingError, RuntimeError): jpayne@7: resp.raw.read(decode_content=False) jpayne@7: jpayne@7: if len(resp.history) >= self.max_redirects: jpayne@7: raise TooManyRedirects( jpayne@7: f"Exceeded {self.max_redirects} redirects.", response=resp jpayne@7: ) jpayne@7: jpayne@7: # Release the connection back into the pool. jpayne@7: resp.close() jpayne@7: jpayne@7: # Handle redirection without scheme (see: RFC 1808 Section 4) jpayne@7: if url.startswith("//"): jpayne@7: parsed_rurl = urlparse(resp.url) jpayne@7: url = ":".join([to_native_string(parsed_rurl.scheme), url]) jpayne@7: jpayne@7: # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2) jpayne@7: parsed = urlparse(url) jpayne@7: if parsed.fragment == "" and previous_fragment: jpayne@7: parsed = parsed._replace(fragment=previous_fragment) jpayne@7: elif parsed.fragment: jpayne@7: previous_fragment = parsed.fragment jpayne@7: url = parsed.geturl() jpayne@7: jpayne@7: # Facilitate relative 'location' headers, as allowed by RFC 7231. jpayne@7: # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') jpayne@7: # Compliant with RFC3986, we percent encode the url. jpayne@7: if not parsed.netloc: jpayne@7: url = urljoin(resp.url, requote_uri(url)) jpayne@7: else: jpayne@7: url = requote_uri(url) jpayne@7: jpayne@7: prepared_request.url = to_native_string(url) jpayne@7: jpayne@7: self.rebuild_method(prepared_request, resp) jpayne@7: jpayne@7: # https://github.com/psf/requests/issues/1084 jpayne@7: if resp.status_code not in ( jpayne@7: codes.temporary_redirect, jpayne@7: codes.permanent_redirect, jpayne@7: ): jpayne@7: # https://github.com/psf/requests/issues/3490 jpayne@7: purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding") jpayne@7: for header in purged_headers: jpayne@7: prepared_request.headers.pop(header, None) jpayne@7: prepared_request.body = None jpayne@7: jpayne@7: headers = prepared_request.headers jpayne@7: headers.pop("Cookie", None) jpayne@7: jpayne@7: # Extract any cookies sent on the response to the cookiejar jpayne@7: # in the new request. Because we've mutated our copied prepared jpayne@7: # request, use the old one that we haven't yet touched. jpayne@7: extract_cookies_to_jar(prepared_request._cookies, req, resp.raw) jpayne@7: merge_cookies(prepared_request._cookies, self.cookies) jpayne@7: prepared_request.prepare_cookies(prepared_request._cookies) jpayne@7: jpayne@7: # Rebuild auth and proxy information. jpayne@7: proxies = self.rebuild_proxies(prepared_request, proxies) jpayne@7: self.rebuild_auth(prepared_request, resp) jpayne@7: jpayne@7: # A failed tell() sets `_body_position` to `object()`. This non-None jpayne@7: # value ensures `rewindable` will be True, allowing us to raise an jpayne@7: # UnrewindableBodyError, instead of hanging the connection. jpayne@7: rewindable = prepared_request._body_position is not None and ( jpayne@7: "Content-Length" in headers or "Transfer-Encoding" in headers jpayne@7: ) jpayne@7: jpayne@7: # Attempt to rewind consumed file-like object. jpayne@7: if rewindable: jpayne@7: rewind_body(prepared_request) jpayne@7: jpayne@7: # Override the original request. jpayne@7: req = prepared_request jpayne@7: jpayne@7: if yield_requests: jpayne@7: yield req jpayne@7: else: jpayne@7: jpayne@7: resp = self.send( jpayne@7: req, jpayne@7: stream=stream, jpayne@7: timeout=timeout, jpayne@7: verify=verify, jpayne@7: cert=cert, jpayne@7: proxies=proxies, jpayne@7: allow_redirects=False, jpayne@7: **adapter_kwargs, jpayne@7: ) jpayne@7: jpayne@7: extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) jpayne@7: jpayne@7: # extract redirect url, if any, for the next loop jpayne@7: url = self.get_redirect_target(resp) jpayne@7: yield resp jpayne@7: jpayne@7: def rebuild_auth(self, prepared_request, response): jpayne@7: """When being redirected we may want to strip authentication from the jpayne@7: request to avoid leaking credentials. This method intelligently removes jpayne@7: and reapplies authentication where possible to avoid credential loss. jpayne@7: """ jpayne@7: headers = prepared_request.headers jpayne@7: url = prepared_request.url jpayne@7: jpayne@7: if "Authorization" in headers and self.should_strip_auth( jpayne@7: response.request.url, url jpayne@7: ): jpayne@7: # If we get redirected to a new host, we should strip out any jpayne@7: # authentication headers. jpayne@7: del headers["Authorization"] jpayne@7: jpayne@7: # .netrc might have more auth for us on our new host. jpayne@7: new_auth = get_netrc_auth(url) if self.trust_env else None jpayne@7: if new_auth is not None: jpayne@7: prepared_request.prepare_auth(new_auth) jpayne@7: jpayne@7: def rebuild_proxies(self, prepared_request, proxies): jpayne@7: """This method re-evaluates the proxy configuration by considering the jpayne@7: environment variables. If we are redirected to a URL covered by jpayne@7: NO_PROXY, we strip the proxy configuration. Otherwise, we set missing jpayne@7: proxy keys for this URL (in case they were stripped by a previous jpayne@7: redirect). jpayne@7: jpayne@7: This method also replaces the Proxy-Authorization header where jpayne@7: necessary. jpayne@7: jpayne@7: :rtype: dict jpayne@7: """ jpayne@7: headers = prepared_request.headers jpayne@7: scheme = urlparse(prepared_request.url).scheme jpayne@7: new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env) jpayne@7: jpayne@7: if "Proxy-Authorization" in headers: jpayne@7: del headers["Proxy-Authorization"] jpayne@7: jpayne@7: try: jpayne@7: username, password = get_auth_from_url(new_proxies[scheme]) jpayne@7: except KeyError: jpayne@7: username, password = None, None jpayne@7: jpayne@7: # urllib3 handles proxy authorization for us in the standard adapter. jpayne@7: # Avoid appending this to TLS tunneled requests where it may be leaked. jpayne@7: if not scheme.startswith('https') and username and password: jpayne@7: headers["Proxy-Authorization"] = _basic_auth_str(username, password) jpayne@7: jpayne@7: return new_proxies jpayne@7: jpayne@7: def rebuild_method(self, prepared_request, response): jpayne@7: """When being redirected we may want to change the method of the request jpayne@7: based on certain specs or browser behavior. jpayne@7: """ jpayne@7: method = prepared_request.method jpayne@7: jpayne@7: # https://tools.ietf.org/html/rfc7231#section-6.4.4 jpayne@7: if response.status_code == codes.see_other and method != "HEAD": jpayne@7: method = "GET" jpayne@7: jpayne@7: # Do what the browsers do, despite standards... jpayne@7: # First, turn 302s into GETs. jpayne@7: if response.status_code == codes.found and method != "HEAD": jpayne@7: method = "GET" jpayne@7: jpayne@7: # Second, if a POST is responded to with a 301, turn it into a GET. jpayne@7: # This bizarre behaviour is explained in Issue 1704. jpayne@7: if response.status_code == codes.moved and method == "POST": jpayne@7: method = "GET" jpayne@7: jpayne@7: prepared_request.method = method jpayne@7: jpayne@7: jpayne@7: class Session(SessionRedirectMixin): jpayne@7: """A Requests session. jpayne@7: jpayne@7: Provides cookie persistence, connection-pooling, and configuration. jpayne@7: jpayne@7: Basic Usage:: jpayne@7: jpayne@7: >>> import requests jpayne@7: >>> s = requests.Session() jpayne@7: >>> s.get('https://httpbin.org/get') jpayne@7: jpayne@7: jpayne@7: Or as a context manager:: jpayne@7: jpayne@7: >>> with requests.Session() as s: jpayne@7: ... s.get('https://httpbin.org/get') jpayne@7: jpayne@7: """ jpayne@7: jpayne@7: __attrs__ = [ jpayne@7: "headers", jpayne@7: "cookies", jpayne@7: "auth", jpayne@7: "proxies", jpayne@7: "hooks", jpayne@7: "params", jpayne@7: "verify", jpayne@7: "cert", jpayne@7: "adapters", jpayne@7: "stream", jpayne@7: "trust_env", jpayne@7: "max_redirects", jpayne@7: ] jpayne@7: jpayne@7: def __init__(self): jpayne@7: jpayne@7: #: A case-insensitive dictionary of headers to be sent on each jpayne@7: #: :class:`Request ` sent from this jpayne@7: #: :class:`Session `. jpayne@7: self.headers = default_headers() jpayne@7: jpayne@7: #: Default Authentication tuple or object to attach to jpayne@7: #: :class:`Request `. jpayne@7: self.auth = None jpayne@7: jpayne@7: #: Dictionary mapping protocol or protocol and host to the URL of the proxy jpayne@7: #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to jpayne@7: #: be used on each :class:`Request `. jpayne@7: self.proxies = {} jpayne@7: jpayne@7: #: Event-handling hooks. jpayne@7: self.hooks = default_hooks() jpayne@7: jpayne@7: #: Dictionary of querystring data to attach to each jpayne@7: #: :class:`Request `. The dictionary values may be lists for jpayne@7: #: representing multivalued query parameters. jpayne@7: self.params = {} jpayne@7: jpayne@7: #: Stream response content default. jpayne@7: self.stream = False jpayne@7: jpayne@7: #: SSL Verification default. jpayne@7: #: Defaults to `True`, requiring requests to verify the TLS certificate at the jpayne@7: #: remote end. jpayne@7: #: If verify is set to `False`, requests will accept any TLS certificate jpayne@7: #: presented by the server, and will ignore hostname mismatches and/or jpayne@7: #: expired certificates, which will make your application vulnerable to jpayne@7: #: man-in-the-middle (MitM) attacks. jpayne@7: #: Only set this to `False` for testing. jpayne@7: self.verify = True jpayne@7: jpayne@7: #: SSL client certificate default, if String, path to ssl client jpayne@7: #: cert file (.pem). If Tuple, ('cert', 'key') pair. jpayne@7: self.cert = None jpayne@7: jpayne@7: #: Maximum number of redirects allowed. If the request exceeds this jpayne@7: #: limit, a :class:`TooManyRedirects` exception is raised. jpayne@7: #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is jpayne@7: #: 30. jpayne@7: self.max_redirects = DEFAULT_REDIRECT_LIMIT jpayne@7: jpayne@7: #: Trust environment settings for proxy configuration, default jpayne@7: #: authentication and similar. jpayne@7: self.trust_env = True jpayne@7: jpayne@7: #: A CookieJar containing all currently outstanding cookies set on this jpayne@7: #: session. By default it is a jpayne@7: #: :class:`RequestsCookieJar `, but jpayne@7: #: may be any other ``cookielib.CookieJar`` compatible object. jpayne@7: self.cookies = cookiejar_from_dict({}) jpayne@7: jpayne@7: # Default connection adapters. jpayne@7: self.adapters = OrderedDict() jpayne@7: self.mount("https://", HTTPAdapter()) jpayne@7: self.mount("http://", HTTPAdapter()) jpayne@7: jpayne@7: def __enter__(self): jpayne@7: return self jpayne@7: jpayne@7: def __exit__(self, *args): jpayne@7: self.close() jpayne@7: jpayne@7: def prepare_request(self, request): jpayne@7: """Constructs a :class:`PreparedRequest ` for jpayne@7: transmission and returns it. The :class:`PreparedRequest` has settings jpayne@7: merged from the :class:`Request ` instance and those of the jpayne@7: :class:`Session`. jpayne@7: jpayne@7: :param request: :class:`Request` instance to prepare with this jpayne@7: session's settings. jpayne@7: :rtype: requests.PreparedRequest jpayne@7: """ jpayne@7: cookies = request.cookies or {} jpayne@7: jpayne@7: # Bootstrap CookieJar. jpayne@7: if not isinstance(cookies, cookielib.CookieJar): jpayne@7: cookies = cookiejar_from_dict(cookies) jpayne@7: jpayne@7: # Merge with session cookies jpayne@7: merged_cookies = merge_cookies( jpayne@7: merge_cookies(RequestsCookieJar(), self.cookies), cookies jpayne@7: ) jpayne@7: jpayne@7: # Set environment's basic authentication if not explicitly set. jpayne@7: auth = request.auth jpayne@7: if self.trust_env and not auth and not self.auth: jpayne@7: auth = get_netrc_auth(request.url) jpayne@7: jpayne@7: p = PreparedRequest() jpayne@7: p.prepare( jpayne@7: method=request.method.upper(), jpayne@7: url=request.url, jpayne@7: files=request.files, jpayne@7: data=request.data, jpayne@7: json=request.json, jpayne@7: headers=merge_setting( jpayne@7: request.headers, self.headers, dict_class=CaseInsensitiveDict jpayne@7: ), jpayne@7: params=merge_setting(request.params, self.params), jpayne@7: auth=merge_setting(auth, self.auth), jpayne@7: cookies=merged_cookies, jpayne@7: hooks=merge_hooks(request.hooks, self.hooks), jpayne@7: ) jpayne@7: return p jpayne@7: jpayne@7: def request( jpayne@7: self, jpayne@7: method, jpayne@7: url, jpayne@7: params=None, jpayne@7: data=None, jpayne@7: headers=None, jpayne@7: cookies=None, jpayne@7: files=None, jpayne@7: auth=None, jpayne@7: timeout=None, jpayne@7: allow_redirects=True, jpayne@7: proxies=None, jpayne@7: hooks=None, jpayne@7: stream=None, jpayne@7: verify=None, jpayne@7: cert=None, jpayne@7: json=None, jpayne@7: ): jpayne@7: """Constructs a :class:`Request `, prepares it and sends it. jpayne@7: Returns :class:`Response ` object. jpayne@7: jpayne@7: :param method: method for the new :class:`Request` object. jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param params: (optional) Dictionary or bytes to be sent in the query jpayne@7: string for the :class:`Request`. jpayne@7: :param data: (optional) Dictionary, list of tuples, bytes, or file-like jpayne@7: object to send in the body of the :class:`Request`. jpayne@7: :param json: (optional) json to send in the body of the jpayne@7: :class:`Request`. jpayne@7: :param headers: (optional) Dictionary of HTTP Headers to send with the jpayne@7: :class:`Request`. jpayne@7: :param cookies: (optional) Dict or CookieJar object to send with the jpayne@7: :class:`Request`. jpayne@7: :param files: (optional) Dictionary of ``'filename': file-like-objects`` jpayne@7: for multipart encoding upload. jpayne@7: :param auth: (optional) Auth tuple or callable to enable jpayne@7: Basic/Digest/Custom HTTP Auth. jpayne@7: :param timeout: (optional) How long to wait for the server to send jpayne@7: data before giving up, as a float, or a :ref:`(connect timeout, jpayne@7: read timeout) ` tuple. jpayne@7: :type timeout: float or tuple jpayne@7: :param allow_redirects: (optional) Set to True by default. jpayne@7: :type allow_redirects: bool jpayne@7: :param proxies: (optional) Dictionary mapping protocol or protocol and jpayne@7: hostname to the URL of the proxy. jpayne@7: :param stream: (optional) whether to immediately download the response jpayne@7: content. Defaults to ``False``. jpayne@7: :param verify: (optional) Either a boolean, in which case it controls whether we verify jpayne@7: the server's TLS certificate, or a string, in which case it must be a path jpayne@7: to a CA bundle to use. Defaults to ``True``. When set to jpayne@7: ``False``, requests will accept any TLS certificate presented by jpayne@7: the server, and will ignore hostname mismatches and/or expired jpayne@7: certificates, which will make your application vulnerable to jpayne@7: man-in-the-middle (MitM) attacks. Setting verify to ``False`` jpayne@7: may be useful during local development or testing. jpayne@7: :param cert: (optional) if String, path to ssl client cert file (.pem). jpayne@7: If Tuple, ('cert', 'key') pair. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: # Create the Request. jpayne@7: req = Request( jpayne@7: method=method.upper(), jpayne@7: url=url, jpayne@7: headers=headers, jpayne@7: files=files, jpayne@7: data=data or {}, jpayne@7: json=json, jpayne@7: params=params or {}, jpayne@7: auth=auth, jpayne@7: cookies=cookies, jpayne@7: hooks=hooks, jpayne@7: ) jpayne@7: prep = self.prepare_request(req) jpayne@7: jpayne@7: proxies = proxies or {} jpayne@7: jpayne@7: settings = self.merge_environment_settings( jpayne@7: prep.url, proxies, stream, verify, cert jpayne@7: ) jpayne@7: jpayne@7: # Send the request. jpayne@7: send_kwargs = { jpayne@7: "timeout": timeout, jpayne@7: "allow_redirects": allow_redirects, jpayne@7: } jpayne@7: send_kwargs.update(settings) jpayne@7: resp = self.send(prep, **send_kwargs) jpayne@7: jpayne@7: return resp jpayne@7: jpayne@7: def get(self, url, **kwargs): jpayne@7: r"""Sends a GET request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: kwargs.setdefault("allow_redirects", True) jpayne@7: return self.request("GET", url, **kwargs) jpayne@7: jpayne@7: def options(self, url, **kwargs): jpayne@7: r"""Sends a OPTIONS request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: kwargs.setdefault("allow_redirects", True) jpayne@7: return self.request("OPTIONS", url, **kwargs) jpayne@7: jpayne@7: def head(self, url, **kwargs): jpayne@7: r"""Sends a HEAD request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: kwargs.setdefault("allow_redirects", False) jpayne@7: return self.request("HEAD", url, **kwargs) jpayne@7: jpayne@7: def post(self, url, data=None, json=None, **kwargs): jpayne@7: r"""Sends a POST request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param data: (optional) Dictionary, list of tuples, bytes, or file-like jpayne@7: object to send in the body of the :class:`Request`. jpayne@7: :param json: (optional) json to send in the body of the :class:`Request`. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: return self.request("POST", url, data=data, json=json, **kwargs) jpayne@7: jpayne@7: def put(self, url, data=None, **kwargs): jpayne@7: r"""Sends a PUT request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param data: (optional) Dictionary, list of tuples, bytes, or file-like jpayne@7: object to send in the body of the :class:`Request`. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: return self.request("PUT", url, data=data, **kwargs) jpayne@7: jpayne@7: def patch(self, url, data=None, **kwargs): jpayne@7: r"""Sends a PATCH request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param data: (optional) Dictionary, list of tuples, bytes, or file-like jpayne@7: object to send in the body of the :class:`Request`. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: return self.request("PATCH", url, data=data, **kwargs) jpayne@7: jpayne@7: def delete(self, url, **kwargs): jpayne@7: r"""Sends a DELETE request. Returns :class:`Response` object. jpayne@7: jpayne@7: :param url: URL for the new :class:`Request` object. jpayne@7: :param \*\*kwargs: Optional arguments that ``request`` takes. jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: jpayne@7: return self.request("DELETE", url, **kwargs) jpayne@7: jpayne@7: def send(self, request, **kwargs): jpayne@7: """Send a given PreparedRequest. jpayne@7: jpayne@7: :rtype: requests.Response jpayne@7: """ jpayne@7: # Set defaults that the hooks can utilize to ensure they always have jpayne@7: # the correct parameters to reproduce the previous request. jpayne@7: kwargs.setdefault("stream", self.stream) jpayne@7: kwargs.setdefault("verify", self.verify) jpayne@7: kwargs.setdefault("cert", self.cert) jpayne@7: if "proxies" not in kwargs: jpayne@7: kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env) jpayne@7: jpayne@7: # It's possible that users might accidentally send a Request object. jpayne@7: # Guard against that specific failure case. jpayne@7: if isinstance(request, Request): jpayne@7: raise ValueError("You can only send PreparedRequests.") jpayne@7: jpayne@7: # Set up variables needed for resolve_redirects and dispatching of hooks jpayne@7: allow_redirects = kwargs.pop("allow_redirects", True) jpayne@7: stream = kwargs.get("stream") jpayne@7: hooks = request.hooks jpayne@7: jpayne@7: # Get the appropriate adapter to use jpayne@7: adapter = self.get_adapter(url=request.url) jpayne@7: jpayne@7: # Start time (approximately) of the request jpayne@7: start = preferred_clock() jpayne@7: jpayne@7: # Send the request jpayne@7: r = adapter.send(request, **kwargs) jpayne@7: jpayne@7: # Total elapsed time of the request (approximately) jpayne@7: elapsed = preferred_clock() - start jpayne@7: r.elapsed = timedelta(seconds=elapsed) jpayne@7: jpayne@7: # Response manipulation hooks jpayne@7: r = dispatch_hook("response", hooks, r, **kwargs) jpayne@7: jpayne@7: # Persist cookies jpayne@7: if r.history: jpayne@7: jpayne@7: # If the hooks create history then we want those cookies too jpayne@7: for resp in r.history: jpayne@7: extract_cookies_to_jar(self.cookies, resp.request, resp.raw) jpayne@7: jpayne@7: extract_cookies_to_jar(self.cookies, request, r.raw) jpayne@7: jpayne@7: # Resolve redirects if allowed. jpayne@7: if allow_redirects: jpayne@7: # Redirect resolving generator. jpayne@7: gen = self.resolve_redirects(r, request, **kwargs) jpayne@7: history = [resp for resp in gen] jpayne@7: else: jpayne@7: history = [] jpayne@7: jpayne@7: # Shuffle things around if there's history. jpayne@7: if history: jpayne@7: # Insert the first (original) request at the start jpayne@7: history.insert(0, r) jpayne@7: # Get the last request made jpayne@7: r = history.pop() jpayne@7: r.history = history jpayne@7: jpayne@7: # If redirects aren't being followed, store the response on the Request for Response.next(). jpayne@7: if not allow_redirects: jpayne@7: try: jpayne@7: r._next = next( jpayne@7: self.resolve_redirects(r, request, yield_requests=True, **kwargs) jpayne@7: ) jpayne@7: except StopIteration: jpayne@7: pass jpayne@7: jpayne@7: if not stream: jpayne@7: r.content jpayne@7: jpayne@7: return r jpayne@7: jpayne@7: def merge_environment_settings(self, url, proxies, stream, verify, cert): jpayne@7: """ jpayne@7: Check the environment and merge it with some settings. jpayne@7: jpayne@7: :rtype: dict jpayne@7: """ jpayne@7: # Gather clues from the surrounding environment. jpayne@7: if self.trust_env: jpayne@7: # Set environment's proxies. jpayne@7: no_proxy = proxies.get("no_proxy") if proxies is not None else None jpayne@7: env_proxies = get_environ_proxies(url, no_proxy=no_proxy) jpayne@7: for (k, v) in env_proxies.items(): jpayne@7: proxies.setdefault(k, v) jpayne@7: jpayne@7: # Look for requests environment configuration jpayne@7: # and be compatible with cURL. jpayne@7: if verify is True or verify is None: jpayne@7: verify = ( jpayne@7: os.environ.get("REQUESTS_CA_BUNDLE") jpayne@7: or os.environ.get("CURL_CA_BUNDLE") jpayne@7: or verify jpayne@7: ) jpayne@7: jpayne@7: # Merge all the kwargs. jpayne@7: proxies = merge_setting(proxies, self.proxies) jpayne@7: stream = merge_setting(stream, self.stream) jpayne@7: verify = merge_setting(verify, self.verify) jpayne@7: cert = merge_setting(cert, self.cert) jpayne@7: jpayne@7: return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert} jpayne@7: jpayne@7: def get_adapter(self, url): jpayne@7: """ jpayne@7: Returns the appropriate connection adapter for the given URL. jpayne@7: jpayne@7: :rtype: requests.adapters.BaseAdapter jpayne@7: """ jpayne@7: for (prefix, adapter) in self.adapters.items(): jpayne@7: jpayne@7: if url.lower().startswith(prefix.lower()): jpayne@7: return adapter jpayne@7: jpayne@7: # Nothing matches :-/ jpayne@7: raise InvalidSchema(f"No connection adapters were found for {url!r}") jpayne@7: jpayne@7: def close(self): jpayne@7: """Closes all adapters and as such the session""" jpayne@7: for v in self.adapters.values(): jpayne@7: v.close() jpayne@7: jpayne@7: def mount(self, prefix, adapter): jpayne@7: """Registers a connection adapter to a prefix. jpayne@7: jpayne@7: Adapters are sorted in descending order by prefix length. jpayne@7: """ jpayne@7: self.adapters[prefix] = adapter jpayne@7: keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] jpayne@7: jpayne@7: for key in keys_to_move: jpayne@7: self.adapters[key] = self.adapters.pop(key) jpayne@7: jpayne@7: def __getstate__(self): jpayne@7: state = {attr: getattr(self, attr, None) for attr in self.__attrs__} jpayne@7: return state jpayne@7: jpayne@7: def __setstate__(self, state): jpayne@7: for attr, value in state.items(): jpayne@7: setattr(self, attr, value) jpayne@7: jpayne@7: jpayne@7: def session(): jpayne@7: """ jpayne@7: Returns a :class:`Session` for context-management. jpayne@7: jpayne@7: .. deprecated:: 1.0.0 jpayne@7: jpayne@7: This method has been deprecated since version 1.0.0 and is only kept for jpayne@7: backwards compatibility. New code should use :class:`~requests.sessions.Session` jpayne@7: to create a session. This may be removed at a future date. jpayne@7: jpayne@7: :rtype: Session jpayne@7: """ jpayne@7: return Session()