diff options
Diffstat (limited to 'requests/models.py')
-rw-r--r-- | requests/models.py | 931 |
1 files changed, 363 insertions, 568 deletions
diff --git a/requests/models.py b/requests/models.py index fbdfb56..5202e6f 100644 --- a/requests/models.py +++ b/requests/models.py @@ -7,380 +7,291 @@ requests.models This module contains the primary objects that power Requests. """ -import json -import os -from datetime import datetime +import collections +import logging -from .hooks import dispatch_hook, HOOKS +from io import BytesIO +from .hooks import default_hooks from .structures import CaseInsensitiveDict from .status_codes import codes -from .auth import HTTPBasicAuth, HTTPProxyAuth -from .cookies import cookiejar_from_dict, extract_cookies_to_jar, get_cookie_header -from .packages.urllib3.response import HTTPResponse -from .packages.urllib3.exceptions import MaxRetryError, LocationParseError -from .packages.urllib3.exceptions import SSLError as _SSLError -from .packages.urllib3.exceptions import HTTPError as _HTTPError -from .packages.urllib3 import connectionpool, poolmanager +from .auth import HTTPBasicAuth +from .cookies import cookiejar_from_dict, get_cookie_header from .packages.urllib3.filepost import encode_multipart_formdata -from .defaults import SCHEMAS -from .exceptions import ( - ConnectionError, HTTPError, RequestException, Timeout, TooManyRedirects, - URLRequired, SSLError, MissingSchema, InvalidSchema, InvalidURL) +from .exceptions import HTTPError, RequestException, MissingSchema, InvalidURL from .utils import ( - get_encoding_from_headers, stream_untransfer, guess_filename, requote_uri, - stream_decode_response_unicode, get_netrc_auth, get_environ_proxies, - DEFAULT_CA_BUNDLE_PATH) + stream_untransfer, guess_filename, requote_uri, + stream_decode_response_unicode, to_key_val_list, parse_header_links, + iter_slices, guess_json_utf, super_len) from .compat import ( - cookielib, urlparse, urlunparse, urljoin, urlsplit, urlencode, str, bytes, - StringIO, is_py2) - -# Import chardet if it is available. -try: - import chardet - # hush pyflakes - chardet -except ImportError: - chardet = None + cookielib, urlparse, urlunparse, urlsplit, urlencode, str, bytes, StringIO, + is_py2, chardet, json, builtin_str, basestring) REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved) CONTENT_CHUNK_SIZE = 10 * 1024 +ITER_CHUNK_SIZE = 10 * 1024 -class Request(object): - """The :class:`Request <Request>` object. It carries out all functionality of - Requests. Recommended interface is with the Requests functions. - """ - - def __init__(self, - url=None, - headers=dict(), - files=None, - method=None, - data=dict(), - params=dict(), - auth=None, - cookies=None, - timeout=None, - redirect=False, - allow_redirects=False, - proxies=None, - hooks=None, - config=None, - prefetch=False, - _poolmanager=None, - verify=None, - session=None, - cert=None): - - #: Dictionary of configurations for this request. - self.config = dict(config or []) - - #: Float describes the timeout of the request. - # (Use socket.setdefaulttimeout() as fallback) - self.timeout = timeout - - #: Request URL. - self.url = url - - #: Dictionary of HTTP Headers to attach to the :class:`Request <Request>`. - self.headers = dict(headers or []) - - #: Dictionary of files to multipart upload (``{filename: content}``). - self.files = None +log = logging.getLogger(__name__) - #: HTTP Method to use. - self.method = method - #: Dictionary or byte of request body data to attach to the - #: :class:`Request <Request>`. - self.data = None +class RequestEncodingMixin(object): + @property + def path_url(self): + """Build the path URL to use.""" - #: Dictionary or byte of querystring data to attach to the - #: :class:`Request <Request>`. The dictionary values can be lists for representing - #: multivalued query parameters. - self.params = None + url = [] - #: True if :class:`Request <Request>` is part of a redirect chain (disables history - #: and HTTPError storage). - self.redirect = redirect + p = urlsplit(self.url) - #: Set to True if full redirects are allowed (e.g. re-POST-ing of data at new ``Location``) - self.allow_redirects = allow_redirects + path = p.path + if not path: + path = '/' - # Dictionary mapping protocol to the URL of the proxy (e.g. {'http': 'foo.bar:3128'}) - self.proxies = dict(proxies or []) + url.append(path) - # If no proxies are given, allow configuration by environment variables - # HTTP_PROXY and HTTPS_PROXY. - if not self.proxies and self.config.get('trust_env'): - self.proxies = get_environ_proxies() + query = p.query + if query: + url.append('?') + url.append(query) - self.data = data - self.params = params - self.files = files + return ''.join(url) - #: :class:`Response <Response>` instance, containing - #: content and metadata of HTTP Response, once :attr:`sent <send>`. - self.response = Response() + @staticmethod + def _encode_params(data): + """Encode parameters in a piece of data. - #: Authentication tuple or object to attach to :class:`Request <Request>`. - self.auth = auth + Will successfully encode parameters when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but abritrary + if parameters are supplied as a dict. + """ - #: CookieJar to attach to :class:`Request <Request>`. - if isinstance(cookies, cookielib.CookieJar): - self.cookies = cookies + 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: - self.cookies = cookiejar_from_dict(cookies) - - #: True if Request has been sent. - self.sent = False - - #: Event-handling hooks. - self.hooks = {} + return data - for event in HOOKS: - self.hooks[event] = [] + @staticmethod + def _encode_files(files, data): + """Build the body for a multipart/form-data request. - hooks = hooks or {} + Will successfully encode files when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but abritrary + if parameters are supplied as a dict. - for (k, v) in list(hooks.items()): - self.register_hook(event=k, hook=v) + """ + if (not files) or isinstance(data, str): + return None - #: Session. - self.session = session + new_fields = [] + fields = to_key_val_list(data or {}) + files = to_key_val_list(files or {}) - #: SSL Verification. - self.verify = verify + for field, val in fields: + if isinstance(val, list): + for v in val: + new_fields.append((field, builtin_str(v))) + else: + new_fields.append((field, builtin_str(val))) - #: SSL Certificate - self.cert = cert + for (k, v) in files: + # support for explicit filename + ft = None + if isinstance(v, (tuple, list)): + if len(v) == 2: + fn, fp = v + else: + fn, fp, ft = v + else: + fn = guess_filename(v) or k + fp = v + if isinstance(fp, str): + fp = StringIO(fp) + if isinstance(fp, bytes): + fp = BytesIO(fp) + + if ft: + new_v = (fn, fp.read(), ft) + else: + new_v = (fn, fp.read()) + new_fields.append((k, new_v)) - #: Prefetch response content - self.prefetch = prefetch + body, content_type = encode_multipart_formdata(new_fields) - if headers: - headers = CaseInsensitiveDict(self.headers) - else: - headers = CaseInsensitiveDict() + return body, content_type - # Add configured base headers. - for (k, v) in list(self.config.get('base_headers', {}).items()): - if k not in headers: - headers[k] = v - self.headers = headers - self._poolmanager = _poolmanager +class RequestHooksMixin(object): + def register_hook(self, event, hook): + """Properly register a hook.""" - def __repr__(self): - return '<Request [%s]>' % (self.method) + if isinstance(hook, collections.Callable): + self.hooks[event].append(hook) + elif hasattr(hook, '__iter__'): + self.hooks[event].extend(h for h in hook if isinstance(h, collections.Callable)) - def _build_response(self, resp): - """Build internal :class:`Response <Response>` object - from given response. + def deregister_hook(self, event, hook): + """Deregister a previously registered hook. + Returns True if the hook existed, False if not. """ - def build(resp): - - response = Response() - - # Pass settings over. - response.config = self.config - - if resp: - - # Fallback to None if there's no status_code, for whatever reason. - response.status_code = getattr(resp, 'status', None) + try: + self.hooks[event].remove(hook) + return True + except ValueError: + return False - # Make headers case-insensitive. - response.headers = CaseInsensitiveDict(getattr(resp, 'headers', None)) - # Set encoding. - response.encoding = get_encoding_from_headers(response.headers) +class Request(RequestHooksMixin): + """A user-created :class:`Request <Request>` object. - # Add new cookies from the server. - extract_cookies_to_jar(self.cookies, self, resp) + Used to prepare a :class:`PreparedRequest <PreparedRequest>`, which is sent to the server. - # Save cookies in Response. - response.cookies = self.cookies + :param method: HTTP method to use. + :param url: URL to send. + :param headers: dictionary of headers to send. + :param files: dictionary of {filename: fileobject} files to multipart upload. + :param data: the body to attach the request. If a dictionary is provided, form-encoding will take place. + :param params: dictionary of URL parameters to append to the URL. + :param auth: Auth handler or (user, pass) tuple. + :param cookies: dictionary or CookieJar of cookies to attach to this request. + :param hooks: dictionary of callback hooks, for internal usage. - # No exceptions were harmed in the making of this request. - response.error = getattr(resp, 'error', None) + Usage:: - # Save original response for later. - response.raw = resp - if isinstance(self.full_url, bytes): - response.url = self.full_url.decode('utf-8') - else: - response.url = self.full_url + >>> import requests + >>> req = requests.Request('GET', 'http://httpbin.org/get') + >>> req.prepare() + <PreparedRequest [GET]> - return response + """ + def __init__(self, + method=None, + url=None, + headers=None, + files=None, + data=dict(), + params=dict(), + auth=None, + cookies=None, + hooks=None): - history = [] - r = build(resp) + # 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 - if r.status_code in REDIRECT_STATI and not self.redirect: + self.hooks = default_hooks() + for (k, v) in list(hooks.items()): + self.register_hook(event=k, hook=v) - while (('location' in r.headers) and - ((r.status_code is codes.see_other) or (self.allow_redirects))): + self.method = method + self.url = url + self.headers = headers + self.files = files + self.data = data + self.params = params + self.auth = auth + self.cookies = cookies + self.hooks = hooks - r.content # Consume socket so it can be released + def __repr__(self): + return '<Request [%s]>' % (self.method) - if not len(history) < self.config.get('max_redirects'): - raise TooManyRedirects() + def prepare(self): + """Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it.""" + p = PreparedRequest() - # Release the connection back into the pool. - r.raw.release_conn() + p.prepare_method(self.method) + p.prepare_url(self.url, self.params) + p.prepare_headers(self.headers) + p.prepare_cookies(self.cookies) + p.prepare_body(self.data, self.files) + # Note that prepare_auth must be last to enable authentication schemes + # such as OAuth to work on a fully prepared request. + p.prepare_auth(self.auth) - history.append(r) + return p - url = r.headers['location'] - data = self.data - files = self.files - # Handle redirection without scheme (see: RFC 1808 Section 4) - if url.startswith('//'): - parsed_rurl = urlparse(r.url) - url = '%s:%s' % (parsed_rurl.scheme, url) +class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): + """The fully mutable :class:`PreparedRequest <PreparedRequest>` object, + containing the exact bytes that will be sent to the server. - # Facilitate non-RFC2616-compliant 'location' headers - # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') - if not urlparse(url).netloc: - url = urljoin(r.url, - # Compliant with RFC3986, we percent - # encode the url. - requote_uri(url)) + Generated from either a :class:`Request <Request>` object or manually. - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 - if r.status_code is codes.see_other: - method = 'GET' - data = None - files = None - else: - method = self.method - - # Do what the browsers do if strict_mode is off... - if (not self.config.get('strict_mode')): - - if r.status_code in (codes.moved, codes.found) and self.method == 'POST': - method = 'GET' - data = None - files = None - - if (r.status_code == 303) and self.method != 'HEAD': - method = 'GET' - data = None - files = None - - # Remove the cookie headers that were sent. - headers = self.headers - try: - del headers['Cookie'] - except KeyError: - pass - - request = Request( - url=url, - headers=headers, - files=files, - method=method, - params=self.session.params, - auth=self.auth, - cookies=self.cookies, - redirect=True, - data=data, - config=self.config, - timeout=self.timeout, - _poolmanager=self._poolmanager, - proxies=self.proxies, - verify=self.verify, - session=self.session, - cert=self.cert - ) - - request.send() - r = request.response - - r.history = history - - self.response = r - self.response.request = self + Usage:: - @staticmethod - def _encode_params(data): - """Encode parameters in a piece of data. + >>> import requests + >>> req = requests.Request('GET', 'http://httpbin.org/get') + >>> r = req.prepare() + <PreparedRequest [GET]> - Will successfully encode parameters when passed as a dict or a list of - 2-tuples. Order is retained if data is a list of 2-tuples but abritrary - if parameters are supplied as a dict. - """ + >>> s = requests.Session() + >>> s.send(r) + <Response [200]> - if isinstance(data, bytes): - return data - if isinstance(data, str): - return data - elif hasattr(data, '__iter__'): - try: - dict(data) - except ValueError: - raise ValueError('Unable to encode lists with elements that are not 2-tuples.') + """ - params = list(data.items() if isinstance(data, dict) else data) - result = [] - for k, vs in params: - for v in isinstance(vs, list) and vs or [vs]: - 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 + 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 + #: request body to send to the server. + self.body = None + #: dictionary of callback hooks, for internal usage. + self.hooks = default_hooks() - def _encode_files(self, files): + def __repr__(self): + return '<PreparedRequest [%s]>' % (self.method) - if (not files) or isinstance(self.data, str): - return None + def prepare_method(self, method): + """Prepares the given HTTP method.""" + self.method = method + if self.method is not None: + self.method = self.method.upper() + def prepare_url(self, url, params): + """Prepares the given HTTP URL.""" + #: Accept objects that have string representations. try: - fields = self.data.copy() - except AttributeError: - fields = dict(self.data) - - for (k, v) in list(files.items()): - # support for explicit filename - if isinstance(v, (tuple, list)): - fn, fp = v - else: - fn = guess_filename(v) or k - fp = v - if isinstance(fp, (bytes, str)): - fp = StringIO(fp) - fields.update({k: (fn, fp.read())}) - - (body, content_type) = encode_multipart_formdata(fields) - - return (body, content_type) - - @property - def full_url(self): - """Build the actual URL to use.""" - - if not self.url: - raise URLRequired() - - url = self.url + url = unicode(url) + except NameError: + # We're on Python 3. + url = str(url) + except UnicodeDecodeError: + pass # Support for unicode domain names and paths. - scheme, netloc, path, params, query, fragment = urlparse(url) + scheme, netloc, path, _params, query, fragment = urlparse(url) if not scheme: raise MissingSchema("Invalid URL %r: No schema supplied" % url) - if not scheme in SCHEMAS: - raise InvalidSchema("Invalid scheme %r" % scheme) - - netloc = netloc.encode('idna').decode('utf-8') + try: + netloc = netloc.encode('idna').decode('utf-8') + except UnicodeError: + raise InvalidURL('URL has an invalid label.') + # Bare domains aren't valid URLs. if not path: path = '/' @@ -391,270 +302,127 @@ class Request(object): netloc = netloc.encode('utf-8') if isinstance(path, str): path = path.encode('utf-8') - if isinstance(params, str): - params = params.encode('utf-8') + if isinstance(_params, str): + _params = _params.encode('utf-8') if isinstance(query, str): query = query.encode('utf-8') if isinstance(fragment, str): fragment = fragment.encode('utf-8') - url = (urlunparse([scheme, netloc, path, params, query, fragment])) - - enc_params = self._encode_params(self.params) + enc_params = self._encode_params(params) if enc_params: - if urlparse(url).query: - url = '%s&%s' % (url, enc_params) + if query: + query = '%s&%s' % (query, enc_params) else: - url = '%s?%s' % (url, enc_params) - - if self.config.get('encode_uri', True): - url = requote_uri(url) - - return url - - @property - def path_url(self): - """Build the path URL to use.""" - - url = [] - - p = urlsplit(self.full_url) - - # Proxies use full URLs. - if p.scheme in self.proxies: - return self.full_url + query = enc_params - path = p.path - if not path: - path = '/' - - url.append(path) - - query = p.query - if query: - url.append('?') - url.append(query) - - return ''.join(url) - - def register_hook(self, event, hook): - """Properly register a hook.""" - - self.hooks[event].append(hook) - - def deregister_hook(self, event, hook): - """Deregister a previously registered hook. - Returns True if the hook existed, False if not. - """ - - try: - self.hooks[event].remove(hook) - return True - except ValueError: - return False - - def send(self, anyway=False, prefetch=False): - """Sends the request. Returns True of successful, False if not. - If there was an HTTPError during transmission, - self.response.status_code will contain the HTTPError code. + url = requote_uri(urlunparse([scheme, netloc, path, _params, query, fragment])) + self.url = url - Once a request is successfully sent, `sent` will equal True. + def prepare_headers(self, headers): + """Prepares the given HTTP headers.""" - :param anyway: If True, request will be sent, even if it has - already been sent. - """ - - # Build the URL - url = self.full_url + if headers: + self.headers = CaseInsensitiveDict(headers) + else: + self.headers = CaseInsensitiveDict() - # Pre-request hook. - r = dispatch_hook('pre_request', self.hooks, self) - self.__dict__.update(r.__dict__) + def prepare_body(self, data, files): + """Prepares the given HTTP body data.""" - # Logging - if self.config.get('verbose'): - self.config.get('verbose').write('%s %s %s\n' % ( - datetime.now().isoformat(), self.method, url - )) + # Check if file, fo, generator, iterator. + # If not, run through normal process. # Nottin' on you. body = None content_type = None + length = None + is_stream = False - # Use .netrc auth if none was provided. - if not self.auth and self.config.get('trust_env'): - self.auth = get_netrc_auth(url) - - if self.auth: - if isinstance(self.auth, tuple) and len(self.auth) == 2: - # special-case basic HTTP auth - self.auth = HTTPBasicAuth(*self.auth) - - # Allow auth to make its changes. - r = self.auth(self) - - # Update self to reflect the auth changes. - self.__dict__.update(r.__dict__) - - # Multi-part file uploads. - if self.files: - (body, content_type) = self._encode_files(self.files) - else: - if self.data: - - body = self._encode_params(self.data) - if isinstance(self.data, str): - content_type = None - else: - content_type = 'application/x-www-form-urlencoded' - - # Add content-type if it wasn't explicitly provided. - if (content_type) and (not 'content-type' in self.headers): - self.headers['Content-Type'] = content_type - - _p = urlparse(url) - proxy = self.proxies.get(_p.scheme) - - if proxy: - conn = poolmanager.proxy_from_url(proxy) - _proxy = urlparse(proxy) - if '@' in _proxy.netloc: - auth, url = _proxy.netloc.split('@', 1) - self.proxy_auth = HTTPProxyAuth(*auth.split(':', 1)) - r = self.proxy_auth(self) - self.__dict__.update(r.__dict__) - else: - # Check to see if keep_alive is allowed. - try: - if self.config.get('keep_alive'): - conn = self._poolmanager.connection_from_url(url) - else: - conn = connectionpool.connection_from_url(url) - self.headers['Connection'] = 'close' - except LocationParseError as e: - raise InvalidURL(e) - - if url.startswith('https') and self.verify: - - cert_loc = None + is_stream = all([ + hasattr(data, '__iter__'), + not isinstance(data, basestring), + not isinstance(data, dict) + ]) - # Allow self-specified cert location. - if self.verify is not True: - cert_loc = self.verify - - # Look for configuration. - if not cert_loc and self.config.get('trust_env'): - cert_loc = os.environ.get('REQUESTS_CA_BUNDLE') + try: + length = super_len(data) + except (TypeError, AttributeError): + length = False - # Curl compatibility. - if not cert_loc and self.config.get('trust_env'): - cert_loc = os.environ.get('CURL_CA_BUNDLE') + if is_stream: + body = data - if not cert_loc: - cert_loc = DEFAULT_CA_BUNDLE_PATH + if files: + raise NotImplementedError('Streamed bodies and files are mutually exclusive.') - if not cert_loc: - raise Exception("Could not find a suitable SSL CA certificate bundle.") + if length: + self.headers['Content-Length'] = length + else: + self.headers['Transfer-Encoding'] = 'chunked' + # Check if file, fo, generator, iterator. + # If not, run through normal process. - conn.cert_reqs = 'CERT_REQUIRED' - conn.ca_certs = cert_loc else: - conn.cert_reqs = 'CERT_NONE' - conn.ca_certs = None - - if self.cert and self.verify: - if len(self.cert) == 2: - conn.cert_file = self.cert[0] - conn.key_file = self.cert[1] + # Multi-part file uploads. + if files: + (body, content_type) = self._encode_files(files, data) else: - conn.cert_file = self.cert - - if not self.sent or anyway: - - # Skip if 'cookie' header is explicitly set. - if 'cookie' not in self.headers: - cookie_header = get_cookie_header(self.cookies, self) - if cookie_header is not None: - self.headers['Cookie'] = cookie_header - - # Pre-send hook. - r = dispatch_hook('pre_send', self.hooks, self) - self.__dict__.update(r.__dict__) - - try: - # The inner try .. except re-raises certain exceptions as - # internal exception types; the outer suppresses exceptions - # when safe mode is set. - try: - # Send the request. - r = conn.urlopen( - method=self.method, - url=self.path_url, - body=body, - headers=self.headers, - redirect=False, - assert_same_host=False, - preload_content=False, - decode_content=False, - retries=self.config.get('max_retries', 0), - timeout=self.timeout, - ) - self.sent = True - - except MaxRetryError as e: - raise ConnectionError(e) - - except (_SSLError, _HTTPError) as e: - if self.verify and isinstance(e, _SSLError): - raise SSLError(e) - - raise Timeout('Request timed out.') - - except RequestException as e: - if self.config.get('safe_mode', False): - # In safe mode, catch the exception and attach it to - # a blank urllib3.HTTPResponse object. - r = HTTPResponse() - r.error = e - else: - raise - - # build_response can throw TooManyRedirects - try: - self._build_response(r) - except RequestException as e: - if self.config.get('safe_mode', False): - # In safe mode, catch the exception - self.response.error = e - else: - raise + if data: + body = self._encode_params(data) + if isinstance(data, str) or isinstance(data, builtin_str) or hasattr(data, 'read'): + content_type = None + else: + content_type = 'application/x-www-form-urlencoded' + + self.headers['Content-Length'] = '0' + if hasattr(body, 'seek') and hasattr(body, 'tell'): + body.seek(0, 2) + self.headers['Content-Length'] = str(body.tell()) + body.seek(0, 0) + elif body is not None: + self.headers['Content-Length'] = str(len(body)) + + # Add content-type if it wasn't explicitly provided. + if (content_type) and (not 'content-type' in self.headers): + self.headers['Content-Type'] = content_type + + self.body = body + + def prepare_auth(self, auth): + """Prepares the given HTTP auth data.""" + if auth: + if isinstance(auth, tuple) and len(auth) == 2: + # special-case basic HTTP auth + auth = HTTPBasicAuth(*auth) - # Response manipulation hook. - self.response = dispatch_hook('response', self.hooks, self.response) + # Allow auth to make its changes. + r = auth(self) - # Post-request hook. - r = dispatch_hook('post_request', self.hooks, self) + # Update self to reflect the auth changes. self.__dict__.update(r.__dict__) - # If prefetch is True, mark content as consumed. - if prefetch or self.prefetch: - # Save the response. - self.response.content + def prepare_cookies(self, cookies): + """Prepares the given HTTP cookie data.""" - if self.config.get('danger_mode'): - self.response.raise_for_status() + if isinstance(cookies, cookielib.CookieJar): + cookies = cookies + else: + cookies = cookiejar_from_dict(cookies) - return self.sent + if 'cookie' not in self.headers: + cookie_header = get_cookie_header(cookies, self) + if cookie_header is not None: + self.headers['Cookie'] = cookie_header class Response(object): - """The core :class:`Response <Response>` object. All - :class:`Request <Request>` objects contain a - :class:`response <Response>` attribute, which is an instance - of this class. + """The :class:`Response <Response>` object, which contains a + server's response to an HTTP request. """ def __init__(self): + super(Response, self).__init__() self._content = False self._content_consumed = False @@ -668,30 +436,25 @@ class Response(object): self.headers = CaseInsensitiveDict() #: File-like object representation of response (for advanced usage). + #: Requires that ``stream=True` on the request. + # This requirement does not apply for use internally to Requests. self.raw = None #: Final URL location of Response. self.url = None - #: Resulting :class:`HTTPError` of request, if one occurred. - self.error = None - - #: Encoding to decode with when accessing r.content. + #: 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. + #: up here. The list is sorted from the oldest to the most recent request. self.history = [] - #: The :class:`Request <Request>` that created the Response. - self.request = None + self.reason = None #: A CookieJar of Cookies the server sent back. - self.cookies = None - - #: Dictionary of configurations for this request. - self.config = {} + self.cookies = cookiejar_from_dict({}) def __repr__(self): return '<Response [%s]>' % (self.status_code) @@ -712,6 +475,12 @@ class Response(object): return False return True + @property + def apparent_encoding(self): + """The apparent encoding, provided by the lovely Charade library + (Thanks, Ian!).""" + return chardet.detect(self.content)['encoding'] + def iter_content(self, chunk_size=1, decode_unicode=False): """Iterates over the response data. This avoids reading the content at once into memory for large responses. The chunk size is the number @@ -719,9 +488,8 @@ class Response(object): length of each item returned as decoding can take place. """ if self._content_consumed: - raise RuntimeError( - 'The content for this response was already consumed' - ) + # simulate reading small chunks of the content + return iter_slices(self._content, chunk_size) def generate(): while 1: @@ -738,7 +506,7 @@ class Response(object): return gen - def iter_lines(self, chunk_size=10 * 1024, decode_unicode=None): + def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None): """Iterates over the response data, one line at a time. This avoids reading the content at once into memory for large responses. @@ -785,6 +553,8 @@ class Response(object): self._content = None 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 @@ -799,48 +569,73 @@ class Response(object): content = None encoding = self.encoding + if not self.content: + return str('') + # Fallback to auto-detected encoding. if self.encoding is None: - if chardet is not None: - encoding = chardet.detect(self.content)['encoding'] + encoding = self.apparent_encoding # Decode unicode from given encoding. try: content = str(self.content, encoding, errors='replace') - except LookupError: + 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 - @property def json(self): - """Returns the json-encoded content of a request, if any.""" - try: - return json.loads(self.text or self.content) - except ValueError: - return None + """Returns the json-encoded content of a response, if any.""" + + if not self.encoding 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 chardet to make + # a best guess). + encoding = guess_json_utf(self.content) + if encoding is not None: + return json.loads(self.content.decode(encoding)) + return json.loads(self.text or self.content) + + @property + def links(self): + """Returns the parsed header links of the response, if any.""" + + header = self.headers['link'] + + # l = MultiDict() + l = {} + + if header: + links = parse_header_links(header) - def raise_for_status(self, allow_redirects=True): + for link in links: + key = link.get('rel') or link.get('url') + l[key] = link + + return l + + def raise_for_status(self): """Raises stored :class:`HTTPError` or :class:`URLError`, if one occurred.""" - if self.error: - raise self.error + http_error_msg = '' - if (self.status_code >= 300) and (self.status_code < 400) and not allow_redirects: - http_error = HTTPError('%s Redirection' % self.status_code) - http_error.response = self - raise http_error + if 400 <= self.status_code < 500: + http_error_msg = '%s Client Error: %s' % (self.status_code, self.reason) - elif (self.status_code >= 400) and (self.status_code < 500): - http_error = HTTPError('%s Client Error' % self.status_code) - http_error.response = self - raise http_error + elif 500 <= self.status_code < 600: + http_error_msg = '%s Server Error: %s' % (self.status_code, self.reason) - elif (self.status_code >= 500) and (self.status_code < 600): - http_error = HTTPError('%s Server Error' % self.status_code) + if http_error_msg: + http_error = HTTPError(http_error_msg) http_error.response = self raise http_error + + def close(self): + return self.raw.release_conn() |