Update requests

This commit is contained in:
Ruud committed 2015-01-11 16:17:33 +01:00
1 parent e1bb8c5419
commit e1e39cd3f4
19 files changed
+521 -191

No files matched your search

+4 -4
View File
@@ -13,7 +13,7 @@ Requests is an HTTP library, written in Python, for human beings. Basic GET
usage: usage:
>>> import requests >>> import requests
>>> r = requests.get('http://python.org') >>> r = requests.get('https://www.python.org')
>>> r.status_code >>> r.status_code
200 200
>>> 'Python is a programming language' in r.content >>> 'Python is a programming language' in r.content
@@ -22,7 +22,7 @@ usage:
... or POST: ... or POST:
>>> payload = dict(key1='value1', key2='value2') >>> payload = dict(key1='value1', key2='value2')
>>> r = requests.post("http://httpbin.org/post", data=payload) >>> r = requests.post('http://httpbin.org/post', data=payload)
>>> print(r.text) >>> print(r.text)
{ {
... ...
@@ -42,8 +42,8 @@ is at <http://python-requests.org>.
""" """
__title__ = 'requests' __title__ = 'requests'
__version__ = '2.4.0' __version__ = '2.5.1'
__build__ = 0x020400 __build__ = 0x020501
__author__ = 'Kenneth Reitz' __author__ = 'Kenneth Reitz'
__license__ = 'Apache 2.0' __license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2014 Kenneth Reitz' __copyright__ = 'Copyright 2014 Kenneth Reitz'
+27 -13
View File
@@ -15,19 +15,21 @@ from .packages.urllib3 import Retry
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
from .packages.urllib3.response import HTTPResponse from .packages.urllib3.response import HTTPResponse
from .packages.urllib3.util import Timeout as TimeoutSauce from .packages.urllib3.util import Timeout as TimeoutSauce
from .compat import urlparse, basestring, urldefrag from .compat import urlparse, basestring
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers, from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
prepend_scheme_if_needed, get_auth_from_url) prepend_scheme_if_needed, get_auth_from_url, urldefragauth)
from .structures import CaseInsensitiveDict from .structures import CaseInsensitiveDict
from .packages.urllib3.exceptions import ConnectTimeoutError from .packages.urllib3.exceptions import ConnectTimeoutError
from .packages.urllib3.exceptions import HTTPError as _HTTPError from .packages.urllib3.exceptions import HTTPError as _HTTPError
from .packages.urllib3.exceptions import MaxRetryError from .packages.urllib3.exceptions import MaxRetryError
from .packages.urllib3.exceptions import ProxyError as _ProxyError from .packages.urllib3.exceptions import ProxyError as _ProxyError
from .packages.urllib3.exceptions import ProtocolError
from .packages.urllib3.exceptions import ReadTimeoutError from .packages.urllib3.exceptions import ReadTimeoutError
from .packages.urllib3.exceptions import SSLError as _SSLError from .packages.urllib3.exceptions import SSLError as _SSLError
from .packages.urllib3.exceptions import ResponseError
from .cookies import extract_cookies_to_jar from .cookies import extract_cookies_to_jar
from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError, from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
ProxyError) ProxyError, RetryError)
from .auth import _basic_auth_str from .auth import _basic_auth_str
DEFAULT_POOLBLOCK = False DEFAULT_POOLBLOCK = False
@@ -59,8 +61,12 @@ class HTTPAdapter(BaseAdapter):
:param pool_connections: The number of urllib3 connection pools to cache. :param pool_connections: The number of urllib3 connection pools to cache.
:param pool_maxsize: The maximum number of connections to save in the pool. :param pool_maxsize: The maximum number of connections to save in the pool.
:param int max_retries: The maximum number of retries each connection :param int max_retries: The maximum number of retries each connection
should attempt. Note, this applies only to failed connections and should attempt. Note, this applies only to failed DNS lookups, socket
timeouts, never to requests where the server returns a response. connections and connection timeouts, never to requests where data has
made it to the server. By default, Requests does not retry failed
connections. If you need granular control over the conditions under
which we retry a request, import urllib3's ``Retry`` class and pass
that instead.
:param pool_block: Whether the connection pool should block for connections. :param pool_block: Whether the connection pool should block for connections.
Usage:: Usage::
@@ -76,7 +82,10 @@ class HTTPAdapter(BaseAdapter):
def __init__(self, pool_connections=DEFAULT_POOLSIZE, def __init__(self, pool_connections=DEFAULT_POOLSIZE,
pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES, pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
pool_block=DEFAULT_POOLBLOCK): pool_block=DEFAULT_POOLBLOCK):
self.max_retries = max_retries if max_retries == DEFAULT_RETRIES:
self.max_retries = Retry(0, read=False)
else:
self.max_retries = Retry.from_int(max_retries)
self.config = {} self.config = {}
self.proxy_manager = {} self.proxy_manager = {}
@@ -122,7 +131,7 @@ class HTTPAdapter(BaseAdapter):
self._pool_block = block self._pool_block = block
self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
block=block, **pool_kwargs) block=block, strict=True, **pool_kwargs)
def proxy_manager_for(self, proxy, **proxy_kwargs): def proxy_manager_for(self, proxy, **proxy_kwargs):
"""Return urllib3 ProxyManager for the given proxy. """Return urllib3 ProxyManager for the given proxy.
@@ -269,7 +278,7 @@ class HTTPAdapter(BaseAdapter):
proxy = proxies.get(scheme) proxy = proxies.get(scheme)
if proxy and scheme != 'https': if proxy and scheme != 'https':
url, _ = urldefrag(request.url) url = urldefragauth(request.url)
else: else:
url = request.path_url url = request.path_url
@@ -316,8 +325,10 @@ class HTTPAdapter(BaseAdapter):
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent. :param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
:param stream: (optional) Whether to stream the request content. :param stream: (optional) Whether to stream the request content.
:param timeout: (optional) The timeout on the request. :param timeout: (optional) How long to wait for the server to send
:type timeout: float or tuple (connect timeout, read timeout), eg (3.1, 20) data before giving up, as a float, or a (`connect timeout, read
timeout <user/advanced.html#timeouts>`_) tuple.
:type timeout: float or tuple
:param verify: (optional) Whether to verify SSL certificates. :param verify: (optional) Whether to verify SSL certificates.
:param cert: (optional) Any user-provided SSL certificate to be trusted. :param cert: (optional) Any user-provided SSL certificate to be trusted.
:param proxies: (optional) The proxies dictionary to apply to the request. :param proxies: (optional) The proxies dictionary to apply to the request.
@@ -355,7 +366,7 @@ class HTTPAdapter(BaseAdapter):
assert_same_host=False, assert_same_host=False,
preload_content=False, preload_content=False,
decode_content=False, decode_content=False,
retries=Retry(self.max_retries, read=False), retries=self.max_retries,
timeout=timeout timeout=timeout
) )
@@ -400,13 +411,16 @@ class HTTPAdapter(BaseAdapter):
# All is well, return the connection to the pool. # All is well, return the connection to the pool.
conn._put_conn(low_conn) conn._put_conn(low_conn)
except socket.error as sockerr: except (ProtocolError, socket.error) as err:
raise ConnectionError(sockerr, request=request) raise ConnectionError(err, request=request)
except MaxRetryError as e: except MaxRetryError as e:
if isinstance(e.reason, ConnectTimeoutError): if isinstance(e.reason, ConnectTimeoutError):
raise ConnectTimeout(e, request=request) raise ConnectTimeout(e, request=request)
if isinstance(e.reason, ResponseError):
raise RetryError(e, request=request)
raise ConnectionError(e, request=request) raise ConnectionError(e, request=request)
except _ProxyError as e: except _ProxyError as e:
+16 -5
View File
@@ -22,12 +22,17 @@ def request(method, url, **kwargs):
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload. :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload.
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) Float describing the timeout of the request in seconds. :param timeout: (optional) How long to wait for the server to send data
before giving up, as a float, or a (`connect timeout, read timeout
<user/advanced.html#timeouts>`_) tuple.
:type timeout: float or tuple
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed. :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param stream: (optional) if ``False``, the response content will be immediately downloaded. :param stream: (optional) if ``False``, the response content will be immediately downloaded.
@@ -41,7 +46,12 @@ def request(method, url, **kwargs):
""" """
session = sessions.Session() session = sessions.Session()
return session.request(method=method, url=url, **kwargs) response = session.request(method=method, url=url, **kwargs)
# By explicitly closing the session, we avoid leaving sockets open which
# can trigger a ResourceWarning in some cases, and look like a memory leak
# in others.
session.close()
return response
def get(url, **kwargs): def get(url, **kwargs):
@@ -77,15 +87,16 @@ def head(url, **kwargs):
return request('head', url, **kwargs) return request('head', url, **kwargs)
def post(url, data=None, **kwargs): def post(url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object. """Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
""" """
return request('post', url, data=data, **kwargs) return request('post', url, data=data, json=json, **kwargs)
def put(url, data=None, **kwargs): def put(url, data=None, **kwargs):
+15 -3
View File
@@ -17,6 +17,7 @@ from base64 import b64encode
from .compat import urlparse, str from .compat import urlparse, str
from .cookies import extract_cookies_to_jar from .cookies import extract_cookies_to_jar
from .utils import parse_dict_header, to_native_string from .utils import parse_dict_header, to_native_string
from .status_codes import codes
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
CONTENT_TYPE_MULTI_PART = 'multipart/form-data' CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
@@ -66,6 +67,7 @@ class HTTPDigestAuth(AuthBase):
self.nonce_count = 0 self.nonce_count = 0
self.chal = {} self.chal = {}
self.pos = None self.pos = None
self.num_401_calls = 1
def build_digest_header(self, method, url): def build_digest_header(self, method, url):
@@ -150,6 +152,11 @@ class HTTPDigestAuth(AuthBase):
return 'Digest %s' % (base) return 'Digest %s' % (base)
def handle_redirect(self, r, **kwargs):
"""Reset num_401_calls counter on redirects."""
if r.is_redirect:
self.num_401_calls = 1
def handle_401(self, r, **kwargs): def handle_401(self, r, **kwargs):
"""Takes the given response and tries digest-auth, if needed.""" """Takes the given response and tries digest-auth, if needed."""
@@ -162,7 +169,7 @@ class HTTPDigestAuth(AuthBase):
if 'digest' in s_auth.lower() and num_401_calls < 2: if 'digest' in s_auth.lower() and num_401_calls < 2:
setattr(self, 'num_401_calls', num_401_calls + 1) self.num_401_calls += 1
pat = re.compile(r'digest ', flags=re.IGNORECASE) pat = re.compile(r'digest ', flags=re.IGNORECASE)
self.chal = parse_dict_header(pat.sub('', s_auth, count=1)) self.chal = parse_dict_header(pat.sub('', s_auth, count=1))
@@ -182,7 +189,7 @@ class HTTPDigestAuth(AuthBase):
return _r return _r
setattr(self, 'num_401_calls', 1) self.num_401_calls = 1
return r return r
def __call__(self, r): def __call__(self, r):
@@ -192,6 +199,11 @@ class HTTPDigestAuth(AuthBase):
try: try:
self.pos = r.body.tell() self.pos = r.body.tell()
except AttributeError: except AttributeError:
pass # In the case of HTTPDigestAuth being reused and the body of
# the previous request was a file-like object, pos has the
# file position of the previous body. Ensure it's set to
# None.
self.pos = None
r.register_hook('response', self.handle_401) r.register_hook('response', self.handle_401)
r.register_hook('response', self.handle_redirect)
return r return r
+1 -1
View File
@@ -76,7 +76,7 @@ is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
try: try:
import simplejson as json import simplejson as json
except (ImportError, SyntaxError): except (ImportError, SyntaxError):
# simplejson does not support Python 3.2, it thows a SyntaxError # simplejson does not support Python 3.2, it throws a SyntaxError
# because of u'...' Unicode literals. # because of u'...' Unicode literals.
import json import json
+13 -4
View File
@@ -46,15 +46,16 @@ class SSLError(ConnectionError):
class Timeout(RequestException): class Timeout(RequestException):
"""The request timed out. """The request timed out.
Catching this error will catch both :exc:`ConnectTimeout` and Catching this error will catch both
:exc:`ReadTimeout` errors. :exc:`~requests.exceptions.ConnectTimeout` and
:exc:`~requests.exceptions.ReadTimeout` errors.
""" """
class ConnectTimeout(ConnectionError, Timeout): class ConnectTimeout(ConnectionError, Timeout):
"""The request timed out while trying to connect to the server. """The request timed out while trying to connect to the remote server.
Requests that produce this error are safe to retry Requests that produced this error are safe to retry.
""" """
@@ -88,3 +89,11 @@ class ChunkedEncodingError(RequestException):
class ContentDecodingError(RequestException, BaseHTTPError): class ContentDecodingError(RequestException, BaseHTTPError):
"""Failed to decode response content""" """Failed to decode response content"""
class StreamConsumedError(RequestException, TypeError):
"""The content for this response was already consumed"""
class RetryError(RequestException):
"""Custom retries logic failed"""
+46 -23
View File
@@ -20,10 +20,10 @@ from .packages.urllib3.fields import RequestField
from .packages.urllib3.filepost import encode_multipart_formdata from .packages.urllib3.filepost import encode_multipart_formdata
from .packages.urllib3.util import parse_url from .packages.urllib3.util import parse_url
from .packages.urllib3.exceptions import ( from .packages.urllib3.exceptions import (
DecodeError, ReadTimeoutError, ProtocolError) DecodeError, ReadTimeoutError, ProtocolError, LocationParseError)
from .exceptions import ( from .exceptions import (
HTTPError, RequestException, MissingSchema, InvalidURL, HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,
ChunkedEncodingError, ContentDecodingError, ConnectionError) ContentDecodingError, ConnectionError, StreamConsumedError)
from .utils import ( from .utils import (
guess_filename, get_auth_from_url, requote_uri, guess_filename, get_auth_from_url, requote_uri,
stream_decode_response_unicode, to_key_val_list, parse_header_links, stream_decode_response_unicode, to_key_val_list, parse_header_links,
@@ -46,6 +46,8 @@ DEFAULT_REDIRECT_LIMIT = 30
CONTENT_CHUNK_SIZE = 10 * 1024 CONTENT_CHUNK_SIZE = 10 * 1024
ITER_CHUNK_SIZE = 512 ITER_CHUNK_SIZE = 512
json_dumps = json.dumps
class RequestEncodingMixin(object): class RequestEncodingMixin(object):
@property @property
@@ -189,7 +191,8 @@ class Request(RequestHooksMixin):
:param url: URL to send. :param url: URL to send.
:param headers: dictionary of headers to send. :param headers: dictionary of headers to send.
:param files: dictionary of {filename: fileobject} files to multipart upload. :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 data: the body to attach to the request. If a dictionary is provided, form-encoding will take place.
:param json: json for the body to attach to the request (if data is not specified).
:param params: dictionary of URL parameters to append to the URL. :param params: dictionary of URL parameters to append to the URL.
:param auth: Auth handler or (user, pass) tuple. :param auth: Auth handler or (user, pass) tuple.
:param cookies: dictionary or CookieJar of cookies to attach to this request. :param cookies: dictionary or CookieJar of cookies to attach to this request.
@@ -212,7 +215,8 @@ class Request(RequestHooksMixin):
params=None, params=None,
auth=None, auth=None,
cookies=None, cookies=None,
hooks=None): hooks=None,
json=None):
# Default empty dicts for dict params. # Default empty dicts for dict params.
data = [] if data is None else data data = [] if data is None else data
@@ -230,6 +234,7 @@ class Request(RequestHooksMixin):
self.headers = headers self.headers = headers
self.files = files self.files = files
self.data = data self.data = data
self.json = json
self.params = params self.params = params
self.auth = auth self.auth = auth
self.cookies = cookies self.cookies = cookies
@@ -246,6 +251,7 @@ class Request(RequestHooksMixin):
headers=self.headers, headers=self.headers,
files=self.files, files=self.files,
data=self.data, data=self.data,
json=self.json,
params=self.params, params=self.params,
auth=self.auth, auth=self.auth,
cookies=self.cookies, cookies=self.cookies,
@@ -289,14 +295,15 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
self.hooks = default_hooks() self.hooks = default_hooks()
def prepare(self, method=None, url=None, headers=None, files=None, def prepare(self, method=None, url=None, headers=None, files=None,
data=None, params=None, auth=None, cookies=None, hooks=None): data=None, params=None, auth=None, cookies=None, hooks=None,
json=None):
"""Prepares the entire request with the given parameters.""" """Prepares the entire request with the given parameters."""
self.prepare_method(method) self.prepare_method(method)
self.prepare_url(url, params) self.prepare_url(url, params)
self.prepare_headers(headers) self.prepare_headers(headers)
self.prepare_cookies(cookies) self.prepare_cookies(cookies)
self.prepare_body(data, files) self.prepare_body(data, files, json)
self.prepare_auth(auth, url) self.prepare_auth(auth, url)
# Note that prepare_auth must be last to enable authentication schemes # Note that prepare_auth must be last to enable authentication schemes
# such as OAuth to work on a fully prepared request. # such as OAuth to work on a fully prepared request.
@@ -326,21 +333,27 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
def prepare_url(self, url, params): def prepare_url(self, url, params):
"""Prepares the given HTTP URL.""" """Prepares the given HTTP URL."""
#: Accept objects that have string representations. #: Accept objects that have string representations.
try: #: We're unable to blindy call unicode/str functions
url = unicode(url) #: as this will include the bytestring indicator (b'')
except NameError: #: on python 3.x.
# We're on Python 3. #: https://github.com/kennethreitz/requests/pull/2238
url = str(url) if isinstance(url, bytes):
except UnicodeDecodeError: url = url.decode('utf8')
pass else:
url = unicode(url) if is_py2 else str(url)
# Don't do any URL preparation for oddball schemes # Don't do any URL preparation for non-HTTP schemes like `mailto`,
# `data` etc to work around exceptions from `url_parse`, which
# handles RFC 3986 only.
if ':' in url and not url.lower().startswith('http'): if ':' in url and not url.lower().startswith('http'):
self.url = url self.url = url
return return
# Support for unicode domain names and paths. # Support for unicode domain names and paths.
scheme, auth, host, port, path, query, fragment = parse_url(url) try:
scheme, auth, host, port, path, query, fragment = parse_url(url)
except LocationParseError as e:
raise InvalidURL(*e.args)
if not scheme: if not scheme:
raise MissingSchema("Invalid URL {0!r}: No schema supplied. " raise MissingSchema("Invalid URL {0!r}: No schema supplied. "
@@ -397,7 +410,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
else: else:
self.headers = CaseInsensitiveDict() self.headers = CaseInsensitiveDict()
def prepare_body(self, data, files): def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data.""" """Prepares the given HTTP body data."""
# Check if file, fo, generator, iterator. # Check if file, fo, generator, iterator.
@@ -408,6 +421,10 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
content_type = None content_type = None
length = None length = None
if json is not None:
content_type = 'application/json'
body = json_dumps(json)
is_stream = all([ is_stream = all([
hasattr(data, '__iter__'), hasattr(data, '__iter__'),
not isinstance(data, (basestring, list, tuple, dict)) not isinstance(data, (basestring, list, tuple, dict))
@@ -433,7 +450,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
if files: if files:
(body, content_type) = self._encode_files(files, data) (body, content_type) = self._encode_files(files, data)
else: else:
if data: if data and json is None:
body = self._encode_params(data) body = self._encode_params(data)
if isinstance(data, basestring) or hasattr(data, 'read'): if isinstance(data, basestring) or hasattr(data, 'read'):
content_type = None content_type = None
@@ -443,7 +460,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
self.prepare_content_length(body) self.prepare_content_length(body)
# Add content-type if it wasn't explicitly provided. # Add content-type if it wasn't explicitly provided.
if (content_type) and (not 'content-type' in self.headers): if content_type and ('content-type' not in self.headers):
self.headers['Content-Type'] = content_type self.headers['Content-Type'] = content_type
self.body = body self.body = body
@@ -457,7 +474,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
l = super_len(body) l = super_len(body)
if l: if l:
self.headers['Content-Length'] = builtin_str(l) self.headers['Content-Length'] = builtin_str(l)
elif self.method not in ('GET', 'HEAD'): elif (self.method not in ('GET', 'HEAD')) and (self.headers.get('Content-Length') is None):
self.headers['Content-Length'] = '0' self.headers['Content-Length'] = '0'
def prepare_auth(self, auth, url=''): def prepare_auth(self, auth, url=''):
@@ -600,7 +617,7 @@ class Response(object):
def ok(self): def ok(self):
try: try:
self.raise_for_status() self.raise_for_status()
except RequestException: except HTTPError:
return False return False
return True return True
@@ -653,6 +670,8 @@ class Response(object):
self._content_consumed = True self._content_consumed = True
if self._content_consumed and isinstance(self._content, bool):
raise StreamConsumedError()
# simulate reading small chunks of the content # simulate reading small chunks of the content
reused_chunks = iter_slices(self._content, chunk_size) reused_chunks = iter_slices(self._content, chunk_size)
@@ -665,7 +684,7 @@ class Response(object):
return chunks return chunks
def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None): def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None, delimiter=None):
"""Iterates over the response data, one line at a time. When """Iterates over the response data, one line at a time. When
stream=True is set on the request, this avoids reading the stream=True is set on the request, this avoids reading the
content at once into memory for large responses. content at once into memory for large responses.
@@ -677,7 +696,11 @@ class Response(object):
if pending is not None: if pending is not None:
chunk = pending + chunk chunk = pending + chunk
lines = chunk.splitlines()
if delimiter:
lines = chunk.split(delimiter)
else:
lines = chunk.splitlines()
if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
pending = lines.pop() pending = lines.pop()
+1 -1
View File
@@ -57,7 +57,7 @@ del NullHandler
# Set security warning to only go off once by default. # Set security warning to only go off once by default.
import warnings import warnings
warnings.simplefilter('module', exceptions.SecurityWarning) warnings.simplefilter('always', exceptions.SecurityWarning)
def disable_warnings(category=exceptions.HTTPWarning): def disable_warnings(category=exceptions.HTTPWarning):
""" """
@@ -14,7 +14,7 @@ try: # Python 2.7+
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
from .packages.ordered_dict import OrderedDict from .packages.ordered_dict import OrderedDict
from .packages.six import itervalues from .packages.six import iterkeys, itervalues
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] __all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
@@ -85,8 +85,7 @@ class RecentlyUsedContainer(MutableMapping):
def clear(self): def clear(self):
with self.lock: with self.lock:
# Copy pointers to all values, then wipe the mapping # Copy pointers to all values, then wipe the mapping
# under Python 2, this copies the list of values twice :-| values = list(itervalues(self._container))
values = list(self._container.values())
self._container.clear() self._container.clear()
if self.dispose_func: if self.dispose_func:
@@ -95,7 +94,7 @@ class RecentlyUsedContainer(MutableMapping):
def keys(self): def keys(self):
with self.lock: with self.lock:
return self._container.keys() return list(iterkeys(self._container))
class HTTPHeaderDict(MutableMapping): class HTTPHeaderDict(MutableMapping):
+20 -4
View File
@@ -3,6 +3,7 @@ import sys
import socket import socket
from socket import timeout as SocketTimeout from socket import timeout as SocketTimeout
import warnings import warnings
from .packages import six
try: # Python 3 try: # Python 3
from http.client import HTTPConnection as _HTTPConnection, HTTPException from http.client import HTTPConnection as _HTTPConnection, HTTPException
@@ -26,12 +27,20 @@ except (ImportError, AttributeError): # Platform-specific: No SSL.
pass pass
try: # Python 3:
# Not a no-op, we're adding this to the namespace so it can be imported.
ConnectionError = ConnectionError
except NameError: # Python 2:
class ConnectionError(Exception):
pass
from .exceptions import ( from .exceptions import (
ConnectTimeoutError, ConnectTimeoutError,
SystemTimeWarning, SystemTimeWarning,
SecurityWarning,
) )
from .packages.ssl_match_hostname import match_hostname from .packages.ssl_match_hostname import match_hostname
from .packages import six
from .util.ssl_ import ( from .util.ssl_ import (
resolve_cert_reqs, resolve_cert_reqs,
@@ -40,8 +49,8 @@ from .util.ssl_ import (
assert_fingerprint, assert_fingerprint,
) )
from .util import connection
from .util import connection
port_by_scheme = { port_by_scheme = {
'http': 80, 'http': 80,
@@ -233,8 +242,15 @@ class VerifiedHTTPSConnection(HTTPSConnection):
self.assert_fingerprint) self.assert_fingerprint)
elif resolved_cert_reqs != ssl.CERT_NONE \ elif resolved_cert_reqs != ssl.CERT_NONE \
and self.assert_hostname is not False: and self.assert_hostname is not False:
match_hostname(self.sock.getpeercert(), cert = self.sock.getpeercert()
self.assert_hostname or hostname) if not cert.get('subjectAltName', ()):
warnings.warn((
'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. '
'This feature is being removed by major browsers and deprecated by RFC 2818. '
'(See https://github.com/shazow/urllib3/issues/497 for details.)'),
SecurityWarning
)
match_hostname(cert, self.assert_hostname or hostname)
self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED
or self.assert_fingerprint is not None) or self.assert_fingerprint is not None)
@@ -32,7 +32,7 @@ from .connection import (
port_by_scheme, port_by_scheme,
DummyConnection, DummyConnection,
HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection,
HTTPException, BaseSSLError, HTTPException, BaseSSLError, ConnectionError
) )
from .request import RequestMethods from .request import RequestMethods
from .response import HTTPResponse from .response import HTTPResponse
@@ -278,6 +278,23 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
# can be removed later # can be removed later
return Timeout.from_float(timeout) return Timeout.from_float(timeout)
def _raise_timeout(self, err, url, timeout_value):
"""Is the error actually a timeout? Will raise a ReadTimeout or pass"""
if isinstance(err, SocketTimeout):
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
# See the above comment about EAGAIN in Python 3. In Python 2 we have
# to specifically catch it and throw the timeout error
if hasattr(err, 'errno') and err.errno in _blocking_errnos:
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
# Catch possible read timeouts thrown as SSL errors. If not the
# case, rethrow the original. We need to do this because of:
# http://bugs.python.org/issue10272
if 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
def _make_request(self, conn, method, url, timeout=_Default, def _make_request(self, conn, method, url, timeout=_Default,
**httplib_request_kw): **httplib_request_kw):
""" """
@@ -301,7 +318,12 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
conn.timeout = timeout_obj.connect_timeout conn.timeout = timeout_obj.connect_timeout
# Trigger any extra validation we need to do. # Trigger any extra validation we need to do.
self._validate_conn(conn) try:
self._validate_conn(conn)
except (SocketTimeout, BaseSSLError) as e:
# Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)
raise
# conn.request() calls httplib.*.request, not the method in # conn.request() calls httplib.*.request, not the method in
# urllib3.request. It also calls makefile (recv) on the socket. # urllib3.request. It also calls makefile (recv) on the socket.
@@ -331,28 +353,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
httplib_response = conn.getresponse(buffering=True) httplib_response = conn.getresponse(buffering=True)
except TypeError: # Python 2.6 and older except TypeError: # Python 2.6 and older
httplib_response = conn.getresponse() httplib_response = conn.getresponse()
except SocketTimeout: except (SocketTimeout, BaseSSLError, SocketError) as e:
raise ReadTimeoutError( self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
except BaseSSLError as e:
# Catch possible read timeouts thrown as SSL errors. If not the
# case, rethrow the original. We need to do this because of:
# http://bugs.python.org/issue10272
if 'timed out' in str(e) or \
'did not complete (read)' in str(e): # Python 2.6
raise ReadTimeoutError(
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
raise
except SocketError as e: # Platform-specific: Python 2
# See the above comment about EAGAIN in Python 3. In Python 2 we
# have to specifically catch it and throw the timeout error
if e.errno in _blocking_errnos:
raise ReadTimeoutError(
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
raise raise
# AppEngine doesn't have a version attr. # AppEngine doesn't have a version attr.
@@ -537,12 +539,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
raise EmptyPoolError(self, "No pool connections are available.") raise EmptyPoolError(self, "No pool connections are available.")
except (BaseSSLError, CertificateError) as e: except (BaseSSLError, CertificateError) as e:
# Release connection unconditionally because there is no way to # Close the connection. If a connection is reused on which there
# close it externally in case of exception. # was a Certificate error, the next request will certainly raise
release_conn = True # another Certificate error.
if conn:
conn.close()
conn = None
raise SSLError(e) raise SSLError(e)
except (TimeoutError, HTTPException, SocketError) as e: except (TimeoutError, HTTPException, SocketError, ConnectionError) as e:
if conn: if conn:
# Discard the connection for these exceptions. It will be # Discard the connection for these exceptions. It will be
# be replaced during the next _get_conn() call. # be replaced during the next _get_conn() call.
@@ -725,8 +730,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
warnings.warn(( warnings.warn((
'Unverified HTTPS request is being made. ' 'Unverified HTTPS request is being made. '
'Adding certificate verification is strongly advised. See: ' 'Adding certificate verification is strongly advised. See: '
'https://urllib3.readthedocs.org/en/latest/security.html ' 'https://urllib3.readthedocs.org/en/latest/security.html'),
'(This warning will only appear once by default.)'),
InsecureRequestWarning) InsecureRequestWarning)
@@ -29,7 +29,7 @@ Now you can use :mod:`urllib3` as you normally would, and it will support SNI
when the required modules are installed. when the required modules are installed.
Activating this module also has the positive side effect of disabling SSL/TLS Activating this module also has the positive side effect of disabling SSL/TLS
encryption in Python 2 (see `CRIME attack`_). compression in Python 2 (see `CRIME attack`_).
If you want to configure the default list of supported cipher suites, you can If you want to configure the default list of supported cipher suites, you can
set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
@@ -70,9 +70,14 @@ HAS_SNI = SUBJ_ALT_NAME_SUPPORT
# Map from urllib3 to PyOpenSSL compatible parameter-values. # Map from urllib3 to PyOpenSSL compatible parameter-values.
_openssl_versions = { _openssl_versions = {
ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD,
ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
} }
try:
_openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
except AttributeError:
pass
_openssl_verify = { _openssl_verify = {
ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
@@ -199,8 +204,21 @@ class WrappedSocket(object):
def settimeout(self, timeout): def settimeout(self, timeout):
return self.socket.settimeout(timeout) return self.socket.settimeout(timeout)
def _send_until_done(self, data):
while True:
try:
return self.connection.send(data)
except OpenSSL.SSL.WantWriteError:
_, wlist, _ = select.select([], [self.socket], [],
self.socket.gettimeout())
if not wlist:
raise timeout()
continue
def sendall(self, data): def sendall(self, data):
return self.connection.sendall(data) while len(data):
sent = self._send_until_done(data)
data = data[sent:]
def close(self): def close(self):
if self._makefile_refs < 1: if self._makefile_refs < 1:
@@ -248,6 +266,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
ssl_version=None): ssl_version=None):
ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version]) ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version])
if certfile: if certfile:
keyfile = keyfile or certfile # Match behaviour of the normal python ssl library
ctx.use_certificate_file(certfile) ctx.use_certificate_file(certfile)
if keyfile: if keyfile:
ctx.use_privatekey_file(keyfile) ctx.use_privatekey_file(keyfile)
+8 -5
View File
@@ -72,11 +72,8 @@ class MaxRetryError(RequestError):
def __init__(self, pool, url, reason=None): def __init__(self, pool, url, reason=None):
self.reason = reason self.reason = reason
message = "Max retries exceeded with url: %s" % url message = "Max retries exceeded with url: %s (Caused by %r)" % (
if reason: url, reason)
message += " (Caused by %r)" % reason
else:
message += " (Caused by redirect)"
RequestError.__init__(self, pool, url, message) RequestError.__init__(self, pool, url, message)
@@ -141,6 +138,12 @@ class LocationParseError(LocationValueError):
self.location = location self.location = location
class ResponseError(HTTPError):
"Used as a container for an error reason supplied in a MaxRetryError."
GENERIC_ERROR = 'too many error responses'
SPECIFIC_ERROR = 'too many {status_code} error responses'
class SecurityWarning(HTTPWarning): class SecurityWarning(HTTPWarning):
"Warned when perfoming security reducing actions" "Warned when perfoming security reducing actions"
pass pass
+17 -11
View File
@@ -118,18 +118,24 @@ class RequestMethods(object):
which is used to compose the body of the request. The random boundary which is used to compose the body of the request. The random boundary
string can be explicitly set with the ``multipart_boundary`` parameter. string can be explicitly set with the ``multipart_boundary`` parameter.
""" """
if encode_multipart:
body, content_type = encode_multipart_formdata(
fields or {}, boundary=multipart_boundary)
else:
body, content_type = (urlencode(fields or {}),
'application/x-www-form-urlencoded')
if headers is None: if headers is None:
headers = self.headers headers = self.headers
headers_ = {'Content-Type': content_type} extra_kw = {'headers': {}}
headers_.update(headers)
return self.urlopen(method, url, body=body, headers=headers_, if fields:
**urlopen_kw) if 'body' in urlopen_kw:
raise TypeError('request got values for both \'fields\' and \'body\', can only specify one.')
if encode_multipart:
body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary)
else:
body, content_type = urlencode(fields), 'application/x-www-form-urlencoded'
extra_kw['body'] = body
extra_kw['headers'] = {'Content-Type': content_type}
extra_kw['headers'].update(headers)
extra_kw.update(urlopen_kw)
return self.urlopen(method, url, **extra_kw)
+15 -9
View File
@@ -2,10 +2,11 @@ import time
import logging import logging
from ..exceptions import ( from ..exceptions import (
ProtocolError,
ConnectTimeoutError, ConnectTimeoutError,
ReadTimeoutError,
MaxRetryError, MaxRetryError,
ProtocolError,
ReadTimeoutError,
ResponseError,
) )
from ..packages import six from ..packages import six
@@ -36,7 +37,6 @@ class Retry(object):
Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
retries are disabled, in which case the causing exception will be raised. retries are disabled, in which case the causing exception will be raised.
:param int total: :param int total:
Total number of retries to allow. Takes precedence over other counts. Total number of retries to allow. Takes precedence over other counts.
@@ -184,8 +184,8 @@ class Retry(object):
return isinstance(err, ConnectTimeoutError) return isinstance(err, ConnectTimeoutError)
def _is_read_error(self, err): def _is_read_error(self, err):
""" Errors that occur after the request has been started, so we can't """ Errors that occur after the request has been started, so we should
assume that the server did not process any of it. assume that the server began processing it.
""" """
return isinstance(err, (ReadTimeoutError, ProtocolError)) return isinstance(err, (ReadTimeoutError, ProtocolError))
@@ -198,8 +198,7 @@ class Retry(object):
return self.status_forcelist and status_code in self.status_forcelist return self.status_forcelist and status_code in self.status_forcelist
def is_exhausted(self): def is_exhausted(self):
""" Are we out of retries? """ Are we out of retries? """
"""
retry_counts = (self.total, self.connect, self.read, self.redirect) retry_counts = (self.total, self.connect, self.read, self.redirect)
retry_counts = list(filter(None, retry_counts)) retry_counts = list(filter(None, retry_counts))
if not retry_counts: if not retry_counts:
@@ -230,6 +229,7 @@ class Retry(object):
connect = self.connect connect = self.connect
read = self.read read = self.read
redirect = self.redirect redirect = self.redirect
cause = 'unknown'
if error and self._is_connection_error(error): if error and self._is_connection_error(error):
# Connect retry? # Connect retry?
@@ -251,10 +251,16 @@ class Retry(object):
# Redirect retry? # Redirect retry?
if redirect is not None: if redirect is not None:
redirect -= 1 redirect -= 1
cause = 'too many redirects'
else: else:
# FIXME: Nothing changed, scenario doesn't make sense. # Incrementing because of a server error like a 500 in
# status_forcelist and a the given method is in the whitelist
_observed_errors += 1 _observed_errors += 1
cause = ResponseError.GENERIC_ERROR
if response and response.status:
cause = ResponseError.SPECIFIC_ERROR.format(
status_code=response.status)
new_retry = self.new( new_retry = self.new(
total=total, total=total,
@@ -262,7 +268,7 @@ class Retry(object):
_observed_errors=_observed_errors) _observed_errors=_observed_errors)
if new_retry.is_exhausted(): if new_retry.is_exhausted():
raise MaxRetryError(_pool, url, error) raise MaxRetryError(_pool, url, error or ResponseError(cause))
log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry)) log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry))
+162 -40
View File
@@ -4,18 +4,84 @@ from hashlib import md5, sha1
from ..exceptions import SSLError from ..exceptions import SSLError
try: # Test for SSL features SSLContext = None
SSLContext = None HAS_SNI = False
HAS_SNI = False create_default_context = None
import ssl import errno
import ssl
try: # Test for SSL features
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
from ssl import SSLContext # Modern SSL?
from ssl import HAS_SNI # Has SNI? from ssl import HAS_SNI # Has SNI?
except ImportError: except ImportError:
pass pass
try:
from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION
except ImportError:
OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
OP_NO_COMPRESSION = 0x20000
try:
from ssl import _DEFAULT_CIPHERS
except ImportError:
_DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:ECDH+RC4:'
'DH+RC4:RSA+RC4:!aNULL:!eNULL:!MD5'
)
try:
from ssl import SSLContext # Modern SSL?
except ImportError:
import sys
class SSLContext(object): # Platform-specific: Python 2 & 3.1
supports_set_ciphers = sys.version_info >= (2, 7)
def __init__(self, protocol_version):
self.protocol = protocol_version
# Use default values from a real SSLContext
self.check_hostname = False
self.verify_mode = ssl.CERT_NONE
self.ca_certs = None
self.options = 0
self.certfile = None
self.keyfile = None
self.ciphers = None
def load_cert_chain(self, certfile, keyfile):
self.certfile = certfile
self.keyfile = keyfile
def load_verify_locations(self, location):
self.ca_certs = location
def set_ciphers(self, cipher_suite):
if not self.supports_set_ciphers:
raise TypeError(
'Your version of Python does not support setting '
'a custom cipher suite. Please upgrade to Python '
'2.7, 3.2, or later if you need this functionality.'
)
self.ciphers = cipher_suite
def wrap_socket(self, socket, server_hostname=None):
kwargs = {
'keyfile': self.keyfile,
'certfile': self.certfile,
'ca_certs': self.ca_certs,
'cert_reqs': self.verify_mode,
'ssl_version': self.protocol,
}
if self.supports_set_ciphers: # Platform-specific: Python 2.7+
return wrap_socket(socket, ciphers=self.ciphers, **kwargs)
else: # Platform-specific: Python 2.6
return wrap_socket(socket, **kwargs)
def assert_fingerprint(cert, fingerprint): def assert_fingerprint(cert, fingerprint):
""" """
Checks if given fingerprint matches the supplied certificate. Checks if given fingerprint matches the supplied certificate.
@@ -91,42 +157,98 @@ def resolve_ssl_version(candidate):
return candidate return candidate
if SSLContext is not None: # Python 3.2+ def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, options=None, ciphers=None):
ca_certs=None, server_hostname=None, """All arguments have the same meaning as ``ssl_wrap_socket``.
ssl_version=None):
"""
All arguments except `server_hostname` have the same meaning as for
:func:`ssl.wrap_socket`
:param server_hostname: By default, this function does a lot of the same work that
Hostname of the expected certificate ``ssl.create_default_context`` does on Python 3.4+. It:
"""
context = SSLContext(ssl_version)
context.verify_mode = cert_reqs
# Disable TLS compression to migitate CRIME attack (issue #309) - Disables SSLv2, SSLv3, and compression
OP_NO_COMPRESSION = 0x20000 - Sets a restricted set of server ciphers
context.options |= OP_NO_COMPRESSION
if ca_certs: If you wish to enable SSLv3, you can do::
try:
context.load_verify_locations(ca_certs) from urllib3.util import ssl_
# Py32 raises IOError context = ssl_.create_urllib3_context()
# Py33 raises FileNotFoundError context.options &= ~ssl_.OP_NO_SSLv3
except Exception as e: # Reraise as SSLError
You can do the same to enable compression (substituting ``COMPRESSION``
for ``SSLv3`` in the last line above).
:param ssl_version:
The desired protocol version to use. This will default to
PROTOCOL_SSLv23 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support.
:param cert_reqs:
Whether to require the certificate verification. This defaults to
``ssl.CERT_REQUIRED``.
:param options:
Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,
``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``.
:param ciphers:
Which cipher suites to allow the server to select.
:returns:
Constructed SSLContext object with specified options
:rtype: SSLContext
"""
context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
if options is None:
options = 0
# SSLv2 is easily broken and is considered harmful and dangerous
options |= OP_NO_SSLv2
# SSLv3 has several problems and is now dangerous
options |= OP_NO_SSLv3
# Disable compression to prevent CRIME attacks for OpenSSL 1.0+
# (issue #309)
options |= OP_NO_COMPRESSION
context.options |= options
if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6
context.set_ciphers(ciphers or _DEFAULT_CIPHERS)
context.verify_mode = cert_reqs
if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2
context.check_hostname = (context.verify_mode == ssl.CERT_REQUIRED)
return context
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
ca_certs=None, server_hostname=None,
ssl_version=None, ciphers=None, ssl_context=None):
"""
All arguments except for server_hostname and ssl_context have the same
meaning as they do when using :func:`ssl.wrap_socket`.
:param server_hostname:
When SNI is supported, the expected hostname of the certificate
:param ssl_context:
A pre-made :class:`SSLContext` object. If none is provided, one will
be created using :func:`create_urllib3_context`.
:param ciphers:
A string of ciphers we wish the client to support. This is not
supported on Python 2.6 as the ssl module does not support it.
"""
context = ssl_context
if context is None:
context = create_urllib3_context(ssl_version, cert_reqs,
ciphers=ciphers)
if ca_certs:
try:
context.load_verify_locations(ca_certs)
except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2
raise SSLError(e)
# Py33 raises FileNotFoundError which subclasses OSError
# These are not equivalent unless we check the errno attribute
except OSError as e: # Platform-specific: Python 3.3 and beyond
if e.errno == errno.ENOENT:
raise SSLError(e) raise SSLError(e)
if certfile: raise
# FIXME: This block needs a test. if certfile:
context.load_cert_chain(certfile, keyfile) context.load_cert_chain(certfile, keyfile)
if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
return context.wrap_socket(sock, server_hostname=server_hostname) return context.wrap_socket(sock, server_hostname=server_hostname)
return context.wrap_socket(sock) return context.wrap_socket(sock)
else: # Python 3.1 and earlier
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
ca_certs=None, server_hostname=None,
ssl_version=None):
return wrap_socket(sock, keyfile=keyfile, certfile=certfile,
ca_certs=ca_certs, cert_reqs=cert_reqs,
ssl_version=ssl_version)
+43 -2
View File
@@ -40,6 +40,48 @@ class Url(namedtuple('Url', url_attrs)):
return '%s:%d' % (self.host, self.port) return '%s:%d' % (self.host, self.port)
return self.host return self.host
@property
def url(self):
"""
Convert self into a url
This function should more or less round-trip with :func:`.parse_url`. The
returned url may not be exactly the same as the url inputted to
:func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls
with a blank port will have : removed).
Example: ::
>>> U = parse_url('http://google.com/mail/')
>>> U.url
'http://google.com/mail/'
>>> Url('http', 'username:password', 'host.com', 80,
... '/path', 'query', 'fragment').url
'http://username:password@host.com:80/path?query#fragment'
"""
scheme, auth, host, port, path, query, fragment = self
url = ''
# We use "is not None" we want things to happen with empty strings (or 0 port)
if scheme is not None:
url += scheme + '://'
if auth is not None:
url += auth + '@'
if host is not None:
url += host
if port is not None:
url += ':' + str(port)
if path is not None:
url += path
if query is not None:
url += '?' + query
if fragment is not None:
url += '#' + fragment
return url
def __str__(self):
return self.url
def split_first(s, delims): def split_first(s, delims):
""" """
@@ -84,7 +126,7 @@ def parse_url(url):
Example:: Example::
>>> parse_url('http://google.com/mail/') >>> parse_url('http://google.com/mail/')
Url(scheme='http', host='google.com', port=None, path='/', ...) Url(scheme='http', host='google.com', port=None, path='/mail/', ...)
>>> parse_url('google.com:80') >>> parse_url('google.com:80')
Url(scheme=None, host='google.com', port=80, path=None, ...) Url(scheme=None, host='google.com', port=80, path=None, ...)
>>> parse_url('/foo?bar') >>> parse_url('/foo?bar')
@@ -162,7 +204,6 @@ def parse_url(url):
return Url(scheme, auth, host, port, path, query, fragment) return Url(scheme, auth, host, port, path, query, fragment)
def get_host(url): def get_host(url):
""" """
Deprecated. Use :func:`.parse_url` instead. Deprecated. Use :func:`.parse_url` instead.
+44 -22
View File
@@ -13,7 +13,7 @@ from collections import Mapping
from datetime import datetime from datetime import datetime
from .auth import _basic_auth_str from .auth import _basic_auth_str
from .compat import cookielib, OrderedDict, urljoin, urlparse, builtin_str from .compat import cookielib, OrderedDict, urljoin, urlparse
from .cookies import ( from .cookies import (
cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)
from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT
@@ -21,6 +21,7 @@ from .hooks import default_hooks, dispatch_hook
from .utils import to_key_val_list, default_headers, to_native_string from .utils import to_key_val_list, default_headers, to_native_string
from .exceptions import ( from .exceptions import (
TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError) TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)
from .packages.urllib3._collections import RecentlyUsedContainer
from .structures import CaseInsensitiveDict from .structures import CaseInsensitiveDict
from .adapters import HTTPAdapter from .adapters import HTTPAdapter
@@ -35,6 +36,8 @@ from .status_codes import codes
# formerly defined here, reexposed here for backward compatibility # formerly defined here, reexposed here for backward compatibility
from .models import REDIRECT_STATI from .models import REDIRECT_STATI
REDIRECT_CACHE_SIZE = 1000
def merge_setting(request_setting, session_setting, dict_class=OrderedDict): def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
""" """
@@ -128,14 +131,14 @@ class SessionRedirectMixin(object):
# Facilitate relative 'location' headers, as allowed by RFC 7231. # Facilitate relative 'location' headers, as allowed by RFC 7231.
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
# Compliant with RFC3986, we percent encode the url. # Compliant with RFC3986, we percent encode the url.
if not urlparse(url).netloc: if not parsed.netloc:
url = urljoin(resp.url, requote_uri(url)) url = urljoin(resp.url, requote_uri(url))
else: else:
url = requote_uri(url) url = requote_uri(url)
prepared_request.url = to_native_string(url) prepared_request.url = to_native_string(url)
# cache the url # Cache the url, unless it redirects to itself.
if resp.is_permanent_redirect: if resp.is_permanent_redirect and req.url != prepared_request.url:
self.redirect_cache[req.url] = prepared_request.url self.redirect_cache[req.url] = prepared_request.url
# http://tools.ietf.org/html/rfc7231#section-6.4.4 # http://tools.ietf.org/html/rfc7231#section-6.4.4
@@ -271,9 +274,10 @@ class Session(SessionRedirectMixin):
""" """
__attrs__ = [ __attrs__ = [
'headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks', 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
'params', 'verify', 'cert', 'prefetch', 'adapters', 'stream', 'cert', 'prefetch', 'adapters', 'stream', 'trust_env',
'trust_env', 'max_redirects', 'redirect_cache'] 'max_redirects',
]
def __init__(self): def __init__(self):
@@ -326,7 +330,8 @@ class Session(SessionRedirectMixin):
self.mount('https://', HTTPAdapter()) self.mount('https://', HTTPAdapter())
self.mount('http://', HTTPAdapter()) self.mount('http://', HTTPAdapter())
self.redirect_cache = {} # Only store 1000 redirects to prevent using infinite memory
self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
def __enter__(self): def __enter__(self):
return self return self
@@ -365,6 +370,7 @@ class Session(SessionRedirectMixin):
url=request.url, url=request.url,
files=request.files, files=request.files,
data=request.data, data=request.data,
json=request.json,
headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict), headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
params=merge_setting(request.params, self.params), params=merge_setting(request.params, self.params),
auth=merge_setting(auth, self.auth), auth=merge_setting(auth, self.auth),
@@ -386,7 +392,8 @@ class Session(SessionRedirectMixin):
hooks=None, hooks=None,
stream=None, stream=None,
verify=None, verify=None,
cert=None): cert=None,
json=None):
"""Constructs a :class:`Request <Request>`, prepares it and sends it. """Constructs a :class:`Request <Request>`, prepares it and sends it.
Returns :class:`Response <Response>` object. Returns :class:`Response <Response>` object.
@@ -396,17 +403,22 @@ class Session(SessionRedirectMixin):
string for the :class:`Request`. string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the :param data: (optional) Dictionary or bytes to send in the body of the
:class:`Request`. :class:`Request`.
:param json: (optional) json to send in the body of the
:class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :param headers: (optional) Dictionary of HTTP Headers to send with the
:class:`Request`. :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :param cookies: (optional) Dict or CookieJar object to send with the
:class:`Request`. :class:`Request`.
:param files: (optional) Dictionary of 'filename': file-like-objects :param files: (optional) Dictionary of ``'filename': file-like-objects``
for multipart encoding upload. for multipart encoding upload.
:param auth: (optional) Auth tuple or callable to enable :param auth: (optional) Auth tuple or callable to enable
Basic/Digest/Custom HTTP Auth. Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) Float describing the timeout of the :param timeout: (optional) How long to wait for the server to send
request in seconds. data before giving up, as a float, or a (`connect timeout, read
:param allow_redirects: (optional) Boolean. Set to True by default. timeout <user/advanced.html#timeouts>`_) tuple.
:type timeout: float or tuple
:param allow_redirects: (optional) Set to True by default.
:type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol to the URL of :param proxies: (optional) Dictionary mapping protocol to the URL of
the proxy. the proxy.
:param stream: (optional) whether to immediately download the response :param stream: (optional) whether to immediately download the response
@@ -417,7 +429,7 @@ class Session(SessionRedirectMixin):
If Tuple, ('cert', 'key') pair. If Tuple, ('cert', 'key') pair.
""" """
method = builtin_str(method) method = to_native_string(method)
# Create the Request. # Create the Request.
req = Request( req = Request(
@@ -426,6 +438,7 @@ class Session(SessionRedirectMixin):
headers = headers, headers = headers,
files = files, files = files,
data = data or {}, data = data or {},
json = json,
params = params or {}, params = params or {},
auth = auth, auth = auth,
cookies = cookies, cookies = cookies,
@@ -479,15 +492,16 @@ class Session(SessionRedirectMixin):
kwargs.setdefault('allow_redirects', False) kwargs.setdefault('allow_redirects', False)
return self.request('HEAD', url, **kwargs) return self.request('HEAD', url, **kwargs)
def post(self, url, data=None, **kwargs): def post(self, url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object. """Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
""" """
return self.request('POST', url, data=data, **kwargs) return self.request('POST', url, data=data, json=json, **kwargs)
def put(self, url, data=None, **kwargs): def put(self, url, data=None, **kwargs):
"""Sends a PUT request. Returns :class:`Response` object. """Sends a PUT request. Returns :class:`Response` object.
@@ -532,12 +546,13 @@ class Session(SessionRedirectMixin):
if not isinstance(request, PreparedRequest): if not isinstance(request, PreparedRequest):
raise ValueError('You can only send PreparedRequests.') raise ValueError('You can only send PreparedRequests.')
redirect_count = 0 checked_urls = set()
while request.url in self.redirect_cache: while request.url in self.redirect_cache:
redirect_count += 1 checked_urls.add(request.url)
if redirect_count > self.max_redirects: new_url = self.redirect_cache.get(request.url)
raise TooManyRedirects if new_url in checked_urls:
request.url = self.redirect_cache.get(request.url) break
request.url = new_url
# Set up variables needed for resolve_redirects and dispatching of hooks # Set up variables needed for resolve_redirects and dispatching of hooks
allow_redirects = kwargs.pop('allow_redirects', True) allow_redirects = kwargs.pop('allow_redirects', True)
@@ -647,12 +662,19 @@ class Session(SessionRedirectMixin):
self.adapters[key] = self.adapters.pop(key) self.adapters[key] = self.adapters.pop(key)
def __getstate__(self): def __getstate__(self):
return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__) state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__)
state['redirect_cache'] = dict(self.redirect_cache)
return state
def __setstate__(self, state): def __setstate__(self, state):
redirect_cache = state.pop('redirect_cache', {})
for attr, value in state.items(): for attr, value in state.items():
setattr(self, attr, value) setattr(self, attr, value)
self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
for redirect, to in redirect_cache.items():
self.redirect_cache[redirect] = to
def session(): def session():
"""Returns a :class:`Session` for context-management.""" """Returns a :class:`Session` for context-management."""
+30 -7
View File
@@ -19,6 +19,7 @@ import re
import sys import sys
import socket import socket
import struct import struct
import warnings
from . import __version__ from . import __version__
from . import certs from . import certs
@@ -114,7 +115,7 @@ def get_netrc_auth(url):
def guess_filename(obj): def guess_filename(obj):
"""Tries to guess the filename of the given object.""" """Tries to guess the filename of the given object."""
name = getattr(obj, 'name', None) name = getattr(obj, 'name', None)
if name and name[0] != '<' and name[-1] != '>': if name and isinstance(name, builtin_str) and name[0] != '<' and name[-1] != '>':
return os.path.basename(name) return os.path.basename(name)
@@ -287,6 +288,11 @@ def get_encodings_from_content(content):
:param content: bytestring to extract encodings from. :param content: bytestring to extract encodings from.
""" """
warnings.warn((
'In requests 3.0, get_encodings_from_content will be removed. For '
'more information, please see the discussion on issue #2266. (This'
' warning should only appear once.)'),
DeprecationWarning)
charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I) charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I)
pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I) pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I)
@@ -351,12 +357,14 @@ def get_unicode_from_response(r):
Tried: Tried:
1. charset from content-type 1. charset from content-type
2. fall back and replace all unicode characters
2. every encodings from ``<meta ... charset=XXX>``
3. fall back and replace all unicode characters
""" """
warnings.warn((
'In requests 3.0, get_unicode_from_response will be removed. For '
'more information, please see the discussion on issue #2266. (This'
' warning should only appear once.)'),
DeprecationWarning)
tried_encodings = [] tried_encodings = []
@@ -555,7 +563,7 @@ def default_headers():
'User-Agent': default_user_agent(), 'User-Agent': default_user_agent(),
'Accept-Encoding': ', '.join(('gzip', 'deflate')), 'Accept-Encoding': ', '.join(('gzip', 'deflate')),
'Accept': '*/*', 'Accept': '*/*',
'Connection': 'keep-alive' 'Connection': 'keep-alive',
}) })
@@ -570,7 +578,7 @@ def parse_header_links(value):
replace_chars = " '\"" replace_chars = " '\""
for val in value.split(","): for val in re.split(", *<", value):
try: try:
url, params = val.split(";", 1) url, params = val.split(";", 1)
except ValueError: except ValueError:
@@ -672,3 +680,18 @@ def to_native_string(string, encoding='ascii'):
out = string.decode(encoding) out = string.decode(encoding)
return out return out
def urldefragauth(url):
"""
Given a url remove the fragment and the authentication part
"""
scheme, netloc, path, params, query, fragment = urlparse(url)
# see func:`prepend_scheme_if_needed`
if not netloc:
netloc, path = path, netloc
netloc = netloc.rsplit('@', 1)[-1]
return urlunparse((scheme, netloc, path, params, query, ''))