aboutsummaryrefslogtreecommitdiff
path: root/requests/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'requests/models.py')
-rw-r--r--requests/models.py201
1 files changed, 138 insertions, 63 deletions
diff --git a/requests/models.py b/requests/models.py
index 099f1c6..2d7fc8f 100644
--- a/requests/models.py
+++ b/requests/models.py
@@ -12,18 +12,21 @@ import socket
import zlib
from urllib2 import HTTPError
-from urlparse import urlparse
+from urlparse import urlparse, urlunparse, urljoin
from datetime import datetime
from .config import settings
-from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPDigestAuthHandler, HTTPRedirectHandler
+from .monkeys import Request as _Request, HTTPBasicAuthHandler, HTTPForcedBasicAuthHandler, HTTPDigestAuthHandler, HTTPRedirectHandler
from .structures import CaseInsensitiveDict
from .packages.poster.encode import multipart_encode
from .packages.poster.streaminghttp import register_openers, get_handlers
-from .exceptions import RequestException, AuthenticationError, Timeout, URLRequired, InvalidMethod
+from .utils import dict_from_cookiejar
+from .exceptions import RequestException, AuthenticationError, Timeout, URLRequired, InvalidMethod, TooManyRedirects
+from .status_codes import codes
-REDIRECT_STATI = (301, 302, 303, 307)
+REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved)
+
class Request(object):
@@ -31,34 +34,42 @@ class Request(object):
Requests. Recommended interface is with the Requests functions.
"""
- _METHODS = ('GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH')
-
def __init__(self,
url=None, headers=dict(), files=None, method=None, data=dict(),
params=dict(), auth=None, cookiejar=None, timeout=None, redirect=False,
allow_redirects=False, proxies=None):
- socket.setdefaulttimeout(timeout)
+ #: Float describ the timeout of the request.
+ # (Use socket.setdefaulttimeout() as fallback)
+ self.timeout = timeout
#: Request URL.
self.url = url
+
#: Dictonary of HTTP Headers to attach to the :class:`Request <models.Request>`.
self.headers = headers
+
#: Dictionary of files to multipart upload (``{filename: content}``).
self.files = files
+
#: HTTP Method to use. Available: GET, HEAD, PUT, POST, DELETE.
self.method = method
+
#: Dictionary or byte of request body data to attach to the
#: :class:`Request <models.Request>`.
self.data = None
+
#: Dictionary or byte of querystring data to attach to the
#: :class:`Request <models.Request>`.
self.params = None
+
#: True if :class:`Request <models.Request>` is part of a redirect chain (disables history
#: and HTTPError storage).
self.redirect = redirect
+
#: Set to True if full redirects are allowed (e.g. re-POST-ing of data at new ``Location``)
self.allow_redirects = allow_redirects
+
# Dictionary mapping protocol to the URL of the proxy (e.g. {'http': 'foo.bar:3128'})
self.proxies = proxies
@@ -73,24 +84,36 @@ class Request(object):
auth = AuthObject(*auth)
if not auth:
auth = auth_manager.get_auth(self.url)
+
#: :class:`AuthObject` to attach to :class:`Request <models.Request>`.
self.auth = auth
+
#: CookieJar to attach to :class:`Request <models.Request>`.
self.cookiejar = cookiejar
+
#: True if Request has been sent.
self.sent = False
- def __repr__(self):
- return '<Request [%s]>' % (self.method)
+ # Header manipulation and defaults.
+ if settings.accept_gzip:
+ settings.base_headers.update({'Accept-Encoding': 'gzip'})
- def __setattr__(self, name, value):
- if (name == 'method') and (value):
- if not value in self._METHODS:
- raise InvalidMethod()
+ if headers:
+ headers = CaseInsensitiveDict(self.headers)
+ else:
+ headers = CaseInsensitiveDict()
+
+ for (k, v) in settings.base_headers.items():
+ if k not in headers:
+ headers[k] = v
+
+ self.headers = headers
- object.__setattr__(self, name, value)
+
+ def __repr__(self):
+ return '<Request [%s]>' % (self.method)
def _checks(self):
@@ -110,6 +133,7 @@ class Request(object):
if self.auth:
if not isinstance(self.auth.handler, (urllib2.AbstractBasicAuthHandler, urllib2.AbstractDigestAuthHandler)):
+ # TODO: REMOVE THIS COMPLETELY
auth_manager.add_password(self.auth.realm, self.url, self.auth.username, self.auth.password)
self.auth.handler = self.auth.handler(auth_manager)
auth_manager.add_auth(self.url, self.auth)
@@ -141,7 +165,7 @@ class Request(object):
return opener.open
- def _build_response(self, resp):
+ def _build_response(self, resp, is_error=False):
"""Build internal :class:`Response <models.Response>` object from given response."""
def build(resp):
@@ -151,17 +175,20 @@ class Request(object):
try:
response.headers = CaseInsensitiveDict(getattr(resp.info(), 'dict', None))
- response.content = resp.read()
+ response.read = resp.read
+ response._resp = resp
+ response._close = resp.close
+
+ if self.cookiejar:
+
+ response.cookies = dict_from_cookiejar(self.cookiejar)
+
+
except AttributeError:
pass
- if response.headers['content-encoding'] == 'gzip':
- try:
- response.content = zlib.decompress(response.content, 16+zlib.MAX_WBITS)
- except zlib.error:
- pass
-
- # TODO: Support deflate
+ if is_error:
+ response.error = resp
response.url = getattr(resp, 'url', None)
@@ -172,30 +199,36 @@ class Request(object):
r = build(resp)
- if r.status_code in REDIRECT_STATI:
- self.redirect = True
-
- if self.redirect:
+ if r.status_code in REDIRECT_STATI and not self.redirect:
while (
('location' in r.headers) and
((self.method in ('GET', 'HEAD')) or
- (r.status_code is 303) or
+ (r.status_code is codes.see_other) or
(self.allow_redirects))
):
+ r.close()
+
+ if not len(history) < settings.max_redirects:
+ raise TooManyRedirects()
+
history.append(r)
url = r.headers['location']
+ # Handle redirection without scheme (see: RFC 1808 Section 4)
+ if url.startswith('//'):
+ parsed_rurl = urlparse(r.url)
+ url = '%s:%s' % (parsed_rurl.scheme, url)
+
# Facilitate non-RFC2616-compliant 'location' headers
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
if not urlparse(url).netloc:
- parent_url_components = urlparse(self.url)
- url = '%s://%s/%s' % (parent_url_components.scheme, parent_url_components.netloc, url)
+ url = urljoin(r.url, urllib.quote(urllib.unquote(url)))
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
- if r.status_code is 303:
+ if r.status_code is codes.see_other:
method = 'GET'
else:
method = self.method
@@ -211,6 +244,7 @@ class Request(object):
r.history = history
self.response = r
+ self.response.request = self
@staticmethod
@@ -218,25 +252,34 @@ class Request(object):
"""Encode parameters in a piece of data.
If the data supplied is a dictionary, encodes each parameter in it, and
- returns the dictionary of encoded parameters, and a urlencoded version
- of that.
+ returns a list of tuples containing the encoded parameters, and a urlencoded
+ version of that.
Otherwise, assumes the data is already encoded appropriately, and
returns it twice.
"""
if hasattr(data, 'items'):
- result = {}
- for (k, v) in data.items():
- result[k.encode('utf-8') if isinstance(k, unicode) else k] \
- = v.encode('utf-8') if isinstance(v, unicode) else v
- return result, urllib.urlencode(result)
+ result = []
+ for k, vs in data.items():
+ for v in isinstance(vs, list) and vs or [vs]:
+ result.append((k.encode('utf-8') if isinstance(k, unicode) else k,
+ v.encode('utf-8') if isinstance(v, unicode) else v))
+ return result, urllib.urlencode(result, doseq=True)
else:
return data, data
def _build_url(self):
- """Build the actual URL to use"""
+ """Build the actual URL to use."""
+
+ # Support for unicode domain names and paths.
+ scheme, netloc, path, params, query, fragment = urlparse(self.url)
+ netloc = netloc.encode('idna')
+ if isinstance(path, unicode):
+ path = path.encode('utf-8')
+ path = urllib.quote(urllib.unquote(path))
+ self.url = str(urlunparse([ scheme, netloc, path, params, query, fragment ]))
if self._enc_params:
if urlparse(self.url).query:
@@ -257,6 +300,7 @@ class Request(object):
:param anyway: If True, request will be sent, even if it has
already been sent.
"""
+
self._checks()
success = False
@@ -285,13 +329,32 @@ class Request(object):
req = _Request(url, data=self._enc_data, method=self.method)
if self.headers:
- req.headers.update(self.headers)
+ for k,v in self.headers.iteritems():
+ req.add_header(k, v)
if not self.sent or anyway:
try:
opener = self._get_opener()
- resp = opener(req)
+ try:
+
+ resp = opener(req, timeout=self.timeout)
+
+ except TypeError, err:
+ # timeout argument is new since Python v2.6
+ if not 'timeout' in str(err):
+ raise
+
+ if settings.timeout_fallback:
+ # fall-back and use global socket timeout (This is not thread-safe!)
+ old_timeout = socket.getdefaulttimeout()
+ socket.setdefaulttimeout(self.timeout)
+
+ resp = opener(req)
+
+ if settings.timeout_fallback:
+ # restore gobal timeout
+ socket.setdefaulttimeout(old_timeout)
if self.cookiejar is not None:
self.cookiejar.extract_cookies(resp, req)
@@ -301,28 +364,19 @@ class Request(object):
if isinstance(why.reason, socket.timeout):
why = Timeout(why)
- self._build_response(why)
- if not self.redirect:
- self.response.error = why
+ self._build_response(why, is_error=True)
+
else:
self._build_response(resp)
self.response.ok = True
- self.response.cached = False
- else:
- self.response.cached = True
self.sent = self.response.ok
-
return self.sent
- def read(self, *args):
- return self.response.read()
-
-
class Response(object):
"""The core :class:`Response <models.Response>` object. All
@@ -335,7 +389,7 @@ class Response(object):
#: Raw content of the response, in bytes.
#: If ``content-encoding`` of response was set to ``gzip``, the
#: response data will be automatically deflated.
- self.content = None
+ self._content = None
#: Integer Code of responded HTTP Status.
self.status_code = None
#: Case-insensitive Dictionary of Response Headers.
@@ -348,12 +402,14 @@ class Response(object):
self.ok = False
#: Resulting :class:`HTTPError` of request, if one occured.
self.error = None
- #: True, if the response :attr:`content` is cached locally.
- self.cached = False
#: A list of :class:`Response <models.Response>` objects from
#: the history of the Request. Any redirect responses will end
#: up here.
self.history = []
+ #: The Request that created the Response.
+ self.request = None
+ #: A dictionary of Cookies the server sent back.
+ self.cookies = None
def __repr__(self):
@@ -365,17 +421,31 @@ class Response(object):
return not self.error
+ def __getattr__(self, name):
+ """Read and returns the full stream when accessing to :attr: `content`"""
+ if name == 'content':
+ if self._content is not None:
+ return self._content
+ self._content = self.read()
+ if self.headers.get('content-encoding', '') == 'gzip':
+ try:
+ self._content = zlib.decompress(self._content, 16+zlib.MAX_WBITS)
+ except zlib.error:
+ pass
+ return self._content
+ else:
+ raise AttributeError
+
def raise_for_status(self):
- """Raises stored :class:`HTTPError`, if one occured."""
+ """Raises stored :class:`HTTPError` or :class:`URLError`, if one occured."""
if self.error:
raise self.error
- def read(self, *args):
- """Returns :attr:`content`. Used for file-like object compatiblity."""
-
- return self.content
-
+ def close(self):
+ if self._resp.fp is not None and hasattr(self._resp.fp, '_sock'):
+ self._resp.fp._sock.recv = None
+ self._close()
class AuthManager(object):
"""Requests Authentication Manager."""
@@ -450,8 +520,10 @@ class AuthManager(object):
def reduce_uri(self, uri, default_port=True):
"""Accept authority or URI and extract only the authority and path."""
+
# note HTTP URLs do not have a userinfo component
parts = urllib2.urlparse.urlsplit(uri)
+
if parts[1]:
# URI
scheme = parts[0]
@@ -462,7 +534,9 @@ class AuthManager(object):
scheme = None
authority = uri
path = '/'
+
host, port = urllib2.splitport(authority)
+
if default_port and port is None and scheme is not None:
dport = {"http": 80,
"https": 443,
@@ -532,17 +606,18 @@ class AuthObject(object):
_handlers = {
'basic': HTTPBasicAuthHandler,
+ 'forced_basic': HTTPForcedBasicAuthHandler,
'digest': HTTPDigestAuthHandler,
'proxy_basic': urllib2.ProxyBasicAuthHandler,
'proxy_digest': urllib2.ProxyDigestAuthHandler
}
- def __init__(self, username, password, handler='basic', realm=None):
+ def __init__(self, username, password, handler='forced_basic', realm=None):
self.username = username
self.password = password
self.realm = realm
if isinstance(handler, basestring):
- self.handler = self._handlers.get(handler.lower(), urllib2.HTTPBasicAuthHandler)
+ self.handler = self._handlers.get(handler.lower(), HTTPForcedBasicAuthHandler)
else:
self.handler = handler