jpayne@7: """ jpayne@7: requests.models jpayne@7: ~~~~~~~~~~~~~~~ jpayne@7: jpayne@7: This module contains the primary objects that power Requests. jpayne@7: """ jpayne@7: jpayne@7: import datetime jpayne@7: jpayne@7: # Import encoding now, to avoid implicit import later. jpayne@7: # Implicit import within threads may cause LookupError when standard library is in a ZIP, jpayne@7: # such as in Embedded Python. See https://github.com/psf/requests/issues/3578. jpayne@7: import encodings.idna # noqa: F401 jpayne@7: from io import UnsupportedOperation jpayne@7: jpayne@7: from urllib3.exceptions import ( jpayne@7: DecodeError, jpayne@7: LocationParseError, jpayne@7: ProtocolError, jpayne@7: ReadTimeoutError, jpayne@7: SSLError, jpayne@7: ) jpayne@7: from urllib3.fields import RequestField jpayne@7: from urllib3.filepost import encode_multipart_formdata jpayne@7: from urllib3.util import parse_url jpayne@7: jpayne@7: from ._internal_utils import to_native_string, unicode_is_ascii jpayne@7: from .auth import HTTPBasicAuth jpayne@7: from .compat import ( jpayne@7: Callable, jpayne@7: JSONDecodeError, jpayne@7: Mapping, jpayne@7: basestring, jpayne@7: builtin_str, jpayne@7: chardet, jpayne@7: cookielib, jpayne@7: ) jpayne@7: from .compat import json as complexjson jpayne@7: from .compat import urlencode, urlsplit, urlunparse jpayne@7: from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header jpayne@7: from .exceptions import ( jpayne@7: ChunkedEncodingError, jpayne@7: ConnectionError, jpayne@7: ContentDecodingError, jpayne@7: HTTPError, jpayne@7: InvalidJSONError, jpayne@7: InvalidURL, jpayne@7: ) jpayne@7: from .exceptions import JSONDecodeError as RequestsJSONDecodeError jpayne@7: from .exceptions import MissingSchema jpayne@7: from .exceptions import SSLError as RequestsSSLError jpayne@7: from .exceptions import StreamConsumedError jpayne@7: from .hooks import default_hooks jpayne@7: from .status_codes import codes jpayne@7: from .structures import CaseInsensitiveDict jpayne@7: from .utils import ( jpayne@7: check_header_validity, jpayne@7: get_auth_from_url, jpayne@7: guess_filename, jpayne@7: guess_json_utf, jpayne@7: iter_slices, jpayne@7: parse_header_links, jpayne@7: requote_uri, jpayne@7: stream_decode_response_unicode, jpayne@7: super_len, jpayne@7: to_key_val_list, jpayne@7: ) jpayne@7: jpayne@7: #: The set of HTTP status codes that indicate an automatically jpayne@7: #: processable redirect. jpayne@7: REDIRECT_STATI = ( jpayne@7: codes.moved, # 301 jpayne@7: codes.found, # 302 jpayne@7: codes.other, # 303 jpayne@7: codes.temporary_redirect, # 307 jpayne@7: codes.permanent_redirect, # 308 jpayne@7: ) jpayne@7: jpayne@7: DEFAULT_REDIRECT_LIMIT = 30 jpayne@7: CONTENT_CHUNK_SIZE = 10 * 1024 jpayne@7: ITER_CHUNK_SIZE = 512 jpayne@7: jpayne@7: jpayne@7: class RequestEncodingMixin: jpayne@7: @property jpayne@7: def path_url(self): jpayne@7: """Build the path URL to use.""" jpayne@7: jpayne@7: url = [] jpayne@7: jpayne@7: p = urlsplit(self.url) jpayne@7: jpayne@7: path = p.path jpayne@7: if not path: jpayne@7: path = "/" jpayne@7: jpayne@7: url.append(path) jpayne@7: jpayne@7: query = p.query jpayne@7: if query: jpayne@7: url.append("?") jpayne@7: url.append(query) jpayne@7: jpayne@7: return "".join(url) jpayne@7: jpayne@7: @staticmethod jpayne@7: def _encode_params(data): jpayne@7: """Encode parameters in a piece of data. jpayne@7: jpayne@7: Will successfully encode parameters when passed as a dict or a list of jpayne@7: 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary jpayne@7: if parameters are supplied as a dict. jpayne@7: """ jpayne@7: jpayne@7: if isinstance(data, (str, bytes)): jpayne@7: return data jpayne@7: elif hasattr(data, "read"): jpayne@7: return data jpayne@7: elif hasattr(data, "__iter__"): jpayne@7: result = [] jpayne@7: for k, vs in to_key_val_list(data): jpayne@7: if isinstance(vs, basestring) or not hasattr(vs, "__iter__"): jpayne@7: vs = [vs] jpayne@7: for v in vs: jpayne@7: if v is not None: jpayne@7: result.append( jpayne@7: ( jpayne@7: k.encode("utf-8") if isinstance(k, str) else k, jpayne@7: v.encode("utf-8") if isinstance(v, str) else v, jpayne@7: ) jpayne@7: ) jpayne@7: return urlencode(result, doseq=True) jpayne@7: else: jpayne@7: return data jpayne@7: jpayne@7: @staticmethod jpayne@7: def _encode_files(files, data): jpayne@7: """Build the body for a multipart/form-data request. jpayne@7: jpayne@7: Will successfully encode files when passed as a dict or a list of jpayne@7: tuples. Order is retained if data is a list of tuples but arbitrary jpayne@7: if parameters are supplied as a dict. jpayne@7: The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype) jpayne@7: or 4-tuples (filename, fileobj, contentype, custom_headers). jpayne@7: """ jpayne@7: if not files: jpayne@7: raise ValueError("Files must be provided.") jpayne@7: elif isinstance(data, basestring): jpayne@7: raise ValueError("Data must not be a string.") jpayne@7: jpayne@7: new_fields = [] jpayne@7: fields = to_key_val_list(data or {}) jpayne@7: files = to_key_val_list(files or {}) jpayne@7: jpayne@7: for field, val in fields: jpayne@7: if isinstance(val, basestring) or not hasattr(val, "__iter__"): jpayne@7: val = [val] jpayne@7: for v in val: jpayne@7: if v is not None: jpayne@7: # Don't call str() on bytestrings: in Py3 it all goes wrong. jpayne@7: if not isinstance(v, bytes): jpayne@7: v = str(v) jpayne@7: jpayne@7: new_fields.append( jpayne@7: ( jpayne@7: field.decode("utf-8") jpayne@7: if isinstance(field, bytes) jpayne@7: else field, jpayne@7: v.encode("utf-8") if isinstance(v, str) else v, jpayne@7: ) jpayne@7: ) jpayne@7: jpayne@7: for (k, v) in files: jpayne@7: # support for explicit filename jpayne@7: ft = None jpayne@7: fh = None jpayne@7: if isinstance(v, (tuple, list)): jpayne@7: if len(v) == 2: jpayne@7: fn, fp = v jpayne@7: elif len(v) == 3: jpayne@7: fn, fp, ft = v jpayne@7: else: jpayne@7: fn, fp, ft, fh = v jpayne@7: else: jpayne@7: fn = guess_filename(v) or k jpayne@7: fp = v jpayne@7: jpayne@7: if isinstance(fp, (str, bytes, bytearray)): jpayne@7: fdata = fp jpayne@7: elif hasattr(fp, "read"): jpayne@7: fdata = fp.read() jpayne@7: elif fp is None: jpayne@7: continue jpayne@7: else: jpayne@7: fdata = fp jpayne@7: jpayne@7: rf = RequestField(name=k, data=fdata, filename=fn, headers=fh) jpayne@7: rf.make_multipart(content_type=ft) jpayne@7: new_fields.append(rf) jpayne@7: jpayne@7: body, content_type = encode_multipart_formdata(new_fields) jpayne@7: jpayne@7: return body, content_type jpayne@7: jpayne@7: jpayne@7: class RequestHooksMixin: jpayne@7: def register_hook(self, event, hook): jpayne@7: """Properly register a hook.""" jpayne@7: jpayne@7: if event not in self.hooks: jpayne@7: raise ValueError(f'Unsupported event specified, with event name "{event}"') jpayne@7: jpayne@7: if isinstance(hook, Callable): jpayne@7: self.hooks[event].append(hook) jpayne@7: elif hasattr(hook, "__iter__"): jpayne@7: self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) jpayne@7: jpayne@7: def deregister_hook(self, event, hook): jpayne@7: """Deregister a previously registered hook. jpayne@7: Returns True if the hook existed, False if not. jpayne@7: """ jpayne@7: jpayne@7: try: jpayne@7: self.hooks[event].remove(hook) jpayne@7: return True jpayne@7: except ValueError: jpayne@7: return False jpayne@7: jpayne@7: jpayne@7: class Request(RequestHooksMixin): jpayne@7: """A user-created :class:`Request ` object. jpayne@7: jpayne@7: Used to prepare a :class:`PreparedRequest `, which is sent to the server. jpayne@7: jpayne@7: :param method: HTTP method to use. jpayne@7: :param url: URL to send. jpayne@7: :param headers: dictionary of headers to send. jpayne@7: :param files: dictionary of {filename: fileobject} files to multipart upload. jpayne@7: :param data: the body to attach to the request. If a dictionary or jpayne@7: list of tuples ``[(key, value)]`` is provided, form-encoding will jpayne@7: take place. jpayne@7: :param json: json for the body to attach to the request (if files or data is not specified). jpayne@7: :param params: URL parameters to append to the URL. If a dictionary or jpayne@7: list of tuples ``[(key, value)]`` is provided, form-encoding will jpayne@7: take place. jpayne@7: :param auth: Auth handler or (user, pass) tuple. jpayne@7: :param cookies: dictionary or CookieJar of cookies to attach to this request. jpayne@7: :param hooks: dictionary of callback hooks, for internal usage. jpayne@7: jpayne@7: Usage:: jpayne@7: jpayne@7: >>> import requests jpayne@7: >>> req = requests.Request('GET', 'https://httpbin.org/get') jpayne@7: >>> req.prepare() jpayne@7: jpayne@7: """ jpayne@7: jpayne@7: def __init__( jpayne@7: self, jpayne@7: method=None, jpayne@7: url=None, jpayne@7: headers=None, jpayne@7: files=None, jpayne@7: data=None, jpayne@7: params=None, jpayne@7: auth=None, jpayne@7: cookies=None, jpayne@7: hooks=None, jpayne@7: json=None, jpayne@7: ): jpayne@7: jpayne@7: # Default empty dicts for dict params. jpayne@7: data = [] if data is None else data jpayne@7: files = [] if files is None else files jpayne@7: headers = {} if headers is None else headers jpayne@7: params = {} if params is None else params jpayne@7: hooks = {} if hooks is None else hooks jpayne@7: jpayne@7: self.hooks = default_hooks() jpayne@7: for (k, v) in list(hooks.items()): jpayne@7: self.register_hook(event=k, hook=v) jpayne@7: jpayne@7: self.method = method jpayne@7: self.url = url jpayne@7: self.headers = headers jpayne@7: self.files = files jpayne@7: self.data = data jpayne@7: self.json = json jpayne@7: self.params = params jpayne@7: self.auth = auth jpayne@7: self.cookies = cookies jpayne@7: jpayne@7: def __repr__(self): jpayne@7: return f"" jpayne@7: jpayne@7: def prepare(self): jpayne@7: """Constructs a :class:`PreparedRequest ` for transmission and returns it.""" jpayne@7: p = PreparedRequest() jpayne@7: p.prepare( jpayne@7: method=self.method, jpayne@7: url=self.url, jpayne@7: headers=self.headers, jpayne@7: files=self.files, jpayne@7: data=self.data, jpayne@7: json=self.json, jpayne@7: params=self.params, jpayne@7: auth=self.auth, jpayne@7: cookies=self.cookies, jpayne@7: hooks=self.hooks, jpayne@7: ) jpayne@7: return p jpayne@7: jpayne@7: jpayne@7: class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): jpayne@7: """The fully mutable :class:`PreparedRequest ` object, jpayne@7: containing the exact bytes that will be sent to the server. jpayne@7: jpayne@7: Instances are generated from a :class:`Request ` object, and jpayne@7: should not be instantiated manually; doing so may produce undesirable jpayne@7: effects. jpayne@7: jpayne@7: Usage:: jpayne@7: jpayne@7: >>> import requests jpayne@7: >>> req = requests.Request('GET', 'https://httpbin.org/get') jpayne@7: >>> r = req.prepare() jpayne@7: >>> r jpayne@7: jpayne@7: jpayne@7: >>> s = requests.Session() jpayne@7: >>> s.send(r) jpayne@7: jpayne@7: """ jpayne@7: jpayne@7: def __init__(self): jpayne@7: #: HTTP verb to send to the server. jpayne@7: self.method = None jpayne@7: #: HTTP URL to send the request to. jpayne@7: self.url = None jpayne@7: #: dictionary of HTTP headers. jpayne@7: self.headers = None jpayne@7: # The `CookieJar` used to create the Cookie header will be stored here jpayne@7: # after prepare_cookies is called jpayne@7: self._cookies = None jpayne@7: #: request body to send to the server. jpayne@7: self.body = None jpayne@7: #: dictionary of callback hooks, for internal usage. jpayne@7: self.hooks = default_hooks() jpayne@7: #: integer denoting starting position of a readable file-like body. jpayne@7: self._body_position = None jpayne@7: jpayne@7: def prepare( jpayne@7: self, jpayne@7: method=None, jpayne@7: url=None, jpayne@7: headers=None, jpayne@7: files=None, jpayne@7: data=None, jpayne@7: params=None, jpayne@7: auth=None, jpayne@7: cookies=None, jpayne@7: hooks=None, jpayne@7: json=None, jpayne@7: ): jpayne@7: """Prepares the entire request with the given parameters.""" jpayne@7: jpayne@7: self.prepare_method(method) jpayne@7: self.prepare_url(url, params) jpayne@7: self.prepare_headers(headers) jpayne@7: self.prepare_cookies(cookies) jpayne@7: self.prepare_body(data, files, json) jpayne@7: self.prepare_auth(auth, url) jpayne@7: jpayne@7: # Note that prepare_auth must be last to enable authentication schemes jpayne@7: # such as OAuth to work on a fully prepared request. jpayne@7: jpayne@7: # This MUST go after prepare_auth. Authenticators could add a hook jpayne@7: self.prepare_hooks(hooks) jpayne@7: jpayne@7: def __repr__(self): jpayne@7: return f"" jpayne@7: jpayne@7: def copy(self): jpayne@7: p = PreparedRequest() jpayne@7: p.method = self.method jpayne@7: p.url = self.url jpayne@7: p.headers = self.headers.copy() if self.headers is not None else None jpayne@7: p._cookies = _copy_cookie_jar(self._cookies) jpayne@7: p.body = self.body jpayne@7: p.hooks = self.hooks jpayne@7: p._body_position = self._body_position jpayne@7: return p jpayne@7: jpayne@7: def prepare_method(self, method): jpayne@7: """Prepares the given HTTP method.""" jpayne@7: self.method = method jpayne@7: if self.method is not None: jpayne@7: self.method = to_native_string(self.method.upper()) jpayne@7: jpayne@7: @staticmethod jpayne@7: def _get_idna_encoded_host(host): jpayne@7: import idna jpayne@7: jpayne@7: try: jpayne@7: host = idna.encode(host, uts46=True).decode("utf-8") jpayne@7: except idna.IDNAError: jpayne@7: raise UnicodeError jpayne@7: return host jpayne@7: jpayne@7: def prepare_url(self, url, params): jpayne@7: """Prepares the given HTTP URL.""" jpayne@7: #: Accept objects that have string representations. jpayne@7: #: We're unable to blindly call unicode/str functions jpayne@7: #: as this will include the bytestring indicator (b'') jpayne@7: #: on python 3.x. jpayne@7: #: https://github.com/psf/requests/pull/2238 jpayne@7: if isinstance(url, bytes): jpayne@7: url = url.decode("utf8") jpayne@7: else: jpayne@7: url = str(url) jpayne@7: jpayne@7: # Remove leading whitespaces from url jpayne@7: url = url.lstrip() jpayne@7: jpayne@7: # Don't do any URL preparation for non-HTTP schemes like `mailto`, jpayne@7: # `data` etc to work around exceptions from `url_parse`, which jpayne@7: # handles RFC 3986 only. jpayne@7: if ":" in url and not url.lower().startswith("http"): jpayne@7: self.url = url jpayne@7: return jpayne@7: jpayne@7: # Support for unicode domain names and paths. jpayne@7: try: jpayne@7: scheme, auth, host, port, path, query, fragment = parse_url(url) jpayne@7: except LocationParseError as e: jpayne@7: raise InvalidURL(*e.args) jpayne@7: jpayne@7: if not scheme: jpayne@7: raise MissingSchema( jpayne@7: f"Invalid URL {url!r}: No scheme supplied. " jpayne@7: f"Perhaps you meant https://{url}?" jpayne@7: ) jpayne@7: jpayne@7: if not host: jpayne@7: raise InvalidURL(f"Invalid URL {url!r}: No host supplied") jpayne@7: jpayne@7: # In general, we want to try IDNA encoding the hostname if the string contains jpayne@7: # non-ASCII characters. This allows users to automatically get the correct IDNA jpayne@7: # behaviour. For strings containing only ASCII characters, we need to also verify jpayne@7: # it doesn't start with a wildcard (*), before allowing the unencoded hostname. jpayne@7: if not unicode_is_ascii(host): jpayne@7: try: jpayne@7: host = self._get_idna_encoded_host(host) jpayne@7: except UnicodeError: jpayne@7: raise InvalidURL("URL has an invalid label.") jpayne@7: elif host.startswith(("*", ".")): jpayne@7: raise InvalidURL("URL has an invalid label.") jpayne@7: jpayne@7: # Carefully reconstruct the network location jpayne@7: netloc = auth or "" jpayne@7: if netloc: jpayne@7: netloc += "@" jpayne@7: netloc += host jpayne@7: if port: jpayne@7: netloc += f":{port}" jpayne@7: jpayne@7: # Bare domains aren't valid URLs. jpayne@7: if not path: jpayne@7: path = "/" jpayne@7: jpayne@7: if isinstance(params, (str, bytes)): jpayne@7: params = to_native_string(params) jpayne@7: jpayne@7: enc_params = self._encode_params(params) jpayne@7: if enc_params: jpayne@7: if query: jpayne@7: query = f"{query}&{enc_params}" jpayne@7: else: jpayne@7: query = enc_params jpayne@7: jpayne@7: url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment])) jpayne@7: self.url = url jpayne@7: jpayne@7: def prepare_headers(self, headers): jpayne@7: """Prepares the given HTTP headers.""" jpayne@7: jpayne@7: self.headers = CaseInsensitiveDict() jpayne@7: if headers: jpayne@7: for header in headers.items(): jpayne@7: # Raise exception on invalid header value. jpayne@7: check_header_validity(header) jpayne@7: name, value = header jpayne@7: self.headers[to_native_string(name)] = value jpayne@7: jpayne@7: def prepare_body(self, data, files, json=None): jpayne@7: """Prepares the given HTTP body data.""" jpayne@7: jpayne@7: # Check if file, fo, generator, iterator. jpayne@7: # If not, run through normal process. jpayne@7: jpayne@7: # Nottin' on you. jpayne@7: body = None jpayne@7: content_type = None jpayne@7: jpayne@7: if not data and json is not None: jpayne@7: # urllib3 requires a bytes-like body. Python 2's json.dumps jpayne@7: # provides this natively, but Python 3 gives a Unicode string. jpayne@7: content_type = "application/json" jpayne@7: jpayne@7: try: jpayne@7: body = complexjson.dumps(json, allow_nan=False) jpayne@7: except ValueError as ve: jpayne@7: raise InvalidJSONError(ve, request=self) jpayne@7: jpayne@7: if not isinstance(body, bytes): jpayne@7: body = body.encode("utf-8") jpayne@7: jpayne@7: is_stream = all( jpayne@7: [ jpayne@7: hasattr(data, "__iter__"), jpayne@7: not isinstance(data, (basestring, list, tuple, Mapping)), jpayne@7: ] jpayne@7: ) jpayne@7: jpayne@7: if is_stream: jpayne@7: try: jpayne@7: length = super_len(data) jpayne@7: except (TypeError, AttributeError, UnsupportedOperation): jpayne@7: length = None jpayne@7: jpayne@7: body = data jpayne@7: jpayne@7: if getattr(body, "tell", None) is not None: jpayne@7: # Record the current file position before reading. jpayne@7: # This will allow us to rewind a file in the event jpayne@7: # of a redirect. jpayne@7: try: jpayne@7: self._body_position = body.tell() jpayne@7: except OSError: jpayne@7: # This differentiates from None, allowing us to catch jpayne@7: # a failed `tell()` later when trying to rewind the body jpayne@7: self._body_position = object() jpayne@7: jpayne@7: if files: jpayne@7: raise NotImplementedError( jpayne@7: "Streamed bodies and files are mutually exclusive." jpayne@7: ) jpayne@7: jpayne@7: if length: jpayne@7: self.headers["Content-Length"] = builtin_str(length) jpayne@7: else: jpayne@7: self.headers["Transfer-Encoding"] = "chunked" jpayne@7: else: jpayne@7: # Multi-part file uploads. jpayne@7: if files: jpayne@7: (body, content_type) = self._encode_files(files, data) jpayne@7: else: jpayne@7: if data: jpayne@7: body = self._encode_params(data) jpayne@7: if isinstance(data, basestring) or hasattr(data, "read"): jpayne@7: content_type = None jpayne@7: else: jpayne@7: content_type = "application/x-www-form-urlencoded" jpayne@7: jpayne@7: self.prepare_content_length(body) jpayne@7: jpayne@7: # Add content-type if it wasn't explicitly provided. jpayne@7: if content_type and ("content-type" not in self.headers): jpayne@7: self.headers["Content-Type"] = content_type jpayne@7: jpayne@7: self.body = body jpayne@7: jpayne@7: def prepare_content_length(self, body): jpayne@7: """Prepare Content-Length header based on request method and body""" jpayne@7: if body is not None: jpayne@7: length = super_len(body) jpayne@7: if length: jpayne@7: # If length exists, set it. Otherwise, we fallback jpayne@7: # to Transfer-Encoding: chunked. jpayne@7: self.headers["Content-Length"] = builtin_str(length) jpayne@7: elif ( jpayne@7: self.method not in ("GET", "HEAD") jpayne@7: and self.headers.get("Content-Length") is None jpayne@7: ): jpayne@7: # Set Content-Length to 0 for methods that can have a body jpayne@7: # but don't provide one. (i.e. not GET or HEAD) jpayne@7: self.headers["Content-Length"] = "0" jpayne@7: jpayne@7: def prepare_auth(self, auth, url=""): jpayne@7: """Prepares the given HTTP auth data.""" jpayne@7: jpayne@7: # If no Auth is explicitly provided, extract it from the URL first. jpayne@7: if auth is None: jpayne@7: url_auth = get_auth_from_url(self.url) jpayne@7: auth = url_auth if any(url_auth) else None jpayne@7: jpayne@7: if auth: jpayne@7: if isinstance(auth, tuple) and len(auth) == 2: jpayne@7: # special-case basic HTTP auth jpayne@7: auth = HTTPBasicAuth(*auth) jpayne@7: jpayne@7: # Allow auth to make its changes. jpayne@7: r = auth(self) jpayne@7: jpayne@7: # Update self to reflect the auth changes. jpayne@7: self.__dict__.update(r.__dict__) jpayne@7: jpayne@7: # Recompute Content-Length jpayne@7: self.prepare_content_length(self.body) jpayne@7: jpayne@7: def prepare_cookies(self, cookies): jpayne@7: """Prepares the given HTTP cookie data. jpayne@7: jpayne@7: This function eventually generates a ``Cookie`` header from the jpayne@7: given cookies using cookielib. Due to cookielib's design, the header jpayne@7: will not be regenerated if it already exists, meaning this function jpayne@7: can only be called once for the life of the jpayne@7: :class:`PreparedRequest ` object. Any subsequent calls jpayne@7: to ``prepare_cookies`` will have no actual effect, unless the "Cookie" jpayne@7: header is removed beforehand. jpayne@7: """ jpayne@7: if isinstance(cookies, cookielib.CookieJar): jpayne@7: self._cookies = cookies jpayne@7: else: jpayne@7: self._cookies = cookiejar_from_dict(cookies) jpayne@7: jpayne@7: cookie_header = get_cookie_header(self._cookies, self) jpayne@7: if cookie_header is not None: jpayne@7: self.headers["Cookie"] = cookie_header jpayne@7: jpayne@7: def prepare_hooks(self, hooks): jpayne@7: """Prepares the given hooks.""" jpayne@7: # hooks can be passed as None to the prepare method and to this jpayne@7: # method. To prevent iterating over None, simply use an empty list jpayne@7: # if hooks is False-y jpayne@7: hooks = hooks or [] jpayne@7: for event in hooks: jpayne@7: self.register_hook(event, hooks[event]) jpayne@7: jpayne@7: jpayne@7: class Response: jpayne@7: """The :class:`Response ` object, which contains a jpayne@7: server's response to an HTTP request. jpayne@7: """ jpayne@7: jpayne@7: __attrs__ = [ jpayne@7: "_content", jpayne@7: "status_code", jpayne@7: "headers", jpayne@7: "url", jpayne@7: "history", jpayne@7: "encoding", jpayne@7: "reason", jpayne@7: "cookies", jpayne@7: "elapsed", jpayne@7: "request", jpayne@7: ] jpayne@7: jpayne@7: def __init__(self): jpayne@7: self._content = False jpayne@7: self._content_consumed = False jpayne@7: self._next = None jpayne@7: jpayne@7: #: Integer Code of responded HTTP Status, e.g. 404 or 200. jpayne@7: self.status_code = None jpayne@7: jpayne@7: #: Case-insensitive Dictionary of Response Headers. jpayne@7: #: For example, ``headers['content-encoding']`` will return the jpayne@7: #: value of a ``'Content-Encoding'`` response header. jpayne@7: self.headers = CaseInsensitiveDict() jpayne@7: jpayne@7: #: File-like object representation of response (for advanced usage). jpayne@7: #: Use of ``raw`` requires that ``stream=True`` be set on the request. jpayne@7: #: This requirement does not apply for use internally to Requests. jpayne@7: self.raw = None jpayne@7: jpayne@7: #: Final URL location of Response. jpayne@7: self.url = None jpayne@7: jpayne@7: #: Encoding to decode with when accessing r.text. jpayne@7: self.encoding = None jpayne@7: jpayne@7: #: A list of :class:`Response ` objects from jpayne@7: #: the history of the Request. Any redirect responses will end jpayne@7: #: up here. The list is sorted from the oldest to the most recent request. jpayne@7: self.history = [] jpayne@7: jpayne@7: #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". jpayne@7: self.reason = None jpayne@7: jpayne@7: #: A CookieJar of Cookies the server sent back. jpayne@7: self.cookies = cookiejar_from_dict({}) jpayne@7: jpayne@7: #: The amount of time elapsed between sending the request jpayne@7: #: and the arrival of the response (as a timedelta). jpayne@7: #: This property specifically measures the time taken between sending jpayne@7: #: the first byte of the request and finishing parsing the headers. It jpayne@7: #: is therefore unaffected by consuming the response content or the jpayne@7: #: value of the ``stream`` keyword argument. jpayne@7: self.elapsed = datetime.timedelta(0) jpayne@7: jpayne@7: #: The :class:`PreparedRequest ` object to which this jpayne@7: #: is a response. jpayne@7: self.request = None 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 __getstate__(self): jpayne@7: # Consume everything; accessing the content attribute makes jpayne@7: # sure the content has been fully read. jpayne@7: if not self._content_consumed: jpayne@7: self.content jpayne@7: jpayne@7: return {attr: getattr(self, attr, None) for attr in self.__attrs__} jpayne@7: jpayne@7: def __setstate__(self, state): jpayne@7: for name, value in state.items(): jpayne@7: setattr(self, name, value) jpayne@7: jpayne@7: # pickled objects do not have .raw jpayne@7: setattr(self, "_content_consumed", True) jpayne@7: setattr(self, "raw", None) jpayne@7: jpayne@7: def __repr__(self): jpayne@7: return f"" jpayne@7: jpayne@7: def __bool__(self): jpayne@7: """Returns True if :attr:`status_code` is less than 400. jpayne@7: jpayne@7: This attribute checks if the status code of the response is between jpayne@7: 400 and 600 to see if there was a client error or a server error. If jpayne@7: the status code, is between 200 and 400, this will return True. This jpayne@7: is **not** a check to see if the response code is ``200 OK``. jpayne@7: """ jpayne@7: return self.ok jpayne@7: jpayne@7: def __nonzero__(self): jpayne@7: """Returns True if :attr:`status_code` is less than 400. jpayne@7: jpayne@7: This attribute checks if the status code of the response is between jpayne@7: 400 and 600 to see if there was a client error or a server error. If jpayne@7: the status code, is between 200 and 400, this will return True. This jpayne@7: is **not** a check to see if the response code is ``200 OK``. jpayne@7: """ jpayne@7: return self.ok jpayne@7: jpayne@7: def __iter__(self): jpayne@7: """Allows you to use a response as an iterator.""" jpayne@7: return self.iter_content(128) jpayne@7: jpayne@7: @property jpayne@7: def ok(self): jpayne@7: """Returns True if :attr:`status_code` is less than 400, False if not. jpayne@7: jpayne@7: This attribute checks if the status code of the response is between jpayne@7: 400 and 600 to see if there was a client error or a server error. If jpayne@7: the status code is between 200 and 400, this will return True. This jpayne@7: is **not** a check to see if the response code is ``200 OK``. jpayne@7: """ jpayne@7: try: jpayne@7: self.raise_for_status() jpayne@7: except HTTPError: jpayne@7: return False jpayne@7: return True jpayne@7: jpayne@7: @property jpayne@7: def is_redirect(self): jpayne@7: """True if this Response is a well-formed HTTP redirect that could have jpayne@7: been processed automatically (by :meth:`Session.resolve_redirects`). jpayne@7: """ jpayne@7: return "location" in self.headers and self.status_code in REDIRECT_STATI jpayne@7: jpayne@7: @property jpayne@7: def is_permanent_redirect(self): jpayne@7: """True if this Response one of the permanent versions of redirect.""" jpayne@7: return "location" in self.headers and self.status_code in ( jpayne@7: codes.moved_permanently, jpayne@7: codes.permanent_redirect, jpayne@7: ) jpayne@7: jpayne@7: @property jpayne@7: def next(self): jpayne@7: """Returns a PreparedRequest for the next request in a redirect chain, if there is one.""" jpayne@7: return self._next jpayne@7: jpayne@7: @property jpayne@7: def apparent_encoding(self): jpayne@7: """The apparent encoding, provided by the charset_normalizer or chardet libraries.""" jpayne@7: return chardet.detect(self.content)["encoding"] jpayne@7: jpayne@7: def iter_content(self, chunk_size=1, decode_unicode=False): jpayne@7: """Iterates over the response data. When stream=True is set on the jpayne@7: request, this avoids reading the content at once into memory for jpayne@7: large responses. The chunk size is the number of bytes it should jpayne@7: read into memory. This is not necessarily the length of each item jpayne@7: returned as decoding can take place. jpayne@7: jpayne@7: chunk_size must be of type int or None. A value of None will jpayne@7: function differently depending on the value of `stream`. jpayne@7: stream=True will read data as it arrives in whatever size the jpayne@7: chunks are received. If stream=False, data is returned as jpayne@7: a single chunk. jpayne@7: jpayne@7: If decode_unicode is True, content will be decoded using the best jpayne@7: available encoding based on the response. jpayne@7: """ jpayne@7: jpayne@7: def generate(): jpayne@7: # Special case for urllib3. jpayne@7: if hasattr(self.raw, "stream"): jpayne@7: try: jpayne@7: yield from self.raw.stream(chunk_size, decode_content=True) jpayne@7: except ProtocolError as e: jpayne@7: raise ChunkedEncodingError(e) jpayne@7: except DecodeError as e: jpayne@7: raise ContentDecodingError(e) jpayne@7: except ReadTimeoutError as e: jpayne@7: raise ConnectionError(e) jpayne@7: except SSLError as e: jpayne@7: raise RequestsSSLError(e) jpayne@7: else: jpayne@7: # Standard file-like object. jpayne@7: while True: jpayne@7: chunk = self.raw.read(chunk_size) jpayne@7: if not chunk: jpayne@7: break jpayne@7: yield chunk jpayne@7: jpayne@7: self._content_consumed = True jpayne@7: jpayne@7: if self._content_consumed and isinstance(self._content, bool): jpayne@7: raise StreamConsumedError() jpayne@7: elif chunk_size is not None and not isinstance(chunk_size, int): jpayne@7: raise TypeError( jpayne@7: f"chunk_size must be an int, it is instead a {type(chunk_size)}." jpayne@7: ) jpayne@7: # simulate reading small chunks of the content jpayne@7: reused_chunks = iter_slices(self._content, chunk_size) jpayne@7: jpayne@7: stream_chunks = generate() jpayne@7: jpayne@7: chunks = reused_chunks if self._content_consumed else stream_chunks jpayne@7: jpayne@7: if decode_unicode: jpayne@7: chunks = stream_decode_response_unicode(chunks, self) jpayne@7: jpayne@7: return chunks jpayne@7: jpayne@7: def iter_lines( jpayne@7: self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None jpayne@7: ): jpayne@7: """Iterates over the response data, one line at a time. When jpayne@7: stream=True is set on the request, this avoids reading the jpayne@7: content at once into memory for large responses. jpayne@7: jpayne@7: .. note:: This method is not reentrant safe. jpayne@7: """ jpayne@7: jpayne@7: pending = None jpayne@7: jpayne@7: for chunk in self.iter_content( jpayne@7: chunk_size=chunk_size, decode_unicode=decode_unicode jpayne@7: ): jpayne@7: jpayne@7: if pending is not None: jpayne@7: chunk = pending + chunk jpayne@7: jpayne@7: if delimiter: jpayne@7: lines = chunk.split(delimiter) jpayne@7: else: jpayne@7: lines = chunk.splitlines() jpayne@7: jpayne@7: if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: jpayne@7: pending = lines.pop() jpayne@7: else: jpayne@7: pending = None jpayne@7: jpayne@7: yield from lines jpayne@7: jpayne@7: if pending is not None: jpayne@7: yield pending jpayne@7: jpayne@7: @property jpayne@7: def content(self): jpayne@7: """Content of the response, in bytes.""" jpayne@7: jpayne@7: if self._content is False: jpayne@7: # Read the contents. jpayne@7: if self._content_consumed: jpayne@7: raise RuntimeError("The content for this response was already consumed") jpayne@7: jpayne@7: if self.status_code == 0 or self.raw is None: jpayne@7: self._content = None jpayne@7: else: jpayne@7: self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" jpayne@7: jpayne@7: self._content_consumed = True jpayne@7: # don't need to release the connection; that's been handled by urllib3 jpayne@7: # since we exhausted the data. jpayne@7: return self._content jpayne@7: jpayne@7: @property jpayne@7: def text(self): jpayne@7: """Content of the response, in unicode. jpayne@7: jpayne@7: If Response.encoding is None, encoding will be guessed using jpayne@7: ``charset_normalizer`` or ``chardet``. jpayne@7: jpayne@7: The encoding of the response content is determined based solely on HTTP jpayne@7: headers, following RFC 2616 to the letter. If you can take advantage of jpayne@7: non-HTTP knowledge to make a better guess at the encoding, you should jpayne@7: set ``r.encoding`` appropriately before accessing this property. jpayne@7: """ jpayne@7: jpayne@7: # Try charset from content-type jpayne@7: content = None jpayne@7: encoding = self.encoding jpayne@7: jpayne@7: if not self.content: jpayne@7: return "" jpayne@7: jpayne@7: # Fallback to auto-detected encoding. jpayne@7: if self.encoding is None: jpayne@7: encoding = self.apparent_encoding jpayne@7: jpayne@7: # Decode unicode from given encoding. jpayne@7: try: jpayne@7: content = str(self.content, encoding, errors="replace") jpayne@7: except (LookupError, TypeError): jpayne@7: # A LookupError is raised if the encoding was not found which could jpayne@7: # indicate a misspelling or similar mistake. jpayne@7: # jpayne@7: # A TypeError can be raised if encoding is None jpayne@7: # jpayne@7: # So we try blindly encoding. jpayne@7: content = str(self.content, errors="replace") jpayne@7: jpayne@7: return content jpayne@7: jpayne@7: def json(self, **kwargs): jpayne@7: r"""Returns the json-encoded content of a response, if any. jpayne@7: jpayne@7: :param \*\*kwargs: Optional arguments that ``json.loads`` takes. jpayne@7: :raises requests.exceptions.JSONDecodeError: If the response body does not jpayne@7: contain valid json. jpayne@7: """ jpayne@7: jpayne@7: if not self.encoding and self.content and len(self.content) > 3: jpayne@7: # No encoding set. JSON RFC 4627 section 3 states we should expect jpayne@7: # UTF-8, -16 or -32. Detect which one to use; If the detection or jpayne@7: # decoding fails, fall back to `self.text` (using charset_normalizer to make jpayne@7: # a best guess). jpayne@7: encoding = guess_json_utf(self.content) jpayne@7: if encoding is not None: jpayne@7: try: jpayne@7: return complexjson.loads(self.content.decode(encoding), **kwargs) jpayne@7: except UnicodeDecodeError: jpayne@7: # Wrong UTF codec detected; usually because it's not UTF-8 jpayne@7: # but some other 8-bit codec. This is an RFC violation, jpayne@7: # and the server didn't bother to tell us what codec *was* jpayne@7: # used. jpayne@7: pass jpayne@7: except JSONDecodeError as e: jpayne@7: raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) jpayne@7: jpayne@7: try: jpayne@7: return complexjson.loads(self.text, **kwargs) jpayne@7: except JSONDecodeError as e: jpayne@7: # Catch JSON-related errors and raise as requests.JSONDecodeError jpayne@7: # This aliases json.JSONDecodeError and simplejson.JSONDecodeError jpayne@7: raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) jpayne@7: jpayne@7: @property jpayne@7: def links(self): jpayne@7: """Returns the parsed header links of the response, if any.""" jpayne@7: jpayne@7: header = self.headers.get("link") jpayne@7: jpayne@7: resolved_links = {} jpayne@7: jpayne@7: if header: jpayne@7: links = parse_header_links(header) jpayne@7: jpayne@7: for link in links: jpayne@7: key = link.get("rel") or link.get("url") jpayne@7: resolved_links[key] = link jpayne@7: jpayne@7: return resolved_links jpayne@7: jpayne@7: def raise_for_status(self): jpayne@7: """Raises :class:`HTTPError`, if one occurred.""" jpayne@7: jpayne@7: http_error_msg = "" jpayne@7: if isinstance(self.reason, bytes): jpayne@7: # We attempt to decode utf-8 first because some servers jpayne@7: # choose to localize their reason strings. If the string jpayne@7: # isn't utf-8, we fall back to iso-8859-1 for all other jpayne@7: # encodings. (See PR #3538) jpayne@7: try: jpayne@7: reason = self.reason.decode("utf-8") jpayne@7: except UnicodeDecodeError: jpayne@7: reason = self.reason.decode("iso-8859-1") jpayne@7: else: jpayne@7: reason = self.reason jpayne@7: jpayne@7: if 400 <= self.status_code < 500: jpayne@7: http_error_msg = ( jpayne@7: f"{self.status_code} Client Error: {reason} for url: {self.url}" jpayne@7: ) jpayne@7: jpayne@7: elif 500 <= self.status_code < 600: jpayne@7: http_error_msg = ( jpayne@7: f"{self.status_code} Server Error: {reason} for url: {self.url}" jpayne@7: ) jpayne@7: jpayne@7: if http_error_msg: jpayne@7: raise HTTPError(http_error_msg, response=self) jpayne@7: jpayne@7: def close(self): jpayne@7: """Releases the connection back to the pool. Once this method has been jpayne@7: called the underlying ``raw`` object must not be accessed again. jpayne@7: jpayne@7: *Note: Should not normally need to be called explicitly.* jpayne@7: """ jpayne@7: if not self._content_consumed: jpayne@7: self.raw.close() jpayne@7: jpayne@7: release_conn = getattr(self.raw, "release_conn", None) jpayne@7: if release_conn is not None: jpayne@7: release_conn()