mirror of
https://github.com/kennethreitz/requests.git
synced 2026-06-05 22:50:18 +00:00
merge
This commit is contained in:
@@ -178,3 +178,8 @@ Patches and Suggestions
|
||||
- Ryan Pineo (`@ryanpineo <https://github.com/ryanpineo>`_)
|
||||
- Ed Morley (`@edmorley <https://github.com/edmorley>`_)
|
||||
- Matt Liu <liumatt@gmail.com> (`@mlcrazy <https://github.com/mlcrazy>`_)
|
||||
- Taylor Hoff <primdevs@protonmail.com> (`@PrimordialHelios <https://github.com/PrimordialHelios>`_)
|
||||
- Arthur Vigil (`@ahvigil <https://github.com/ahvigil>`_)
|
||||
- Nehal J Wani (`@nehaljwani <https://github.com/nehaljwani>`_)
|
||||
- Demetrios Bairaktaris (`@DemetriosBairaktaris <https://github.com/demetriosbairaktaris>`_)
|
||||
- Darren Dormer (`@ddormer <https://github.com/ddormer>`_)
|
||||
|
||||
@@ -8,9 +8,17 @@ dev
|
||||
|
||||
**Improvements**
|
||||
|
||||
- Warn user about possible slowdown when using cryptography version < 1.3.4
|
||||
- Check for invalid host in proxy URL, before forwarding request to adapter.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Parsing empty ``Link`` headers with ``parse_header_links()`` no longer return one bogus entry
|
||||
- Fixed issue where loading the default certificate bundle from a zip archive
|
||||
would raise an ``IOError``
|
||||
- Fixed issue with unexpected ``ImportError`` on windows system which do not support ``winreg`` module
|
||||
- DNS resolution in proxy bypass no longer includes the username and password in
|
||||
the request. This also fixes the issue of DNS queries failing on macOS.
|
||||
|
||||
2.18.4 (2017-08-15)
|
||||
+++++++++++++++++++
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@ Requests is ready for today's web.
|
||||
- ``.netrc`` Support
|
||||
- Chunked Requests
|
||||
|
||||
Requests officially supports Python 2.6–2.7 & 3.3–3.7, and runs great on PyPy.
|
||||
Requests officially supports Python 2.6–2.7 & 3.4–3.6, and runs great on PyPy.
|
||||
|
||||
Installation
|
||||
------------
|
||||
@@ -105,6 +105,6 @@ How to Contribute
|
||||
#. Write a test which shows that the bug was fixed or that the feature works as expected.
|
||||
#. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_.
|
||||
|
||||
.. _`the repository`: http://github.com/requests/requests
|
||||
.. _`the repository`: https://github.com/requests/requests
|
||||
.. _AUTHORS: https://github.com/requests/requests/blob/master/AUTHORS.rst
|
||||
.. _Contributor Friendly: https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open
|
||||
|
||||
Vendored
+5
-5
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=requests&repo=requests&type=watch&count=true&size=large"
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=requests&repo=requests&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<h3>Stay Informed</h3>
|
||||
<p>Receive updates on new releases and upcoming projects.</p>
|
||||
|
||||
<p><iframe src="http://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=false"
|
||||
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=false"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
||||
|
||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow @kennethreitz</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></p>
|
||||
@@ -48,9 +48,9 @@
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="http://github.com/requests/requests">Requests @ GitHub</a></li>
|
||||
<li><a href="http://pypi.python.org/pypi/requests">Requests @ PyPI</a></li>
|
||||
<li><a href="http://github.com/requests/requests/issues">Issue Tracker</a></li>
|
||||
<li><a href="https://github.com/requests/requests">Requests @ GitHub</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/requests">Requests @ PyPI</a></li>
|
||||
<li><a href="https://github.com/requests/requests/issues">Issue Tracker</a></li>
|
||||
<li><a href="http://docs.python-requests.org/en/latest/community/updates/#software-updates">Release History</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -4,7 +4,7 @@
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=requests&repo=requests&type=watch&count=true&size=large"
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=requests&repo=requests&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
</p>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<p>If you enjoy using this project, <a href="https://saythanks.io/to/kennethreitz">Say Thanks!</a></p>
|
||||
|
||||
<p><iframe src="http://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=false"
|
||||
<p><iframe src="https://ghbtns.com/github-btn.html?user=kennethreitz&type=follow&count=false"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20"></iframe></p>
|
||||
|
||||
<p><a href="https://twitter.com/kennethreitz" class="twitter-follow-button" data-show-count="false">Follow @kennethreitz</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></p>
|
||||
|
||||
+1
-11
@@ -109,17 +109,7 @@ Status Code Lookup
|
||||
|
||||
.. autoclass:: requests.codes
|
||||
|
||||
::
|
||||
|
||||
>>> requests.codes['temporary_redirect']
|
||||
307
|
||||
|
||||
>>> requests.codes.teapot
|
||||
418
|
||||
|
||||
>>> requests.codes['\o/']
|
||||
200
|
||||
|
||||
.. automodule:: requests.status_codes
|
||||
|
||||
|
||||
Migrating to 1.x
|
||||
|
||||
@@ -18,7 +18,7 @@ Articles & Talks
|
||||
- `Python for the Web <http://gun.io/blog/python-for-the-web/>`_ teaches how to use Python to interact with the web, using Requests.
|
||||
- `Daniel Greenfeld's Review of Requests <http://pydanny.blogspot.com/2011/05/python-http-requests-for-humans.html>`_
|
||||
- `My 'Python for Humans' talk <http://python-for-humans.heroku.com>`_ ( `audio <http://codeconf.s3.amazonaws.com/2011/pycodeconf/talks/PyCodeConf2011%20-%20Kenneth%20Reitz.m4a>`_ )
|
||||
- `Issac Kelly's 'Consuming Web APIs' talk <http://issackelly.github.com/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
|
||||
- `Issac Kelly's 'Consuming Web APIs' talk <https://issackelly.github.com/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
|
||||
- `Blog post about Requests via Yum <http://arunsag.wordpress.com/2011/08/17/new-package-python-requests-http-for-humans/>`_
|
||||
- `Russian blog post introducing Requests <http://habrahabr.ru/blogs/python/126262/>`_
|
||||
- `Sending JSON in Requests <http://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_
|
||||
|
||||
@@ -34,7 +34,7 @@ but do not belong in Requests proper. This library is actively maintained
|
||||
by members of the Requests core team, and reflects the functionality most
|
||||
requested by users within the community.
|
||||
|
||||
.. _Requests-Toolbelt: http://toolbelt.readthedocs.io/en/latest/index.html
|
||||
.. _Requests-Toolbelt: https://toolbelt.readthedocs.io/en/latest/index.html
|
||||
|
||||
|
||||
Requests-Threads
|
||||
|
||||
+1
-1
@@ -376,4 +376,4 @@ epub_exclude_files = ['search.html']
|
||||
# If false, no index is generated.
|
||||
#epub_use_index = True
|
||||
|
||||
intersphinx_mapping = {'urllib3': ('http://urllib3.readthedocs.io/en/latest', None)}
|
||||
intersphinx_mapping = {'urllib3': ('https://urllib3.readthedocs.io/en/latest', None)}
|
||||
|
||||
+1
-1
@@ -61,5 +61,5 @@ Requests currently supports the following versions of Python:
|
||||
Google AppEngine is not officially supported although support is available
|
||||
with the `Requests-Toolbelt`_.
|
||||
|
||||
.. _Requests-Toolbelt: http://toolbelt.readthedocs.io/
|
||||
.. _Requests-Toolbelt: https://toolbelt.readthedocs.io/
|
||||
|
||||
|
||||
+29
-5
@@ -287,7 +287,7 @@ system.
|
||||
For the sake of security we recommend upgrading certifi frequently!
|
||||
|
||||
.. _HTTP persistent connection: https://en.wikipedia.org/wiki/HTTP_persistent_connection
|
||||
.. _connection pooling: http://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.connectionpool
|
||||
.. _connection pooling: https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.connectionpool
|
||||
.. _certifi: http://certifi.io/
|
||||
.. _Mozilla trust store: https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt
|
||||
|
||||
@@ -436,7 +436,7 @@ You can assign a hook function on a per-request basis by passing a
|
||||
``{hook_name: callback_function}`` dictionary to the ``hooks`` request
|
||||
parameter::
|
||||
|
||||
hooks=dict(response=print_url)
|
||||
hooks={'response': print_url}
|
||||
|
||||
That ``callback_function`` will receive a chunk of data as its first
|
||||
argument.
|
||||
@@ -452,12 +452,36 @@ If the callback function returns a value, it is assumed that it is to
|
||||
replace the data that was passed in. If the function doesn't return
|
||||
anything, nothing else is effected.
|
||||
|
||||
::
|
||||
|
||||
def record_hook(r, *args, **kwargs):
|
||||
r.hook_called = True
|
||||
return r
|
||||
|
||||
Let's print some request method arguments at runtime::
|
||||
|
||||
>>> requests.get('http://httpbin.org', hooks=dict(response=print_url))
|
||||
>>> requests.get('http://httpbin.org', hooks={'response': print_url})
|
||||
http://httpbin.org
|
||||
<Response [200]>
|
||||
|
||||
You can add multiple hooks to a single request. Let's call two hooks at once::
|
||||
|
||||
>>> r = requests.get('http://httpbin.org', hooks={'response': [print_url, record_hook]})
|
||||
>>> r.hook_called
|
||||
True
|
||||
|
||||
You can also add hooks to a ``Session`` instance. Any hooks you add will then
|
||||
be called on every request made to the session. For example::
|
||||
|
||||
>>> s = requests.Session()
|
||||
>>> s.hooks['response'].append(print_url)
|
||||
>>> s.get('http://httpbin.org')
|
||||
http://httpbin.org
|
||||
<Response [200]>
|
||||
|
||||
A ``Session`` can have multiple hooks, which will be called in the order
|
||||
they are added.
|
||||
|
||||
.. _custom-auth:
|
||||
|
||||
Custom Authentication
|
||||
@@ -633,7 +657,7 @@ When you receive a response, Requests makes a guess at the encoding to
|
||||
use for decoding the response when you access the :attr:`Response.text
|
||||
<requests.Response.text>` attribute. Requests will first check for an
|
||||
encoding in the HTTP header, and if none is present, will use `chardet
|
||||
<http://pypi.python.org/pypi/chardet>`_ to attempt to guess the encoding.
|
||||
<https://pypi.python.org/pypi/chardet>`_ to attempt to guess the encoding.
|
||||
|
||||
The only time Requests will not do this is if no explicit charset
|
||||
is present in the HTTP headers **and** the ``Content-Type``
|
||||
@@ -860,7 +884,7 @@ Link Headers
|
||||
Many HTTP APIs feature Link headers. They make APIs more self describing and
|
||||
discoverable.
|
||||
|
||||
GitHub uses these for `pagination <http://developer.github.com/v3/#pagination>`_
|
||||
GitHub uses these for `pagination <https://developer.github.com/v3/#pagination>`_
|
||||
in their API, for example::
|
||||
|
||||
>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10'
|
||||
|
||||
@@ -136,11 +136,11 @@ Further examples can be found under the `Requests organization`_ and in the
|
||||
|
||||
.. _OAuth: http://oauth.net/
|
||||
.. _requests_oauthlib: https://github.com/requests/requests-oauthlib
|
||||
.. _requests-oauthlib OAuth2 documentation: http://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html
|
||||
.. _Web Application Flow: http://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#web-application-flow
|
||||
.. _Mobile Application Flow: http://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#mobile-application-flow
|
||||
.. _Legacy Application Flow: http://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#legacy-application-flow
|
||||
.. _Backend Application Flow: http://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#backend-application-flow
|
||||
.. _requests-oauthlib OAuth2 documentation: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html
|
||||
.. _Web Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#web-application-flow
|
||||
.. _Mobile Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#mobile-application-flow
|
||||
.. _Legacy Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#legacy-application-flow
|
||||
.. _Backend Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#backend-application-flow
|
||||
.. _Kerberos: https://github.com/requests/requests-kerberos
|
||||
.. _NTLM: https://github.com/requests/requests-ntlm
|
||||
.. _Requests organization: https://github.com/requests
|
||||
|
||||
@@ -110,7 +110,7 @@ using, and change it, using the ``r.encoding`` property::
|
||||
If you change the encoding, Requests will use the new value of ``r.encoding``
|
||||
whenever you call ``r.text``. You might want to do this in any situation where
|
||||
you can apply special logic to work out what the encoding of the content will
|
||||
be. For example, HTTP and XML have the ability to specify their encoding in
|
||||
be. For example, HTML and XML have the ability to specify their encoding in
|
||||
their body. In situations like this, you should use ``r.content`` to find the
|
||||
encoding, and then set ``r.encoding``. This will let you use ``r.text`` with
|
||||
the correct encoding.
|
||||
@@ -189,6 +189,14 @@ download, the above is the preferred and recommended way to retrieve the
|
||||
content. Note that ``chunk_size`` can be freely adjusted to a number that
|
||||
may better fit your use cases.
|
||||
|
||||
.. note::
|
||||
|
||||
An important note about using ``Response.iter_content`` versus ``Response.raw``.
|
||||
``Response.iter_content`` will automatically decode the ``gzip`` and ``deflate``
|
||||
transfer-encodings. ``Response.raw`` is a raw stream of bytes -- it does not
|
||||
transform the response content. If you really need access to the bytes as they
|
||||
were returned, use ``Response.raw``.
|
||||
|
||||
|
||||
Custom Headers
|
||||
--------------
|
||||
@@ -273,6 +281,7 @@ the ``json`` parameter (added in version 2.4.2) and it will be encoded automatic
|
||||
|
||||
>>> r = requests.post(url, json=payload)
|
||||
|
||||
Note, the ``json`` parameter is ignored if either ``data`` or ``files`` is passed.
|
||||
|
||||
POST a Multipart-Encoded File
|
||||
-----------------------------
|
||||
|
||||
@@ -71,6 +71,17 @@ def check_compatibility(urllib3_version, chardet_version):
|
||||
assert patch >= 2
|
||||
|
||||
|
||||
def _check_cryptography(cryptography_version):
|
||||
# cryptography < 1.3.4
|
||||
try:
|
||||
cryptography_version = list(map(int, cryptography_version.split('.')))
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if cryptography_version < [1, 3, 4]:
|
||||
warning = 'Old version of cryptography ({0}) may cause slowdown.'.format(cryptography_version)
|
||||
warnings.warn(warning, RequestsDependencyWarning)
|
||||
|
||||
# Check imported dependencies for compatibility.
|
||||
try:
|
||||
check_compatibility(urllib3.__version__, chardet.__version__)
|
||||
@@ -83,6 +94,10 @@ except (AssertionError, ValueError):
|
||||
try:
|
||||
from urllib3.contrib import pyopenssl
|
||||
pyopenssl.inject_into_urllib3()
|
||||
|
||||
# Check cryptography version
|
||||
from cryptography import __version__ as cryptography_version
|
||||
_check_cryptography(cryptography_version)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
+10
-5
@@ -13,6 +13,7 @@ import socket
|
||||
|
||||
from urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from urllib3.response import HTTPResponse
|
||||
from urllib3.util import parse_url
|
||||
from urllib3.util import Timeout as TimeoutSauce
|
||||
from urllib3.util.retry import Retry
|
||||
from urllib3.exceptions import ClosedPoolError
|
||||
@@ -28,13 +29,13 @@ from urllib3.exceptions import ResponseError
|
||||
|
||||
from .models import Response
|
||||
from .compat import urlparse, basestring
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
prepend_scheme_if_needed, get_auth_from_url, urldefragauth,
|
||||
select_proxy)
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths,
|
||||
get_encoding_from_headers, prepend_scheme_if_needed,
|
||||
get_auth_from_url, urldefragauth, select_proxy)
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
|
||||
ProxyError, RetryError, InvalidSchema)
|
||||
ProxyError, RetryError, InvalidSchema, InvalidProxyURL)
|
||||
from .auth import _basic_auth_str
|
||||
|
||||
try:
|
||||
@@ -219,7 +220,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
cert_loc = verify
|
||||
|
||||
if not cert_loc:
|
||||
cert_loc = DEFAULT_CA_BUNDLE_PATH
|
||||
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
|
||||
if not cert_loc or not os.path.exists(cert_loc):
|
||||
raise IOError("Could not find a suitable TLS CA certificate bundle, "
|
||||
@@ -300,6 +301,10 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
if proxy:
|
||||
proxy = prepend_scheme_if_needed(proxy, 'http')
|
||||
proxy_url = parse_url(proxy)
|
||||
if not proxy_url.host:
|
||||
raise InvalidProxyURL("Please check proxy URL. It is malformed"
|
||||
" and could be missing the host.")
|
||||
proxy_manager = self.proxy_manager_for(proxy)
|
||||
conn = proxy_manager.connection_from_url(url)
|
||||
else:
|
||||
|
||||
@@ -85,6 +85,10 @@ class InvalidHeader(RequestException, ValueError):
|
||||
"""The header value provided was somehow invalid."""
|
||||
|
||||
|
||||
class InvalidProxyURL(InvalidURL):
|
||||
"""The proxy URL provided is invalid."""
|
||||
|
||||
|
||||
class ChunkedEncodingError(RequestException):
|
||||
"""The server declared chunked encoding but sent an invalid chunk."""
|
||||
|
||||
|
||||
+2
-2
@@ -686,11 +686,11 @@ class Response(object):
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
"""Returns True if :attr:`status_code` is less than 400.
|
||||
"""Returns True if :attr:`status_code` is less than 400, False if not.
|
||||
|
||||
This attribute checks if the status code of the response is between
|
||||
400 and 600 to see if there was a client error or a server error. If
|
||||
the status code, is between 200 and 400, this will return True. This
|
||||
the status code is between 200 and 400, this will return True. This
|
||||
is **not** a check to see if the response code is ``200 OK``.
|
||||
"""
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ This module provides a Session object to manage and persist settings across
|
||||
requests (cookies, auth, proxies).
|
||||
"""
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
from collections import Mapping
|
||||
from datetime import timedelta
|
||||
@@ -38,8 +38,8 @@ from .status_codes import codes
|
||||
from .models import REDIRECT_STATI
|
||||
|
||||
# Preferred clock, based on which one is more accurate on a given system.
|
||||
if platform.system() == 'Windows':
|
||||
try: # Python 3.3+
|
||||
if sys.platform == 'win32':
|
||||
try: # Python 3.4+
|
||||
preferred_clock = time.perf_counter
|
||||
except AttributeError: # Earlier than Python 3.
|
||||
preferred_clock = time.clock
|
||||
@@ -696,7 +696,7 @@ class Session(SessionRedirectMixin):
|
||||
"""
|
||||
for (prefix, adapter) in self.adapters.items():
|
||||
|
||||
if url.lower().startswith(prefix):
|
||||
if url.lower().startswith(prefix.lower()):
|
||||
return adapter
|
||||
|
||||
# Nothing matches :-/
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The ``codes`` object defines a mapping from common names for HTTP statuses
|
||||
to their numerical codes, accessible either as attributes or as dictionary
|
||||
items.
|
||||
|
||||
>>> requests.codes['temporary_redirect']
|
||||
307
|
||||
>>> requests.codes.teapot
|
||||
418
|
||||
>>> requests.codes['\o/']
|
||||
200
|
||||
|
||||
Some codes have multiple names, and both upper- and lower-case versions of
|
||||
the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
|
||||
``codes.okay`` all correspond to the HTTP status code 200.
|
||||
"""
|
||||
|
||||
from .structures import LookupDict
|
||||
|
||||
_codes = {
|
||||
@@ -84,8 +101,19 @@ _codes = {
|
||||
|
||||
codes = LookupDict(name='status_codes')
|
||||
|
||||
for code, titles in _codes.items():
|
||||
for title in titles:
|
||||
setattr(codes, title, code)
|
||||
if not title.startswith(('\\', '/')):
|
||||
setattr(codes, title.upper(), code)
|
||||
def _init():
|
||||
for code, titles in _codes.items():
|
||||
for title in titles:
|
||||
setattr(codes, title, code)
|
||||
if not title.startswith(('\\', '/')):
|
||||
setattr(codes, title.upper(), code)
|
||||
|
||||
def doc(code):
|
||||
names = ', '.join('``%s``' % n for n in _codes[code])
|
||||
return '* %d: %s' % (code, names)
|
||||
|
||||
global __doc__
|
||||
__doc__ = (__doc__ + '\n' +
|
||||
'\n'.join(doc(code) for code in sorted(_codes)))
|
||||
|
||||
_init()
|
||||
|
||||
+85
-18
@@ -8,17 +8,18 @@ This module provides utility functions that are used within Requests
|
||||
that are also useful for external consumption.
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import codecs
|
||||
import collections
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
import zipfile
|
||||
|
||||
from .__version__ import __version__
|
||||
from . import certs
|
||||
@@ -39,19 +40,25 @@ NETRC_FILES = ('.netrc', '_netrc')
|
||||
DEFAULT_CA_BUNDLE_PATH = certs.where()
|
||||
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
if sys.platform == 'win32':
|
||||
# provide a proxy_bypass version on Windows without DNS lookups
|
||||
|
||||
def proxy_bypass_registry(host):
|
||||
if is_py3:
|
||||
import winreg
|
||||
else:
|
||||
import _winreg as winreg
|
||||
try:
|
||||
if is_py3:
|
||||
import winreg
|
||||
else:
|
||||
import _winreg as winreg
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
try:
|
||||
internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
|
||||
proxyEnable = winreg.QueryValueEx(internetSettings,
|
||||
'ProxyEnable')[0]
|
||||
# ProxyEnable could be REG_SZ or REG_DWORD, normalizing it
|
||||
proxyEnable = int(winreg.QueryValueEx(internetSettings,
|
||||
'ProxyEnable')[0])
|
||||
# ProxyOverride is almost always a string
|
||||
proxyOverride = winreg.QueryValueEx(internetSettings,
|
||||
'ProxyOverride')[0]
|
||||
except OSError:
|
||||
@@ -216,6 +223,38 @@ def guess_filename(obj):
|
||||
return os.path.basename(name)
|
||||
|
||||
|
||||
def extract_zipped_paths(path):
|
||||
"""Replace nonexistant paths that look like they refer to a member of a zip
|
||||
archive with the location of an extracted copy of the target, or else
|
||||
just return the provided path unchanged.
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
# this is already a valid path, no need to do anything further
|
||||
return path
|
||||
|
||||
# find the first valid part of the provided path and treat that as a zip archive
|
||||
# assume the rest of the path is the name of a member in the archive
|
||||
archive, member = os.path.split(path)
|
||||
while archive and not os.path.exists(archive):
|
||||
archive, prefix = os.path.split(archive)
|
||||
member = '/'.join([prefix, member])
|
||||
|
||||
if not zipfile.is_zipfile(archive):
|
||||
return path
|
||||
|
||||
zip_file = zipfile.ZipFile(archive)
|
||||
if member not in zip_file.namelist():
|
||||
return path
|
||||
|
||||
# we have a valid zip archive and a valid member of that archive
|
||||
tmp = tempfile.gettempdir()
|
||||
extracted_path = os.path.join(tmp, *member.split('/'))
|
||||
if not os.path.exists(extracted_path):
|
||||
extracted_path = zip_file.extract(member, path=tmp)
|
||||
|
||||
return extracted_path
|
||||
|
||||
|
||||
def from_key_val_list(value):
|
||||
"""Take an object and test to see if it can be represented as a
|
||||
dictionary. Unless it can not be represented as such, return an
|
||||
@@ -407,6 +446,31 @@ def get_encodings_from_content(content):
|
||||
xml_re.findall(content))
|
||||
|
||||
|
||||
def _parse_content_type_header(header):
|
||||
"""Returns content type and parameters from given header
|
||||
|
||||
:param header: string
|
||||
:return: tuple containing content type and dictionary of
|
||||
parameters
|
||||
"""
|
||||
|
||||
tokens = header.split(';')
|
||||
content_type, params = tokens[0].strip(), tokens[1:]
|
||||
params_dict = {}
|
||||
items_to_strip = "\"' "
|
||||
|
||||
for param in params:
|
||||
param = param.strip()
|
||||
if param:
|
||||
key, value = param, True
|
||||
index_of_equals = param.find("=")
|
||||
if index_of_equals != -1:
|
||||
key = param[:index_of_equals].strip(items_to_strip)
|
||||
value = param[index_of_equals + 1:].strip(items_to_strip)
|
||||
params_dict[key] = value
|
||||
return content_type, params_dict
|
||||
|
||||
|
||||
def get_encoding_from_headers(headers):
|
||||
"""Returns encodings from given HTTP Header Dict.
|
||||
|
||||
@@ -419,7 +483,7 @@ def get_encoding_from_headers(headers):
|
||||
if not content_type:
|
||||
return None
|
||||
|
||||
content_type, params = cgi.parse_header(content_type)
|
||||
content_type, params = _parse_content_type_header(content_type)
|
||||
|
||||
if 'charset' in params:
|
||||
return params['charset'].strip("'\"")
|
||||
@@ -639,28 +703,31 @@ def should_bypass_proxies(url, no_proxy):
|
||||
no_proxy_arg = no_proxy
|
||||
if no_proxy is None:
|
||||
no_proxy = get_proxy('no_proxy')
|
||||
netloc = urlparse(url).netloc
|
||||
parsed = urlparse(url)
|
||||
|
||||
if no_proxy:
|
||||
# We need to check whether we match here. We need to see if we match
|
||||
# the end of the netloc, both with and without the port.
|
||||
# the end of the hostname, both with and without the port.
|
||||
no_proxy = (
|
||||
host for host in no_proxy.replace(' ', '').split(',') if host
|
||||
)
|
||||
|
||||
ip = netloc.split(':')[0]
|
||||
if is_ipv4_address(ip):
|
||||
if is_ipv4_address(parsed.hostname):
|
||||
for proxy_ip in no_proxy:
|
||||
if is_valid_cidr(proxy_ip):
|
||||
if address_in_network(ip, proxy_ip):
|
||||
if address_in_network(parsed.hostname, proxy_ip):
|
||||
return True
|
||||
elif ip == proxy_ip:
|
||||
elif parsed.hostname == proxy_ip:
|
||||
# If no_proxy ip was defined in plain IP notation instead of cidr notation &
|
||||
# matches the IP of the index
|
||||
return True
|
||||
else:
|
||||
host_with_port = parsed.hostname
|
||||
if parsed.port:
|
||||
host_with_port += ':{0}'.format(parsed.port)
|
||||
|
||||
for host in no_proxy:
|
||||
if netloc.endswith(host) or netloc.split(':')[0].endswith(host):
|
||||
if parsed.hostname.endswith(host) or host_with_port.endswith(host):
|
||||
# The URL does match something in no_proxy, so we don't want
|
||||
# to apply the proxies on this URL.
|
||||
return True
|
||||
@@ -673,7 +740,7 @@ def should_bypass_proxies(url, no_proxy):
|
||||
# legitimate problems.
|
||||
with set_environ('no_proxy', no_proxy_arg):
|
||||
try:
|
||||
bypass = proxy_bypass(netloc)
|
||||
bypass = proxy_bypass(parsed.hostname)
|
||||
except (TypeError, socket.gaierror):
|
||||
bypass = False
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
@@ -72,6 +72,7 @@ setup(
|
||||
package_data={'': ['LICENSE', 'NOTICE'], 'requests': ['*.pem']},
|
||||
package_dir={'requests': 'requests'},
|
||||
include_package_data=True,
|
||||
python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
install_requires=requires,
|
||||
license=about['__license__'],
|
||||
zip_safe=False,
|
||||
@@ -81,6 +82,7 @@ setup(
|
||||
'Natural Language :: English',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
@@ -98,4 +100,3 @@ setup(
|
||||
'socks:sys_platform == "win32" and (python_version == "2.7" or python_version == "2.6")': ['win_inet_pton'],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+52
-1
@@ -23,7 +23,7 @@ from requests.cookies import (
|
||||
from requests.exceptions import (
|
||||
ConnectionError, ConnectTimeout, InvalidSchema, InvalidURL,
|
||||
MissingSchema, ReadTimeout, Timeout, RetryError, TooManyRedirects,
|
||||
ProxyError, InvalidHeader, UnrewindableBodyError, SSLError)
|
||||
ProxyError, InvalidHeader, UnrewindableBodyError, SSLError, InvalidProxyURL)
|
||||
from requests.models import PreparedRequest
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from requests.sessions import SessionRedirectMixin
|
||||
@@ -526,6 +526,19 @@ class TestRequests:
|
||||
with pytest.raises(ProxyError):
|
||||
requests.get('http://localhost:1', proxies={'http': 'non-resolvable-address'})
|
||||
|
||||
def test_proxy_error_on_bad_url(self, httpbin, httpbin_secure):
|
||||
with pytest.raises(InvalidProxyURL):
|
||||
requests.get(httpbin_secure(), proxies={'https': 'http:/badproxyurl:3128'})
|
||||
|
||||
with pytest.raises(InvalidProxyURL):
|
||||
requests.get(httpbin(), proxies={'http': 'http://:8080'})
|
||||
|
||||
with pytest.raises(InvalidProxyURL):
|
||||
requests.get(httpbin_secure(), proxies={'https': 'https://'})
|
||||
|
||||
with pytest.raises(InvalidProxyURL):
|
||||
requests.get(httpbin(), proxies={'http': 'http:///example.com:8080'})
|
||||
|
||||
def test_basicauth_with_netrc(self, httpbin):
|
||||
auth = ('user', 'pass')
|
||||
wrong_auth = ('wronguser', 'wrongpass')
|
||||
@@ -1351,6 +1364,44 @@ class TestRequests:
|
||||
assert 'http://' in s2.adapters
|
||||
assert 'https://' in s2.adapters
|
||||
|
||||
def test_session_get_adapter_prefix_matching(self, httpbin):
|
||||
prefix = 'https://example.com'
|
||||
more_specific_prefix = prefix + '/some/path'
|
||||
|
||||
url_matching_only_prefix = prefix + '/another/path'
|
||||
url_matching_more_specific_prefix = more_specific_prefix + '/longer/path'
|
||||
url_not_matching_prefix = 'https://another.example.com/'
|
||||
|
||||
s = requests.Session()
|
||||
prefix_adapter = HTTPAdapter()
|
||||
more_specific_prefix_adapter = HTTPAdapter()
|
||||
s.mount(prefix, prefix_adapter)
|
||||
s.mount(more_specific_prefix, more_specific_prefix_adapter)
|
||||
|
||||
assert s.get_adapter(url_matching_only_prefix) is prefix_adapter
|
||||
assert s.get_adapter(url_matching_more_specific_prefix) is more_specific_prefix_adapter
|
||||
assert s.get_adapter(url_not_matching_prefix) not in (prefix_adapter, more_specific_prefix_adapter)
|
||||
|
||||
def test_session_get_adapter_prefix_matching_mixed_case(self, httpbin):
|
||||
mixed_case_prefix = 'hTtPs://eXamPle.CoM/MixEd_CAse_PREfix'
|
||||
url_matching_prefix = mixed_case_prefix + '/full_url'
|
||||
|
||||
s = requests.Session()
|
||||
my_adapter = HTTPAdapter()
|
||||
s.mount(mixed_case_prefix, my_adapter)
|
||||
|
||||
assert s.get_adapter(url_matching_prefix) is my_adapter
|
||||
|
||||
def test_session_get_adapter_prefix_matching_is_case_insensitive(self, httpbin):
|
||||
mixed_case_prefix = 'hTtPs://eXamPle.CoM/MixEd_CAse_PREfix'
|
||||
url_matching_prefix_with_different_case = 'HtTpS://exaMPLe.cOm/MiXeD_caSE_preFIX/another_url'
|
||||
|
||||
s = requests.Session()
|
||||
my_adapter = HTTPAdapter()
|
||||
s.mount(mixed_case_prefix, my_adapter)
|
||||
|
||||
assert s.get_adapter(url_matching_prefix_with_different_case) is my_adapter
|
||||
|
||||
def test_header_remove_is_case_insensitive(self, httpbin):
|
||||
# From issue #1321
|
||||
s = requests.Session()
|
||||
|
||||
+98
-5
@@ -2,15 +2,18 @@
|
||||
|
||||
import os
|
||||
import copy
|
||||
import filecmp
|
||||
from io import BytesIO
|
||||
import zipfile
|
||||
from collections import deque
|
||||
|
||||
import pytest
|
||||
from requests import compat
|
||||
from requests.cookies import RequestsCookieJar
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from requests.utils import (
|
||||
address_in_network, dotted_netmask,
|
||||
get_auth_from_url, get_encoding_from_headers,
|
||||
address_in_network, dotted_netmask, extract_zipped_paths,
|
||||
get_auth_from_url, _parse_content_type_header, get_encoding_from_headers,
|
||||
get_encodings_from_content, get_environ_proxies,
|
||||
guess_filename, guess_json_utf, is_ipv4_address,
|
||||
is_valid_cidr, iter_slices, parse_dict_header,
|
||||
@@ -256,6 +259,32 @@ class TestGuessFilename:
|
||||
assert isinstance(result, expected_type)
|
||||
|
||||
|
||||
class TestExtractZippedPaths:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path', (
|
||||
'/',
|
||||
__file__,
|
||||
pytest.__file__,
|
||||
'/etc/invalid/location',
|
||||
))
|
||||
def test_unzipped_paths_unchanged(self, path):
|
||||
assert path == extract_zipped_paths(path)
|
||||
|
||||
def test_zipped_paths_extracted(self, tmpdir):
|
||||
zipped_py = tmpdir.join('test.zip')
|
||||
with zipfile.ZipFile(zipped_py.strpath, 'w') as f:
|
||||
f.write(__file__)
|
||||
|
||||
_, name = os.path.splitdrive(__file__)
|
||||
zipped_path = os.path.join(zipped_py.strpath, name.lstrip(r'\/'))
|
||||
extracted_path = extract_zipped_paths(zipped_path)
|
||||
|
||||
assert extracted_path != zipped_path
|
||||
assert os.path.exists(extracted_path)
|
||||
assert filecmp.cmp(extracted_path, __file__)
|
||||
|
||||
|
||||
class TestContentEncodingDetection:
|
||||
|
||||
def test_none(self):
|
||||
@@ -441,6 +470,45 @@ def test_parse_dict_header(value, expected):
|
||||
assert parse_dict_header(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value, expected', (
|
||||
(
|
||||
'application/xml',
|
||||
('application/xml', {})
|
||||
),
|
||||
(
|
||||
'application/json ; charset=utf-8',
|
||||
('application/json', {'charset': 'utf-8'})
|
||||
),
|
||||
(
|
||||
'text/plain',
|
||||
('text/plain', {})
|
||||
),
|
||||
(
|
||||
'multipart/form-data; boundary = something ; boundary2=\'something_else\' ; no_equals ',
|
||||
('multipart/form-data', {'boundary': 'something', 'boundary2': 'something_else', 'no_equals': True})
|
||||
),
|
||||
(
|
||||
'multipart/form-data; boundary = something ; boundary2="something_else" ; no_equals ',
|
||||
('multipart/form-data', {'boundary': 'something', 'boundary2': 'something_else', 'no_equals': True})
|
||||
),
|
||||
(
|
||||
'multipart/form-data; boundary = something ; \'boundary2=something_else\' ; no_equals ',
|
||||
('multipart/form-data', {'boundary': 'something', 'boundary2': 'something_else', 'no_equals': True})
|
||||
),
|
||||
(
|
||||
'multipart/form-data; boundary = something ; "boundary2=something_else" ; no_equals ',
|
||||
('multipart/form-data', {'boundary': 'something', 'boundary2': 'something_else', 'no_equals': True})
|
||||
),
|
||||
(
|
||||
'application/json ; ; ',
|
||||
('application/json', {})
|
||||
)
|
||||
))
|
||||
def test__parse_content_type_header(value, expected):
|
||||
assert _parse_content_type_header(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value, expected', (
|
||||
(
|
||||
@@ -546,6 +614,7 @@ def test_urldefragauth(url, expected):
|
||||
('http://172.16.1.1/', True),
|
||||
('http://172.16.1.1:5000/', True),
|
||||
('http://localhost.localdomain:5000/v1.0/', True),
|
||||
('http://google.com:6000/', True),
|
||||
('http://172.16.1.12/', False),
|
||||
('http://172.16.1.12:5000/', False),
|
||||
('http://google.com:5000/v1.0/', False),
|
||||
@@ -554,11 +623,31 @@ def test_should_bypass_proxies(url, expected, monkeypatch):
|
||||
"""Tests for function should_bypass_proxies to check if proxy
|
||||
can be bypassed or not
|
||||
"""
|
||||
monkeypatch.setenv('no_proxy', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1')
|
||||
monkeypatch.setenv('NO_PROXY', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1')
|
||||
monkeypatch.setenv('no_proxy', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000')
|
||||
monkeypatch.setenv('NO_PROXY', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000')
|
||||
assert should_bypass_proxies(url, no_proxy=None) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'url, expected', (
|
||||
('http://172.16.1.1/', '172.16.1.1'),
|
||||
('http://172.16.1.1:5000/', '172.16.1.1'),
|
||||
('http://user:pass@172.16.1.1', '172.16.1.1'),
|
||||
('http://user:pass@172.16.1.1:5000', '172.16.1.1'),
|
||||
('http://hostname/', 'hostname'),
|
||||
('http://hostname:5000/', 'hostname'),
|
||||
('http://user:pass@hostname', 'hostname'),
|
||||
('http://user:pass@hostname:5000', 'hostname'),
|
||||
))
|
||||
def test_should_bypass_proxies_pass_only_hostname(url, expected, mocker):
|
||||
"""The proxy_bypass function should be called with a hostname or IP without
|
||||
a port number or auth credentials.
|
||||
"""
|
||||
proxy_bypass = mocker.patch('requests.utils.proxy_bypass')
|
||||
should_bypass_proxies(url, no_proxy=None)
|
||||
proxy_bypass.assert_called_once_with(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'cookiejar', (
|
||||
compat.cookielib.CookieJar(),
|
||||
@@ -638,6 +727,7 @@ def test_should_bypass_proxies_win_registry(url, expected, override,
|
||||
pass
|
||||
|
||||
ie_settings = RegHandle()
|
||||
proxyEnableValues = deque([1, "1"])
|
||||
|
||||
def OpenKey(key, subkey):
|
||||
return ie_settings
|
||||
@@ -645,7 +735,9 @@ def test_should_bypass_proxies_win_registry(url, expected, override,
|
||||
def QueryValueEx(key, value_name):
|
||||
if key is ie_settings:
|
||||
if value_name == 'ProxyEnable':
|
||||
return [1]
|
||||
# this could be a string (REG_SZ) or a 32-bit number (REG_DWORD)
|
||||
proxyEnableValues.rotate()
|
||||
return [proxyEnableValues[0]]
|
||||
elif value_name == 'ProxyOverride':
|
||||
return [override]
|
||||
|
||||
@@ -656,6 +748,7 @@ def test_should_bypass_proxies_win_registry(url, expected, override,
|
||||
monkeypatch.setenv('NO_PROXY', '')
|
||||
monkeypatch.setattr(winreg, 'OpenKey', OpenKey)
|
||||
monkeypatch.setattr(winreg, 'QueryValueEx', QueryValueEx)
|
||||
assert should_bypass_proxies(url, None) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user