summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetter Reinholdtsen <pere@debian.org>2017-07-16 09:19:44 (GMT)
committerPetter Reinholdtsen <pere@debian.org>2017-07-16 09:19:44 (GMT)
commit64e31395286a5f149e626cd863ed6089b3a31c68 (patch)
tree95f8ff667a603c94ea08d882a91a2374ea3f7bb1
downloadpython-requests-toolbelt-64e31395286a5f149e626cd863ed6089b3a31c68.zip
python-requests-toolbelt-64e31395286a5f149e626cd863ed6089b3a31c68.tar.gz
python-requests-toolbelt-64e31395286a5f149e626cd863ed6089b3a31c68.tar.bz2
Import python-requests-toolbelt_0.8.0.orig.tar.gz
[dgit import orig python-requests-toolbelt_0.8.0.orig.tar.gz]
-rw-r--r--AUTHORS.rst43
-rw-r--r--CODE_OF_CONDUCT.rst54
-rw-r--r--HISTORY.rst297
-rw-r--r--LICENSE13
-rw-r--r--MANIFEST.in14
-rw-r--r--PKG-INFO434
-rw-r--r--README.rst114
-rw-r--r--dev-requirements.txt3
-rw-r--r--docs/Makefile177
-rw-r--r--docs/adapters.rst245
-rw-r--r--docs/authentication.rst142
-rw-r--r--docs/conf.py271
-rw-r--r--docs/contributing.rst161
-rw-r--r--docs/deprecated.rst13
-rw-r--r--docs/downloadutils.rst16
-rw-r--r--docs/dumputils.rst17
-rw-r--r--docs/exceptions.rst10
-rw-r--r--docs/formdata.rst7
-rw-r--r--docs/index.rst50
-rw-r--r--docs/make.bat242
-rw-r--r--docs/sessions.rst24
-rw-r--r--docs/threading.rst170
-rw-r--r--docs/uploading-data.rst172
-rw-r--r--docs/user-agent.rst94
-rw-r--r--docs/user.rst3
-rw-r--r--requests_toolbelt.egg-info/PKG-INFO434
-rw-r--r--requests_toolbelt.egg-info/SOURCES.txt99
-rw-r--r--requests_toolbelt.egg-info/dependency_links.txt1
-rw-r--r--requests_toolbelt.egg-info/requires.txt1
-rw-r--r--requests_toolbelt.egg-info/top_level.txt1
-rw-r--r--requests_toolbelt/__init__.py34
-rw-r--r--requests_toolbelt/_compat.py310
-rw-r--r--requests_toolbelt/adapters/__init__.py15
-rw-r--r--requests_toolbelt/adapters/appengine.py206
-rw-r--r--requests_toolbelt/adapters/fingerprint.py48
-rw-r--r--requests_toolbelt/adapters/host_header_ssl.py43
-rw-r--r--requests_toolbelt/adapters/socket_options.py118
-rw-r--r--requests_toolbelt/adapters/source.py67
-rw-r--r--requests_toolbelt/adapters/ssl.py66
-rw-r--r--requests_toolbelt/auth/__init__.py0
-rw-r--r--requests_toolbelt/auth/_digest_auth_compat.py29
-rw-r--r--requests_toolbelt/auth/guess.py146
-rw-r--r--requests_toolbelt/auth/handler.py142
-rw-r--r--requests_toolbelt/auth/http_proxy_digest.py103
-rw-r--r--requests_toolbelt/cookies/__init__.py0
-rw-r--r--requests_toolbelt/cookies/forgetful.py7
-rw-r--r--requests_toolbelt/downloadutils/__init__.py0
-rw-r--r--requests_toolbelt/downloadutils/stream.py177
-rw-r--r--requests_toolbelt/downloadutils/tee.py123
-rw-r--r--requests_toolbelt/exceptions.py37
-rw-r--r--requests_toolbelt/multipart/__init__.py31
-rw-r--r--requests_toolbelt/multipart/decoder.py158
-rw-r--r--requests_toolbelt/multipart/encoder.py570
-rw-r--r--requests_toolbelt/sessions.py70
-rw-r--r--requests_toolbelt/streaming_iterator.py116
-rw-r--r--requests_toolbelt/threaded/__init__.py97
-rw-r--r--requests_toolbelt/threaded/pool.py211
-rw-r--r--requests_toolbelt/threaded/thread.py53
-rw-r--r--requests_toolbelt/utils/__init__.py0
-rw-r--r--requests_toolbelt/utils/deprecated.py91
-rw-r--r--requests_toolbelt/utils/dump.py195
-rw-r--r--requests_toolbelt/utils/formdata.py108
-rw-r--r--requests_toolbelt/utils/user_agent.py143
-rw-r--r--setup.cfg7
-rw-r--r--setup.py68
-rw-r--r--tests/__init__.py8
-rw-r--r--tests/cassettes/http2bin_cookies.json1
-rw-r--r--tests/cassettes/http2bin_fingerprint.json1
-rw-r--r--tests/cassettes/httpbin_guess_auth_basic.json1
-rw-r--r--tests/cassettes/httpbin_guess_auth_digest.json1
-rw-r--r--tests/cassettes/httpbin_guess_auth_none.json1
-rw-r--r--tests/cassettes/klevas_vu_lt_ssl3.json1
-rw-r--r--tests/cassettes/redirect_request_for_dump_all.json1
-rw-r--r--tests/cassettes/simple_get_request.json1
-rw-r--r--tests/cassettes/stream_response_to_file.json1
-rw-r--r--tests/conftest.py15
-rw-r--r--tests/test_appengine_adapter.py89
-rw-r--r--tests/test_auth.py77
-rw-r--r--tests/test_auth_handler.py58
-rw-r--r--tests/test_downloadutils.py220
-rw-r--r--tests/test_dump.py382
-rw-r--r--tests/test_fingerprintadapter.py20
-rw-r--r--tests/test_forgetfulcookiejar.py26
-rw-r--r--tests/test_formdata.py76
-rw-r--r--tests/test_host_header_ssl_adapter.py48
-rw-r--r--tests/test_multipart_decoder.py164
-rw-r--r--tests/test_multipart_encoder.py260
-rw-r--r--tests/test_multipart_monitor.py65
-rw-r--r--tests/test_proxy_digest_auth.py112
-rw-r--r--tests/test_sessions.py28
-rw-r--r--tests/test_socket_options_adapter.py119
-rw-r--r--tests/test_source_adapter.py38
-rw-r--r--tests/test_ssladapter.py31
-rw-r--r--tests/test_streaming_iterator.py68
-rw-r--r--tests/test_user_agent.py105
-rw-r--r--tests/threaded/__init__.py0
-rw-r--r--tests/threaded/test_api.py59
-rw-r--r--tests/threaded/test_pool.py226
-rw-r--r--tests/threaded/test_thread.py131
-rw-r--r--tox.ini55
100 files changed, 9400 insertions, 0 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst
new file mode 100644
index 0000000..676e322
--- /dev/null
+++ b/AUTHORS.rst
@@ -0,0 +1,43 @@
+Requests-toolbelt is written and maintained by Ian Cordasco, Cory Benfield and
+various contributors:
+
+Development Lead
+````````````````
+
+- Ian Cordasco
+
+- Cory Benfield
+
+
+Requests
+````````
+
+- Kenneth Reitz <me@kennethreitz.com> and various contributors
+
+
+Urllib3
+```````
+
+- Andrey Petrov <andrey.petrov@shazow.net>
+
+
+Patches and Suggestions
+```````````````````````
+
+- Jay De Lanoy <jay@delanoy.co>
+
+- Zhaoyu Luo <luozhaoyu90@gmail.com>
+
+- Markus Unterwaditzer <markus@unterwaditzer.net>
+
+- Bryce Boe <bbzbryce@gmail.com> (@bboe)
+
+- Dan Lipsitt (https://github.com/DanLipsitt)
+
+- Cea Stapleton (http://www.ceastapleton.com)
+
+- Patrick Creech <pcreech@redhat.com>
+
+- Mike Lambert (@mikelambert)
+
+- Ryan Barrett (https://snarfed.org/)
diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst
new file mode 100644
index 0000000..fefaeae
--- /dev/null
+++ b/CODE_OF_CONDUCT.rst
@@ -0,0 +1,54 @@
+Contributor Code of Conduct
+---------------------------
+
+As contributors and maintainers of this project, and in the interest of
+fostering an open and welcoming community, we pledge to respect all
+people who contribute through reporting issues, posting feature
+requests, updating documentation, submitting pull requests or patches,
+and other activities.
+
+We are committed to making participation in this project a
+harassment-free experience for everyone, regardless of level of
+experience, gender, gender identity and expression, sexual orientation,
+disability, personal appearance, body size, race, ethnicity, age,
+religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic
+ addresses, without explicit permission
+* Other unethical or unprofessional conduct
+
+Project maintainers have the right and responsibility to remove, edit,
+or reject comments, commits, code, wiki edits, issues, and other
+contributions that are not aligned to this Code of Conduct, or to ban
+temporarily or permanently any contributor for other behaviors that they
+deem inappropriate, threatening, offensive, or harmful.
+
+By adopting this Code of Conduct, project maintainers commit themselves
+to fairly and consistently applying these principles to every aspect of
+managing this project. Project maintainers who do not follow or enforce
+the Code of Conduct may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public
+spaces when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may
+be reported by contacting a project maintainer at graffatcolmingov@gmail.com.
+All complaints will be reviewed and investigated and will
+result in a response that is deemed necessary and appropriate to the
+circumstances. Maintainers are obligated to maintain confidentiality
+with regard to the reporter of an incident.
+
+This Code of Conduct is adapted from the `Contributor Covenant`_, version
+1.3.0, available at http://contributor-covenant.org/version/1/3/0/
+
+.. _Contributor Covenant: http://contributor-covenant.org
+
+..
+ Re-formatted to reStructuredText from
+ https://raw.githubusercontent.com/CoralineAda/contributor_covenant/master/CODE_OF_CONDUCT.md
diff --git a/HISTORY.rst b/HISTORY.rst
new file mode 100644
index 0000000..e2ad26e
--- /dev/null
+++ b/HISTORY.rst
@@ -0,0 +1,297 @@
+History
+=======
+
+0.8.0 -- 2017-05-20
+-------------------
+
+More information about this release can be found on the `0.8.0 milestone`_.
+
+New Features
+~~~~~~~~~~~~
+
+- Add ``UserAgentBuilder`` to provide more control over generated User-Agent
+ strings.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Include ``_validate_certificate`` in the lits of picked attributes on the
+ ``AppEngineAdapter``.
+- Fix backwards incompatibility in ``get_encodings_from_content``
+
+.. _0.8.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.8.0
+
+0.7.1 -- 2017-02-13
+-------------------
+
+More information about this release can be found on the `0.7.1 milestone`_.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Fixed monkey-patching for the AppEngineAdapter.
+
+- Make it easier to disable certificate verification when monkey-patching
+ AppEngine.
+
+- Handle ``multipart/form-data`` bodies without a trailing ``CRLF``.
+
+
+.. links
+.. _0.7.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestone/9
+
+0.7.0 -- 2016-07-21
+-------------------
+
+More information about this release can be found on the `0.7.0 milestone`_.
+
+New Features
+~~~~~~~~~~~~
+
+- Add ``BaseUrlSession`` to allow developers to have a session that has a
+ "Base" URL. See the documentation for more details and examples.
+
+- Split the logic of ``stream_response_to_file`` into two separate functions:
+
+ * ``get_download_file_path`` to generate the file name from the Response.
+
+ * ``stream_response_to_file`` which will use ``get_download_file_path`` if
+ necessary
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Fixed the issue for people using *very* old versions of Requests where they
+ would see an ImportError from ``requests_toolbelt._compat`` when trying to
+ import ``connection``.
+
+
+.. _0.7.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.7.0
+
+0.6.2 -- 2016-05-10
+-------------------
+
+Fixed Bugs
+~~~~~~~~~~
+
+- When passing a timeout via Requests, it was not appropriately translated to
+ the timeout that the urllib3 code was expecting.
+
+0.6.1 -- 2016-05-05
+-------------------
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Remove assertion about request URLs in the AppEngineAdapter.
+
+- Prevent pip from installing requests 3.0.0 when that is released until we
+ are ready to handle it.
+
+0.6.0 -- 2016-01-27
+-------------------
+
+More information about this release can be found on the `0.6.0 milestone`_.
+
+New Features
+~~~~~~~~~~~~
+
+- Add ``AppEngineAdapter`` to support developers using Google's AppEngine
+ platform with Requests.
+
+- Add ``GuessProxyAuth`` class to support guessing between Basic and Digest
+ Authentication for proxies.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Ensure that proxies use the correct TLS version when using the
+ ``SSLAdapter``.
+
+- Fix an ``AttributeError`` when using the ``HTTPProxyDigestAuth`` class.
+
+Miscellaneous
+~~~~~~~~~~~~~
+
+- Drop testing support for Python 3.2. virtualenv and pip have stopped
+ supporting it meaning that it is harder to test for this with our CI
+ infrastructure. Moving forward we will make a best-effort attempt to
+ support 3.2 but will not test for it.
+
+
+.. _0.6.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.6.0
+
+0.5.1 -- 2015-12-16
+-------------------
+
+More information about this release can be found on the `0.5.1 milestone`_.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Now papers over the differences in requests' ``super_len`` function from
+ versions prior to 2.9.0 and versions 2.9.0 and later.
+
+
+.. _0.5.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.5.1
+
+0.5.0 -- 2015-11-24
+-------------------
+
+More information about this release can be found on the `milestone
+<https://github.com/sigmavirus24/requests-toolbelt/issues?utf8=%E2%9C%93&q=is%3Aall+milestone%3A0.5+>`_
+for 0.5.0.
+
+New Features
+~~~~~~~~~~~~
+
+- The ``tee`` submodule was added to ``requests_toolbelt.downloadutils``. It
+ allows you to iterate over the bytes of a response while also writing them
+ to a file. The ``tee.tee`` function, expects you to pass an open file
+ object, while ``tee.tee_to_file`` will use the provided file name to open
+ the file for you.
+
+- Added a new parameter to ``requests_toolbelt.utils.user_agent`` that allows
+ the user to specify additional items.
+
+- Added nested form-data helper,
+ ``requests_toolbelt.utils.formdata.urlencode``.
+
+- Added the ``ForgetfulCookieJar`` to ``requests_toolbelt.cookies``.
+
+- Added utilities for dumping the information about a request-response cycle
+ in ``requests_toolbelt.utils.dump``.
+
+- Implemented the API described in the ``requests_toolbelt.threaded`` module
+ docstring, i.e., added ``requests_toolbelt.threaded.map`` as an available
+ function.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- Now papers over the API differences in versions of requests installed from
+ system packages versus versions of requests installed from PyPI.
+
+- Allow string types for ``SourceAddressAdapter``.
+
+0.4.0 -- 2015-04-03
+-------------------
+
+For more information about this release, please see `milestone 0.4.0
+<https://github.com/sigmavirus24/requests-toolbelt/issues?q=milestone%3A0.4>`_
+on the project's page.
+
+New Features
+~~~~~~~~~~~~
+
+- A naive implemenation of a thread pool is now included in the toolbelt. See
+ the docs in ``docs/threading.rst`` or on `Read The Docs
+ <https://toolbelt.readthedocs.org>`_.
+
+- The ``StreamingIterator`` now accepts files (such as ``sys.stdin``) without
+ a specific length and will properly stream them.
+
+- The ``MultipartEncoder`` now accepts exactly the same format of fields as
+ requests' ``files`` parameter does. In other words, you can now also pass in
+ extra headers to add to a part in the body. You can also now specify a
+ custom ``Content-Type`` for a part.
+
+- An implementation of HTTP Digest Authentication for Proxies is now included.
+
+- A transport adapter that allows a user to specify a specific Certificate
+ Fingerprint is now included in the toolbelt.
+
+- A transport adapter that simplifies how users specify socket options is now
+ included.
+
+- A transport adapter that simplifies how users can specify TCP Keep-Alive
+ options is now included in the toolbelt.
+
+- Deprecated functions from ``requests.utils`` are now included and
+ maintained.
+
+- An authentication tool that allows users to specify how to authenticate to
+ several different domains at once is now included.
+
+- A function to save streamed responses to disk by analyzing the
+ ``Content-Disposition`` header is now included in the toolbelt.
+
+Fixed Bugs
+~~~~~~~~~~
+
+- The ``MultipartEncoder`` will now allow users to upload files larger than
+ 4GB on 32-bit systems.
+
+- The ``MultipartEncoder`` will now accept empty unicode strings for form
+ values.
+
+0.3.1 -- 2014-06-23
+-------------------
+
+- Fix the fact that 0.3.0 bundle did not include the ``StreamingIterator``
+
+0.3.0 -- 2014-05-21
+-------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Complete rewrite of ``MultipartEncoder`` fixes bug where bytes were lost in
+ uploads
+
+New Features
+~~~~~~~~~~~~
+
+- ``MultipartDecoder`` to accept ``multipart/form-data`` response bodies and
+ parse them into an easy to use object.
+
+- ``SourceAddressAdapter`` to allow users to choose a local address to bind
+ connections to.
+
+- ``GuessAuth`` which accepts a username and password and uses the
+ ``WWW-Authenticate`` header to determine how to authenticate against a
+ server.
+
+- ``MultipartEncoderMonitor`` wraps an instance of the ``MultipartEncoder``
+ and keeps track of how many bytes were read and will call the provided
+ callback.
+
+- ``StreamingIterator`` will wrap an iterator and stream the upload instead of
+ chunk it, provided you also provide the length of the content you wish to
+ upload.
+
+0.2.0 -- 2014-02-24
+-------------------
+
+- Add ability to tell ``MultipartEncoder`` which encoding to use. By default
+ it uses 'utf-8'.
+
+- Fix #10 - allow users to install with pip
+
+- Fix #9 - Fix ``MultipartEncoder#to_string`` so that it properly handles file
+ objects as fields
+
+0.1.2 -- 2014-01-19
+-------------------
+
+- At some point during development we broke how we handle normal file objects.
+ Thanks to @konomae this is now fixed.
+
+0.1.1 -- 2014-01-19
+-------------------
+
+- Handle ``io.BytesIO``-like objects better
+
+0.1.0 -- 2014-01-18
+-------------------
+
+- Add initial implementation of the streaming ``MultipartEncoder``
+
+- Add initial implementation of the ``user_agent`` function
+
+- Add the ``SSLAdapter``
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..13c64ac
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2014 Ian Cordasco, Cory Benfield
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..bb6d32d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,14 @@
+include README.rst
+include LICENSE
+include HISTORY.rst
+include AUTHORS.rst
+include CODE_OF_CONDUCT.rst
+include tox.ini
+include dev-requirements.txt
+
+recursive-include requests_toolbelt *
+recursive-include docs *
+recursive-include tests *
+
+prune docs/_build
+global-exclude *.py[cdo] __pycache__ *.so *.pyd
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..23e3aac
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,434 @@
+Metadata-Version: 1.1
+Name: requests-toolbelt
+Version: 0.8.0
+Summary: A utility belt for advanced users of python-requests
+Home-page: https://toolbelt.readthedocs.org
+Author: Ian Cordasco, Cory Benfield
+Author-email: graffatcolmingov@gmail.com
+License: Apache 2.0
+Description: requests toolbelt
+ =================
+
+ This is just a collection of utilities for `python-requests`_, but don't
+ really belong in ``requests`` proper. The minimum tested requests version is
+ ``2.1.0``. In reality, the toolbelt should work with ``2.0.1`` as well, but
+ some idiosyncracies prevent effective or sane testing on that version.
+
+ ``pip install requests-toolbelt`` to get started!
+
+
+ multipart/form-data Encoder
+ ---------------------------
+
+ The main attraction is a streaming multipart form-data object, ``MultipartEncoder``.
+ Its API looks like this:
+
+ .. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
+ )
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+ You can also use ``multipart/form-data`` encoding for requests that don't
+ require files:
+
+ .. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(fields={'field0': 'value', 'field1': 'value'})
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+ Or, you can just create the string and examine the data:
+
+ .. code-block:: python
+
+ # Assuming `m` is one of the above
+ m.to_string() # Always returns unicode
+
+
+ User-Agent constructor
+ ----------------------
+
+ You can easily construct a requests-style ``User-Agent`` string::
+
+ from requests_toolbelt import user_agent
+
+ headers = {
+ 'User-Agent': user_agent('my_package', '0.0.1')
+ }
+
+ r = requests.get('https://api.github.com/users', headers=headers)
+
+
+ SSLAdapter
+ ----------
+
+ The ``SSLAdapter`` was originally published on `Cory Benfield's blog`_.
+ This adapter allows the user to choose one of the SSL protocols made available
+ in Python's ``ssl`` module for outgoing HTTPS connections:
+
+ .. code-block:: python
+
+ from requests_toolbelt import SSLAdapter
+ import requests
+ import ssl
+
+ s = requests.Session()
+ s.mount('https://', SSLAdapter(ssl.PROTOCOL_TLSv1))
+
+ cookies/ForgetfulCookieJar
+ --------------------------
+
+ The ``ForgetfulCookieJar`` prevents a particular requests session from storing
+ cookies:
+
+ .. code-block:: python
+
+ from requests_toolbelt.cookies.forgetful import ForgetfulCookieJar
+
+ session = requests.Session()
+ session.cookies = ForgetfulCookieJar()
+
+ Known Issues
+ ------------
+
+ On Python 3.3.0 and 3.3.1, the standard library's ``http`` module will fail
+ when passing an instance of the ``MultipartEncoder``. This is fixed in later
+ minor releases of Python 3.3. Please consider upgrading to a later minor
+ version or Python 3.4. *There is absolutely nothing this library can do to
+ work around that bug.*
+
+ Contributing
+ ------------
+
+ Please read the `suggested workflow
+ <https://toolbelt.readthedocs.org/en/latest/contributing.html>`_ for
+ contributing to this project.
+
+ .. _Cory Benfield's blog: https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
+ .. _python-requests: https://github.com/kennethreitz/requests
+
+
+ History
+ =======
+
+ 0.8.0 -- 2017-05-20
+ -------------------
+
+ More information about this release can be found on the `0.8.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``UserAgentBuilder`` to provide more control over generated User-Agent
+ strings.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Include ``_validate_certificate`` in the lits of picked attributes on the
+ ``AppEngineAdapter``.
+ - Fix backwards incompatibility in ``get_encodings_from_content``
+
+ .. _0.8.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.8.0
+
+ 0.7.1 -- 2017-02-13
+ -------------------
+
+ More information about this release can be found on the `0.7.1 milestone`_.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Fixed monkey-patching for the AppEngineAdapter.
+
+ - Make it easier to disable certificate verification when monkey-patching
+ AppEngine.
+
+ - Handle ``multipart/form-data`` bodies without a trailing ``CRLF``.
+
+
+ .. links
+ .. _0.7.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestone/9
+
+ 0.7.0 -- 2016-07-21
+ -------------------
+
+ More information about this release can be found on the `0.7.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``BaseUrlSession`` to allow developers to have a session that has a
+ "Base" URL. See the documentation for more details and examples.
+
+ - Split the logic of ``stream_response_to_file`` into two separate functions:
+
+ * ``get_download_file_path`` to generate the file name from the Response.
+
+ * ``stream_response_to_file`` which will use ``get_download_file_path`` if
+ necessary
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Fixed the issue for people using *very* old versions of Requests where they
+ would see an ImportError from ``requests_toolbelt._compat`` when trying to
+ import ``connection``.
+
+
+ .. _0.7.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.7.0
+
+ 0.6.2 -- 2016-05-10
+ -------------------
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - When passing a timeout via Requests, it was not appropriately translated to
+ the timeout that the urllib3 code was expecting.
+
+ 0.6.1 -- 2016-05-05
+ -------------------
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Remove assertion about request URLs in the AppEngineAdapter.
+
+ - Prevent pip from installing requests 3.0.0 when that is released until we
+ are ready to handle it.
+
+ 0.6.0 -- 2016-01-27
+ -------------------
+
+ More information about this release can be found on the `0.6.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``AppEngineAdapter`` to support developers using Google's AppEngine
+ platform with Requests.
+
+ - Add ``GuessProxyAuth`` class to support guessing between Basic and Digest
+ Authentication for proxies.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Ensure that proxies use the correct TLS version when using the
+ ``SSLAdapter``.
+
+ - Fix an ``AttributeError`` when using the ``HTTPProxyDigestAuth`` class.
+
+ Miscellaneous
+ ~~~~~~~~~~~~~
+
+ - Drop testing support for Python 3.2. virtualenv and pip have stopped
+ supporting it meaning that it is harder to test for this with our CI
+ infrastructure. Moving forward we will make a best-effort attempt to
+ support 3.2 but will not test for it.
+
+
+ .. _0.6.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.6.0
+
+ 0.5.1 -- 2015-12-16
+ -------------------
+
+ More information about this release can be found on the `0.5.1 milestone`_.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Now papers over the differences in requests' ``super_len`` function from
+ versions prior to 2.9.0 and versions 2.9.0 and later.
+
+
+ .. _0.5.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.5.1
+
+ 0.5.0 -- 2015-11-24
+ -------------------
+
+ More information about this release can be found on the `milestone
+ <https://github.com/sigmavirus24/requests-toolbelt/issues?utf8=%E2%9C%93&q=is%3Aall+milestone%3A0.5+>`_
+ for 0.5.0.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - The ``tee`` submodule was added to ``requests_toolbelt.downloadutils``. It
+ allows you to iterate over the bytes of a response while also writing them
+ to a file. The ``tee.tee`` function, expects you to pass an open file
+ object, while ``tee.tee_to_file`` will use the provided file name to open
+ the file for you.
+
+ - Added a new parameter to ``requests_toolbelt.utils.user_agent`` that allows
+ the user to specify additional items.
+
+ - Added nested form-data helper,
+ ``requests_toolbelt.utils.formdata.urlencode``.
+
+ - Added the ``ForgetfulCookieJar`` to ``requests_toolbelt.cookies``.
+
+ - Added utilities for dumping the information about a request-response cycle
+ in ``requests_toolbelt.utils.dump``.
+
+ - Implemented the API described in the ``requests_toolbelt.threaded`` module
+ docstring, i.e., added ``requests_toolbelt.threaded.map`` as an available
+ function.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Now papers over the API differences in versions of requests installed from
+ system packages versus versions of requests installed from PyPI.
+
+ - Allow string types for ``SourceAddressAdapter``.
+
+ 0.4.0 -- 2015-04-03
+ -------------------
+
+ For more information about this release, please see `milestone 0.4.0
+ <https://github.com/sigmavirus24/requests-toolbelt/issues?q=milestone%3A0.4>`_
+ on the project's page.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - A naive implemenation of a thread pool is now included in the toolbelt. See
+ the docs in ``docs/threading.rst`` or on `Read The Docs
+ <https://toolbelt.readthedocs.org>`_.
+
+ - The ``StreamingIterator`` now accepts files (such as ``sys.stdin``) without
+ a specific length and will properly stream them.
+
+ - The ``MultipartEncoder`` now accepts exactly the same format of fields as
+ requests' ``files`` parameter does. In other words, you can now also pass in
+ extra headers to add to a part in the body. You can also now specify a
+ custom ``Content-Type`` for a part.
+
+ - An implementation of HTTP Digest Authentication for Proxies is now included.
+
+ - A transport adapter that allows a user to specify a specific Certificate
+ Fingerprint is now included in the toolbelt.
+
+ - A transport adapter that simplifies how users specify socket options is now
+ included.
+
+ - A transport adapter that simplifies how users can specify TCP Keep-Alive
+ options is now included in the toolbelt.
+
+ - Deprecated functions from ``requests.utils`` are now included and
+ maintained.
+
+ - An authentication tool that allows users to specify how to authenticate to
+ several different domains at once is now included.
+
+ - A function to save streamed responses to disk by analyzing the
+ ``Content-Disposition`` header is now included in the toolbelt.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - The ``MultipartEncoder`` will now allow users to upload files larger than
+ 4GB on 32-bit systems.
+
+ - The ``MultipartEncoder`` will now accept empty unicode strings for form
+ values.
+
+ 0.3.1 -- 2014-06-23
+ -------------------
+
+ - Fix the fact that 0.3.0 bundle did not include the ``StreamingIterator``
+
+ 0.3.0 -- 2014-05-21
+ -------------------
+
+ Bug Fixes
+ ~~~~~~~~~
+
+ - Complete rewrite of ``MultipartEncoder`` fixes bug where bytes were lost in
+ uploads
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - ``MultipartDecoder`` to accept ``multipart/form-data`` response bodies and
+ parse them into an easy to use object.
+
+ - ``SourceAddressAdapter`` to allow users to choose a local address to bind
+ connections to.
+
+ - ``GuessAuth`` which accepts a username and password and uses the
+ ``WWW-Authenticate`` header to determine how to authenticate against a
+ server.
+
+ - ``MultipartEncoderMonitor`` wraps an instance of the ``MultipartEncoder``
+ and keeps track of how many bytes were read and will call the provided
+ callback.
+
+ - ``StreamingIterator`` will wrap an iterator and stream the upload instead of
+ chunk it, provided you also provide the length of the content you wish to
+ upload.
+
+ 0.2.0 -- 2014-02-24
+ -------------------
+
+ - Add ability to tell ``MultipartEncoder`` which encoding to use. By default
+ it uses 'utf-8'.
+
+ - Fix #10 - allow users to install with pip
+
+ - Fix #9 - Fix ``MultipartEncoder#to_string`` so that it properly handles file
+ objects as fields
+
+ 0.1.2 -- 2014-01-19
+ -------------------
+
+ - At some point during development we broke how we handle normal file objects.
+ Thanks to @konomae this is now fixed.
+
+ 0.1.1 -- 2014-01-19
+ -------------------
+
+ - Handle ``io.BytesIO``-like objects better
+
+ 0.1.0 -- 2014-01-18
+ -------------------
+
+ - Add initial implementation of the streaming ``MultipartEncoder``
+
+ - Add initial implementation of the ``user_agent`` function
+
+ - Add the ``SSLAdapter``
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: CPython
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..ff3151b
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,114 @@
+requests toolbelt
+=================
+
+This is just a collection of utilities for `python-requests`_, but don't
+really belong in ``requests`` proper. The minimum tested requests version is
+``2.1.0``. In reality, the toolbelt should work with ``2.0.1`` as well, but
+some idiosyncracies prevent effective or sane testing on that version.
+
+``pip install requests-toolbelt`` to get started!
+
+
+multipart/form-data Encoder
+---------------------------
+
+The main attraction is a streaming multipart form-data object, ``MultipartEncoder``.
+Its API looks like this:
+
+.. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
+ )
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+You can also use ``multipart/form-data`` encoding for requests that don't
+require files:
+
+.. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(fields={'field0': 'value', 'field1': 'value'})
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+Or, you can just create the string and examine the data:
+
+.. code-block:: python
+
+ # Assuming `m` is one of the above
+ m.to_string() # Always returns unicode
+
+
+User-Agent constructor
+----------------------
+
+You can easily construct a requests-style ``User-Agent`` string::
+
+ from requests_toolbelt import user_agent
+
+ headers = {
+ 'User-Agent': user_agent('my_package', '0.0.1')
+ }
+
+ r = requests.get('https://api.github.com/users', headers=headers)
+
+
+SSLAdapter
+----------
+
+The ``SSLAdapter`` was originally published on `Cory Benfield's blog`_.
+This adapter allows the user to choose one of the SSL protocols made available
+in Python's ``ssl`` module for outgoing HTTPS connections:
+
+.. code-block:: python
+
+ from requests_toolbelt import SSLAdapter
+ import requests
+ import ssl
+
+ s = requests.Session()
+ s.mount('https://', SSLAdapter(ssl.PROTOCOL_TLSv1))
+
+cookies/ForgetfulCookieJar
+--------------------------
+
+The ``ForgetfulCookieJar`` prevents a particular requests session from storing
+cookies:
+
+.. code-block:: python
+
+ from requests_toolbelt.cookies.forgetful import ForgetfulCookieJar
+
+ session = requests.Session()
+ session.cookies = ForgetfulCookieJar()
+
+Known Issues
+------------
+
+On Python 3.3.0 and 3.3.1, the standard library's ``http`` module will fail
+when passing an instance of the ``MultipartEncoder``. This is fixed in later
+minor releases of Python 3.3. Please consider upgrading to a later minor
+version or Python 3.4. *There is absolutely nothing this library can do to
+work around that bug.*
+
+Contributing
+------------
+
+Please read the `suggested workflow
+<https://toolbelt.readthedocs.org/en/latest/contributing.html>`_ for
+contributing to this project.
+
+.. _Cory Benfield's blog: https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
+.. _python-requests: https://github.com/kennethreitz/requests
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..e15e2d3
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,3 @@
+pytest
+mock
+git+git://github.com/sigmavirus24/betamax
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..1ef33cf
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/requests_toolbelt.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/requests_toolbelt.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/requests_toolbelt"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/requests_toolbelt"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/adapters.rst b/docs/adapters.rst
new file mode 100644
index 0000000..ec24154
--- /dev/null
+++ b/docs/adapters.rst
@@ -0,0 +1,245 @@
+.. _adapters:
+
+Transport Adapters
+==================
+
+The toolbelt comes with several different transport adapters for you to use
+with requests. The transport adapters are all kept in
+:mod:`requests_toolbelt.adapters` and include
+
+- :class:`requests_toolbelt.adapters.appengine.AppEngineAdapter`
+
+- :class:`requests_toolbelt.adapters.fingerprint.FingerprintAdapter`
+
+- :class:`requests_toolbelt.adapters.socket_options.SocketOptionsAdapter`
+
+- :class:`requests_toolbelt.adapters.socket_options.TCPKeepAliveAdapter`
+
+- :class:`requests_toolbelt.adapters.source.SourceAddressAdapter`
+
+- :class:`requests_toolbelt.adapters.ssl.SSLAdapter`
+
+- :class:`requests_toolbelt.adapters.host_header_ssl.HostHeaderSSLAdapter`
+
+AppEngineAdapter
+----------------
+
+.. versionadded:: 0.6.0
+
+As of version 2.10.0, Requests will be capable of supporting Google's App
+Engine platform. In order to use Requests on GAE, however, you will need a
+custom adapter found here as
+:class:`~requests_toolbelt.adapters.appengine.AppEngineAdapter`. There are two
+ways to take advantage of this support at the moment:
+
+#. Using the :class:`~requests_toolbelt.adapters.appengine.AppEngineAdapter`
+ like every other adapter, e.g.,
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.adapters import appengine
+
+ s = requests.Session()
+ s.mount('http://', appengine.AppEngineAdapter())
+ s.mount('https://', appengine.AppEngineAdapter())
+
+#. By monkey-patching requests to always use the provided adapter:
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.adapters import appengine
+
+ appengine.monkeypatch()
+
+.. _insecure_appengine:
+
+If you should need to disable certificate validation when monkeypatching (to
+force third-party libraries that use Requests to not validate certificates, if
+they do not provide API surface to do so, for example), you can disable it:
+
+ .. code-block:: python
+
+ from requests_toolbelt.adapters import appengine
+ appengine.monkeypatch(validate_certificate=False)
+
+ .. warning::
+
+ If ``validate_certificate`` is ``False``, the monkeypatched adapter
+ will *not* validate certificates. This effectively sets the
+ ``validate_certificate`` argument to urlfetch.Fetch() to ``False``. You
+ should avoid using this wherever possible. Details can be found in the
+ `documentation for urlfetch.Fetch()`_.
+
+ .. _documentation for urlfetch.Fetch(): https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api.urlfetch
+
+.. autoclass:: requests_toolbelt.adapters.appengine.AppEngineAdapter
+
+FingerprintAdapter
+------------------
+
+.. versionadded:: 0.4.0
+
+By default, requests will validate a server's certificate to ensure a
+connection is secure. In addition to this, the user can provide a fingerprint
+of the certificate they're expecting to receive. Unfortunately, the requests
+API does not support this fairly rare use-case. When a user needs this extra
+validation, they should use the
+:class:`~requests_toolbelt.adapters.fingerprint.FingerprintAdapter` class to
+perform the validation.
+
+.. autoclass:: requests_toolbelt.adapters.fingerprint.FingerprintAdapter
+
+SSLAdapter
+----------
+
+The ``SSLAdapter`` is the canonical implementation of the adapter proposed on
+Cory Benfield's blog, `here`_. This adapter allows the user to choose one of
+the SSL/TLS protocols made available in Python's ``ssl`` module for outgoing
+HTTPS connections.
+
+In principle, this shouldn't be necessary: compliant SSL servers should be able
+to negotiate the required SSL version. In practice there have been bugs in some
+versions of OpenSSL that mean that this negotiation doesn't go as planned. It
+can be useful to be able to simply plug in a Transport Adapter that can paste
+over the problem.
+
+For example, suppose you're having difficulty with the server that provides TLS
+for GitHub. You can work around it by using the following code::
+
+ from requests_toolbelt.adapters.ssl import SSLAdapter
+
+ import requests
+ import ssl
+
+ s = requests.Session()
+ s.mount('https://github.com/', SSLAdapter(ssl.PROTOCOL_TLSv1))
+
+Any future requests to GitHub made through that adapter will automatically
+attempt to negotiate TLSv1, and hopefully will succeed.
+
+.. autoclass:: requests_toolbelt.adapters.ssl.SSLAdapter
+
+.. _here: https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
+
+HostHeaderSSLAdapter
+--------------------
+
+.. versionadded:: 0.7.0
+
+Requests supports SSL Verification by default. However, it relies on
+the user making a request with the URL that has the hostname in it. If,
+however, the user needs to make a request with the IP address, they cannot
+actually verify a certificate against the hostname they want to request.
+
+To accomodate this very rare need, we've added
+:class:`~requests_toolbelt.adapters.host_header_ssl.HostHeaderSSLAdapter`.
+Example usage:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.adapters import host_header_ssl
+ s = requests.Session()
+ s.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
+ s.get("https://93.184.216.34", headers={"Host": "example.org"})
+
+.. autoclass:: requests_toolbelt.adapters.host_header_ssl.HostHeaderSSLAdapter
+
+SourceAddressAdapter
+--------------------
+
+.. versionadded:: 0.3.0
+
+The :class:`~requests_toolbelt.adapters.source.SourceAddressAdapter` allows a
+user to specify a source address for their connnection.
+
+.. autoclass:: requests_toolbelt.adapters.source.SourceAddressAdapter
+
+SocketOptionsAdapter
+--------------------
+
+.. versionadded:: 0.4.0
+
+.. note::
+
+ This adapter will only work with requests 2.4.0 or newer. The ability to
+ set arbitrary socket options does not exist prior to requests 2.4.0.
+
+The ``SocketOptionsAdapter`` allows a user to pass specific options to be set
+on created sockets when constructing the Adapter without subclassing. The
+adapter takes advantage of ``urllib3``'s `support`_ for setting arbitrary
+socket options for each ``urllib3.connection.HTTPConnection`` (and
+``HTTPSConnection``).
+
+To pass socket options, you need to send a list of three-item tuples. For
+example, ``requests`` and ``urllib3`` disable `Nagle's Algorithm`_ by default.
+If you need to re-enable it, you would do the following:
+
+.. code-block:: python
+
+ import socket
+ import requests
+ from requests_toolbelt.adapters.socket_options import SocketOptionsAdapter
+
+ nagles = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)]
+ session = requests.Session()
+ for scheme in session.adapters.keys():
+ session.mount(scheme, SocketOptionsAdapter(socket_options=nagles))
+
+This would re-enable Nagle's Algorithm for all ``http://`` and ``https://``
+connections made with that session.
+
+.. autoclass:: requests_toolbelt.adapters.socket_options.SocketOptionsAdapter
+
+.. _support: https://urllib3.readthedocs.org/en/latest/pools.html?highlight=socket_options#urllib3.connection.HTTPConnection.socket_options
+.. _Nagle's Algorithm: https://en.wikipedia.org/wiki/Nagle%27s_algorithm
+
+TCPKeepAliveAdapter
+-------------------
+
+.. versionadded:: 0.4.0
+
+.. note::
+
+ This adapter will only work with requests 2.4.0 or newer. The ability to
+ set arbitrary socket options does not exist prior to requests 2.4.0.
+
+The ``TCPKeepAliveAdapter`` allows a user to pass specific keep-alive related
+options as keyword parameters as well as arbitrary socket options.
+
+.. note::
+
+ Different keep-alive related socket options may not be available for your
+ platform. Check the socket module for the availability of the following
+ constants:
+
+ - ``socket.TCP_KEEPIDLE``
+ - ``socket.TCP_KEEPCNT``
+ - ``socket.TCP_KEEPINTVL``
+
+ The adapter will silently ignore any option passed for a non-existent
+ option.
+
+An example usage of the adapter:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
+
+ session = requests.Session()
+ keep_alive = TCPKeepAliveAdapter(idle=120, count=20, interval=30)
+ session.mount('https://region-a.geo-1.compute.hpcloudsvc.com', keep_alive)
+ session.post('https://region-a.geo-1.compute.hpcloudsvc.com/v2/1234abcdef/servers',
+ # ...
+ )
+
+In this case we know that creating a server on HP Public Cloud can cause
+requests to hang without using TCP Keep-Alive. So we mount the adapter
+specifically for that domain, instead of adding it to every ``https://`` and
+``http://`` request.
+
+.. autoclass:: requests_toolbelt.adapters.socket_options.TCPKeepAliveAdapter
+
diff --git a/docs/authentication.rst b/docs/authentication.rst
new file mode 100644
index 0000000..d835490
--- /dev/null
+++ b/docs/authentication.rst
@@ -0,0 +1,142 @@
+.. _authentication:
+
+Authentication
+==============
+
+requests supports Basic Authentication and HTTP Digest Authentication by
+default. There are also a number of third-party libraries for authentication
+with:
+
+- `OAuth <https://requests-oauthlib.readthedocs.org/en/latest/>`_
+
+- `NTLM <https://github.com/requests/requests-ntlm>`_
+
+- `Kerberos <https://github.com/requests/requests-kerberos>`_
+
+The :mod:`requests_toolbelt.auth` provides extra authentication features in
+addition to those. It provides the following authentication classes:
+
+- :class:`requests_toolbelt.auth.guess.GuessAuth`
+
+- :class:`requests_toolbelt.auth.http_proxy_digest.HTTPProxyDigestAuth`
+
+- :class:`requests_toolbelt.auth.handler.AuthHandler`
+
+AuthHandler
+-----------
+
+The :class:`~requests_toolbelt.auth.handler.AuthHandler` is a way of using a
+single session with multiple websites that require authentication. If you know
+what websites require a certain kind of authentication and what your
+credentials are.
+
+Take for example a session that needs to authenticate to GitHub's API and
+GitLab's API, you would set up and use your
+:class:`~requests_toolbelt.auth.handler.AuthHandler` like so:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.auth.handler import AuthHandler
+
+ def gitlab_auth(request):
+ request.headers['PRIVATE-TOKEN'] = 'asecrettoken'
+
+ handler = AuthHandler({
+ 'https://api.github.com': ('sigmavirus24', 'apassword'),
+ 'https://gitlab.com': gitlab_auth,
+ })
+
+ session = requests.Session()
+ session.auth = handler
+ r = session.get('https://api.github.com/user')
+ # assert r.ok
+ r2 = session.get('https://gitlab.com/api/v3/projects')
+ # assert r2.ok
+
+.. note::
+
+ You **must** provide both the scheme and domain for authentication. The
+ :class:`~requests_toolbelt.auth.handler.AuthHandler` class will check both
+ the scheme and host to ensure your data is not accidentally exposed.
+
+.. autoclass:: requests_toolbelt.auth.handler.AuthHandler
+ :members:
+
+GuessAuth
+---------
+
+The :class:`~requests_toolbelt.auth.guess.GuessAuth` authentication class
+automatically detects whether to use basic auth or digest auth:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.auth import GuessAuth
+
+ requests.get('http://httpbin.org/basic-auth/user/passwd',
+ auth=GuessAuth('user', 'passwd'))
+ requests.get('http://httpbin.org/digest-auth/auth/user/passwd',
+ auth=GuessAuth('user', 'passwd'))
+
+Detection of the auth type is done via the ``WWW-Authenticate`` header sent by
+the server. This requires an additional request in case of basic auth, as
+usually basic auth is sent preemptively. If the server didn't explicitly
+require authentication, no credentials are sent.
+
+.. autoclass:: requests_toolbelt.auth.guess.GuessAuth
+
+
+GuessProxyAuth
+--------------
+
+The :class:`~requests_toolbelt.auth.guess.GuessProxyAuth` handler will
+automatically detect whether to use basic authentication or digest authentication
+when authenticating to the provided proxy.
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.auth.guess import GuessProxyAuth
+
+ proxies = {
+ "http": "http://PROXYSERVER:PROXYPORT",
+ "https": "http://PROXYSERVER:PROXYPORT",
+ }
+ requests.get('http://httpbin.org/basic-auth/user/passwd',
+ auth=GuessProxyAuth('user', 'passwd', 'proxyusr', 'proxypass'),
+ proxies=proxies)
+ requests.get('http://httpbin.org/digest-auth/auth/user/passwd',
+ auth=GuessProxyAuth('user', 'passwd', 'proxyusr', 'proxypass'),
+ proxies=proxies)
+
+Detection of the auth type is done via the ``Proxy-Authenticate`` header sent by
+the server. This requires an additional request in case of basic auth, as
+usually basic auth is sent preemptively. If the server didn't explicitly
+require authentication, no credentials are sent.
+
+.. autoclass:: requests_toolbelt.auth.guess.GuessProxyAuth
+
+HTTPProxyDigestAuth
+-------------------
+
+The ``HTTPProxyDigestAuth`` use digest authentication between the client and
+the proxy.
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.auth.http_proxy_digest import HTTPProxyDigestAuth
+
+
+ proxies = {
+ "http": "http://PROXYSERVER:PROXYPORT",
+ "https": "https://PROXYSERVER:PROXYPORT",
+ }
+ url = "https://toolbelt.readthedocs.org/"
+ auth = HTTPProxyDigestAuth("USERNAME", "PASSWORD")
+ requests.get(url, proxies=proxies, auth=auth)
+
+Program would raise error if the username or password is rejected by the proxy.
+
+.. autoclass:: requests_toolbelt.auth.http_proxy_digest.HTTPProxyDigestAuth
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..9a965e1
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,271 @@
+# -*- coding: utf-8 -*-
+#
+# requests_toolbelt documentation build configuration file, created by
+# sphinx-quickstart on Sun Jan 12 21:24:39 2014.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+import os
+import sys
+sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'requests_toolbelt'
+copyright = u'2015, Ian Cordasco, Cory Benfield'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+from requests_toolbelt import __version__ as version
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+if not on_rtd: # only import and set the theme if we're building docs locally
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#html_theme = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+#html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'requests_toolbelt-doc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'requests_toolbelt.tex', u'requests\\_toolbelt Documentation',
+ u'Ian Cordasco, Cory Benfield', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'requests_toolbelt', u'requests_toolbelt Documentation',
+ [u'Ian Cordasco, Cory Benfield'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'requests_toolbelt', u'requests_toolbelt Documentation',
+ u'Ian Cordasco, Cory Benfield', 'requests_toolbelt', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..55d66bf
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1,161 @@
+Contributing to this project
+============================
+
+Checklist
+---------
+
+#. All potential contributors must read the :ref:`code-of-conduct` and follow
+ it
+
+#. Fork the repository on `GitHub`_ or `GitLab`_
+
+#. Create a new branch, e.g., ``git checkout -b bug/12345``
+
+#. Fix the bug and add tests (if applicable [#]_, see :ref:`how-to-add-tests`)
+
+#. Run the tests (see :ref:`how-to-run-tests` below)
+
+#. Add documentation (as necessary) for your change
+
+#. Build the documentation to check for errors and formatting (see
+ :ref:`how-to-build-the-docs` below)
+
+#. Add yourself to the :file:`AUTHORS.rst` (unless you're already there)
+
+#. Commit it. Follow these rules in your commit message:
+
+ * Keep the subject line under 50 characters
+
+ * Use an imperative verb to start the commit
+
+ * Use an empty line between the subject and the message
+
+ * Describe the *why* in detail in the message portion of the commit
+
+ * Wrap the lines of the message at 72 characters
+
+ * Add the appropriate "Closes #12345" syntax to autoclose the issue it
+ fixed (if it closes an issue)
+
+ * See :ref:`example-commit-message` below
+
+#. Push it to your fork
+
+#. Create a request for us to merge your contribution
+
+After this last step, it is possible that we may leave feedback in the form of
+review comments. When addressing these comments, you can follow two
+strategies:
+
+* Amend/rebase your changes into an existing commit
+
+* Create a new commit with a different message [#]_ describing the changes in
+ that commit and push it to your branch
+
+This project is not opinionated about which approach you should prefer. We
+only ask that you are aware of the following:
+
+* Neither GitHub nor GitLab notifies us that you have pushed new changes. A
+ friendly ping is encouraged
+
+* If you continue to use the same branch that you created the request from,
+ both GitHub and GitLab will update the request on the website. You do
+ **not** need to create a new request for the new changes.
+
+
+.. _code-of-conduct:
+
+.. include:: ../CODE_OF_CONDUCT.rst
+
+.. _how-to-add-tests:
+
+How To Add Tests
+----------------
+
+We use `pytest`_ to run tests and to simplify how we write tests. If you're
+fixing a bug in an existing please find tests for that module or feature and
+add to them. Most tests live in the ``tests`` directory. If you're adding a
+new feature in a new submodule, please create a new module of test code. For
+example, if you're adding a submodule named ``foo`` then you would create
+``tests/test_foo.py`` which will contain the tests for the ``foo`` submodule.
+
+.. _how-to-run-tests:
+
+How To Run The Tests
+--------------------
+
+Run the tests in this project using `tox`_. Before you run the tests, ensure
+you have installed tox either using your system package manager (e.g., apt,
+yum, etc.), or your prefered python installer (e.g., pip).
+
+Then run the tests on at least Python 2.7 and Python 3.x, e.g.,
+
+.. code::
+
+ $ tox -e py27,py34
+
+Finally run one, or both, of the flake8 style enforcers, e.g.,
+
+.. code::
+
+ $ tox -e py27-flake8
+ # or
+ $ tox -e py34-flake8
+
+It is preferable if you run both to catch syntax errors that might occur in
+Python 2 or Python 3 (based on how familiar you are with the common subset of
+language from both).
+
+Tox will manage virtual environments and dependencies for you so it will be
+the only dependency you need to install to contribute to this project.
+
+.. _how-to-build-the-docs:
+
+How To Build The Documentation
+------------------------------
+
+To build the docs, you need to ensure tox is installed and then you may run
+
+.. code::
+
+ $ tox -e docs
+
+This will build the documentation into ``docs/_build/html``. If you then run
+
+.. code::
+
+ $ python2.7 -m SimpleHTTPServer
+ # or
+ $ python3.4 -m http.server
+
+from that directory, you can view the docs locally at http://localhost:8000/.
+
+.. _example-commit-message:
+
+Example Commit Message
+----------------------
+
+::
+
+ Allow users to use the frob when uploading data
+
+ When uploading data with FooBar, users may need to use the frob method
+ to ensure that pieces of data are not munged.
+
+ Closes #1234567
+
+Footnotes
+---------
+
+.. [#] You might not need tests if you're updating documentation, fixing a
+ typo, or updating a docstring. If you're fixing a bug, please add
+ tests.
+
+.. [#] If each commit has the same message, the reviewer may ask you to
+ squash your commits or may squash them for you and perform a manual
+ merge.
+
+.. _GitHub: https://github.com/sigmavirus24/requests-toolbelt
+.. _GitLab: https://gitlab.com/sigmavirus24/toolbelt
+.. _tox: https://tox.readthedocs.org/en/latest/
+.. _pytest: http://pytest.org/latest/
diff --git a/docs/deprecated.rst b/docs/deprecated.rst
new file mode 100644
index 0000000..4da83ed
--- /dev/null
+++ b/docs/deprecated.rst
@@ -0,0 +1,13 @@
+.. _deprecated:
+
+Deprecated Requests Utilities
+=============================
+
+Requests has `decided`_ to deprecate some utility functions in
+:mod:`requests.utils`. To ease users' lives, they've been moved to
+:mod:`requests_toolbelt.utils.deprecated`.
+
+.. automodule:: requests_toolbelt.utils.deprecated
+ :members:
+
+.. _decided: https://github.com/kennethreitz/requests/issues/2266
diff --git a/docs/downloadutils.rst b/docs/downloadutils.rst
new file mode 100644
index 0000000..bcd0f2f
--- /dev/null
+++ b/docs/downloadutils.rst
@@ -0,0 +1,16 @@
+.. _downloadutils:
+
+Utilities for Downloading Streaming Responses
+=============================================
+
+.. autofunction::
+ requests_toolbelt.downloadutils.stream.stream_response_to_file
+
+.. autofunction::
+ requests_toolbelt.downloadutils.tee.tee
+
+.. autofunction::
+ requests_toolbelt.downloadutils.tee.tee_to_bytearray
+
+.. autofunction::
+ requests_toolbelt.downloadutils.tee.tee_to_file
diff --git a/docs/dumputils.rst b/docs/dumputils.rst
new file mode 100644
index 0000000..052ec61
--- /dev/null
+++ b/docs/dumputils.rst
@@ -0,0 +1,17 @@
+.. _dumputils:
+
+Utilities for Dumping Information About Responses
+=================================================
+
+Occasionally, it is helpful to know almost exactly what data was sent to a
+server and what data was received. It can also be challenging at times to
+gather all of that data from requests because of all of the different places
+you may need to look to find it. In :mod:`requests_toolbelt.utils.dump` there
+are two functions that will return a :class:`bytearray` with the information
+retrieved from a response object.
+
+.. autofunction::
+ requests_toolbelt.utils.dump.dump_all
+
+.. autofunction::
+ requests_toolbelt.utils.dump.dump_response
diff --git a/docs/exceptions.rst b/docs/exceptions.rst
new file mode 100644
index 0000000..e10a244
--- /dev/null
+++ b/docs/exceptions.rst
@@ -0,0 +1,10 @@
+.. _exceptions:
+
+Custom Toolbelt Exceptions
+==========================
+
+Below are the exception classes used by the toolbelt to provide error details
+to the user of the toolbelt.
+
+.. automodule:: requests_toolbelt.exceptions
+ :members:
diff --git a/docs/formdata.rst b/docs/formdata.rst
new file mode 100644
index 0000000..0d48d27
--- /dev/null
+++ b/docs/formdata.rst
@@ -0,0 +1,7 @@
+.. _formdatautils:
+
+Utilities for Enhanced Form-Data Serialization
+==============================================
+
+.. autofunction::
+ requests_toolbelt.utils.formdata.urlencode
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..43d66e6
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,50 @@
+.. requests_toolbelt documentation master file, created by
+ sphinx-quickstart on Sun Jan 12 21:24:39 2014.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+requests toolbelt
+=================
+
+This is a collection of utilities that some users of python-requests might need
+but do not belong in requests proper. The library is actively maintained by
+members of the requests core development team, and so reflects the
+functionality most requested by users of the requests library.
+
+To get an overview of what the library contains, consult the :ref:`user <user>`
+documentation.
+
+Overview
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ user
+ contributing
+
+Full Documentation
+------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ adapters
+ authentication
+ deprecated
+ downloadutils
+ dumputils
+ formdata
+ exceptions
+ sessions
+ threading
+ uploading-data
+ user-agent
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..45ea43d
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\requests_toolbelt.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\requests_toolbelt.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/docs/sessions.rst b/docs/sessions.rst
new file mode 100644
index 0000000..924e6dc
--- /dev/null
+++ b/docs/sessions.rst
@@ -0,0 +1,24 @@
+.. _sessions:
+
+Specialized Sessions
+====================
+
+The toolbelt provides specialized session classes in the
+:mod:`requests_toolbelt.sessions` module.
+
+.. automodule:: requests_toolbelt.sessions
+ :members:
+
+
+BaseUrlSession
+--------------
+
+.. versionadded:: 0.7.0
+
+Many people have written Session subclasses that allow a "base URL" to be
+specified so all future requests need not specify the complete URL. To create
+one simplified and easy to configure version, we've added the
+:class:`requests_toolbelt.sessions.BaseUrlSession` object to the Toolbelt.
+
+.. autoclass:: requests_toolbelt.sessions.BaseUrlSession
+ :members:
diff --git a/docs/threading.rst b/docs/threading.rst
new file mode 100644
index 0000000..043b043
--- /dev/null
+++ b/docs/threading.rst
@@ -0,0 +1,170 @@
+.. _threading:
+
+Using requests with Threading
+=============================
+
+.. versionadded:: 0.4.0
+
+The toolbelt provides a simple API for using requests with threading.
+
+A requests Session is documented as threadsafe but there are still a couple
+corner cases where it isn't perfectly threadsafe. The best way to use a
+Session is to use one per thread.
+
+The implementation provided by the toolbelt is naïve. This means that we use
+one session per thread and we make no effort to synchronize attributes (e.g.,
+authentication, cookies, etc.). It also means that we make no attempt to
+direct a request to a session that has already handled a request to the same
+domain. In other words, if you're making requests to multiple domains, the
+toolbelt's Pool will not try to send requests to the same domain to the same
+thread.
+
+This module provides three classes:
+
+- :class:`~requests_toolbelt.threaded.pool.Pool`
+- :class:`~requests_toolbelt.threaded.pool.ThreadResponse`
+- :class:`~requests_toolbelt.threaded.pool.ThreadException`
+
+In 98% of the situations you'll want to just use a
+:class:`~requests_toolbelt.threaded.pool.Pool` and you'll treat a
+:class:`~requests_toolbelt.threaded.pool.ThreadResponse` as if it were a
+regular :class:`requests.Response`.
+
+Here's an example:
+
+.. code-block:: python
+
+ # This example assumes Python 3
+ import queue
+ from requests_toolbelt.threaded import pool
+
+ jobs = queue.Queue()
+ urls = [
+ # My list of URLs to get
+ ]
+
+ for url in urls:
+ queue.put({'method': 'GET', 'url': url})
+
+ p = pool.Pool(job_queue=q)
+ p.join_all()
+
+ for response in p.responses():
+ print('GET {0}. Returned {1}.'.format(response.request_kwargs['url'],
+ response.status_code))
+
+This is clearly a bit underwhelming. This is why there's a short-cut class
+method to create a :class:`~requests_toolbelt.threaded.pool.Pool` from a list
+of URLs.
+
+.. code-block:: python
+
+ from requests_toolbelt.threaded import pool
+
+ urls = [
+ # My list of URLs to get
+ ]
+
+ p = pool.Pool.from_urls(urls)
+ p.join_all()
+
+ for response in p.responses():
+ print('GET {0}. Returned {1}.'.format(response.request_kwargs['url'],
+ response.status_code))
+
+If one of the URLs in your list throws an exception, it will be accessible
+from the :meth:`~Pool.exceptions` generator.
+
+.. code-block:: python
+
+ from requests_toolbelt.threaded import pool
+
+ urls = [
+ # My list of URLs to get
+ ]
+
+ p = pool.Pool.from_urls(urls)
+ p.join_all()
+
+ for exc in p.exceptions():
+ print('GET {0}. Raised {1}.'.format(exc.request_kwargs['url'],
+ exc.message))
+
+If instead, you want to retry the exceptions that have been raised you can do
+the following:
+
+.. code-block:: python
+
+ from requests_toolbelt.threaded import pool
+
+ urls = [
+ # My list of URLs to get
+ ]
+
+ p = pool.Pool.from_urls(urls)
+ p.join_all()
+
+ new_pool = pool.Pool.from_exceptions(p.exceptions())
+ new_pool.join_all()
+
+Not all requests are advisable to retry without checking if they should be
+retried. You would normally check if you want to retry it.
+
+The :class:`~Pool` object takes 4 other keyword arguments:
+
+- ``initializer``
+
+ This is a callback that will initialize things on every session created. The
+ callback must return the session.
+
+- ``auth_generator``
+
+ This is a callback that is called *after* the initializer callback has
+ modified the session. This callback must also return the session.
+
+- ``num_processes``
+
+ By passing a positive integer that indicates how many threads to use. It is
+ ``None`` by default, and will use the result of
+ ``multiproccessing.cpu_count()``.
+
+- ``session``
+
+ You can pass an alternative constructor or any callable that returns a
+ :class:`requests.Sesssion` like object. It will not be passed any arguments
+ because a :class:`requests.Session` does not accept any arguments.
+
+Finally, if you don't want to worry about Queue or Pool management, you can
+try the following:
+
+.. code-block:: python
+
+ from requests_toolbelt import threaded
+
+ requests = [{
+ 'method': 'GET',
+ 'url': 'https://httpbin.org/get',
+ # ...
+ }, {
+ # ...
+ }, {
+ # ...
+ }]
+
+ responses_generator, exceptions_generator = threaded.map(requests)
+ for response in responses_generator:
+ # Do something
+
+API and Module Auto-Generated Documentation
+-------------------------------------------
+
+.. automodule:: requests_toolbelt.threaded
+
+.. autoclass:: requests_toolbelt.threaded.pool.Pool
+ :members:
+
+.. autoclass:: requests_toolbelt.threaded.pool.ThreadResponse
+ :members:
+
+.. autoclass:: requests_toolbelt.threaded.pool.ThreadException
+ :members:
diff --git a/docs/uploading-data.rst b/docs/uploading-data.rst
new file mode 100644
index 0000000..db926ab
--- /dev/null
+++ b/docs/uploading-data.rst
@@ -0,0 +1,172 @@
+.. _uploading-data:
+
+Uploading Data
+==============
+
+Streaming Multipart Data Encoder
+--------------------------------
+
+Requests has `support for multipart uploads`_, but the API means that using
+that functionality to build exactly the Multipart upload you want can be
+difficult or impossible. Additionally, when using Requests' Multipart upload
+functionality all the data must be read into memory before being sent to the
+server. In extreme cases, this can make it impossible to send a file as part of
+a ``multipart/form-data`` upload.
+
+The toolbelt contains a class that allows you to build multipart request bodies
+in exactly the format you need, and to avoid reading files into memory. An
+example of how to use it is like this:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.multipart.encoder import MultipartEncoder
+
+ m = MultipartEncoder(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
+ )
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+The :class:`~requests_toolbelt.multipart.encoder.MultipartEncoder` has the
+``.to_string()`` convenience method, as well. This method renders the
+multipart body into a string. This is useful when developing your code,
+allowing you to confirm that the multipart body has the form you expect before
+you send it on.
+
+The toolbelt also provides a way to monitor your streaming uploads with
+the :class:`~requests_toolbelt.multipart.encoder.MultipartEncoderMonitor`.
+
+.. autoclass:: requests_toolbelt.multipart.encoder.MultipartEncoder
+
+.. _support for multipart uploads: http://docs.python-requests.org/en/latest/user/quickstart/#post-a-multipart-encoded-file
+
+Monitoring Your Streaming Multipart Upload
+------------------------------------------
+
+If you need to stream your ``multipart/form-data`` upload then you're probably
+in the situation where it might take a while to upload the content. In these
+cases, it might make sense to be able to monitor the progress of the upload.
+For this reason, the toolbelt provides the
+:class:`~requests_toolbelt.multipart.encoder.MultipartEncoderMonitor`. The
+monitor wraps an instance of a
+:class:`~requests_toolbelt.multipart.encoder.MultipartEncoder` and is used
+exactly like the encoder. It provides a similar API with some additions:
+
+- The monitor accepts a function as a callback. The function is called every
+ time ``requests`` calls ``read`` on the monitor and passes in the monitor as
+ an argument.
+
+- The monitor tracks how many bytes have been read in the course of the
+ upload.
+
+You might use the monitor to create a progress bar for the upload. Here is `an
+example using clint`_ which displays the progress bar.
+
+To use the monitor you would follow a pattern like this:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.multipart import encoder
+
+ def my_callback(monitor):
+ # Your callback function
+ pass
+
+ e = encoder.MultipartEncoder(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
+ )
+ m = encoder.MultipartEncoderMonitor(e, my_callback)
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+If you have a very simple use case you can also do:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor
+
+ def my_callback(monitor):
+ # Your callback function
+ pass
+
+ m = MultipartEncoderMonitor.from_fields(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')},
+ callback=my_callback
+ )
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+.. autoclass:: requests_toolbelt.multipart.encoder.MultipartEncoderMonitor
+
+.. _an example using clint:
+ https://gitlab.com/sigmavirus24/toolbelt/blob/master/examples/monitor/progress_bar.py
+
+Streaming Data from a Generator
+-------------------------------
+
+There are cases where you, the user, have a generator of some large quantity
+of data and you already know the size of that data. If you pass the generator
+to ``requests`` via the ``data`` parameter, ``requests`` will assume that you
+want to upload the data in chunks and set a ``Transfer-Encoding`` header value
+of ``chunked``. Often times, this causes the server to behave poorly. If you
+want to avoid this, you can use the
+:class:`~requests.toolbelt.streaming_iterator.StreamingIterator`. You pass it
+the size of the data and the generator.
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.streaming_iterator import StreamingIterator
+
+ generator = some_function() # Create your generator
+ size = some_function_size() # Get your generator's size
+ content_type = content_type() # Get the content-type of the data
+
+ streamer = StreamingIterator(size, generator)
+ r = requests.post('https://httpbin.org/post', data=streamer,
+ headers={'Content-Type': content_type})
+
+The streamer will handle your generator for you and buffer the data before
+passing it to ``requests``.
+
+.. versionchanged:: 0.4.0
+
+ File-like objects can be passed instead of a generator.
+
+If, for example, you need to upload data being piped into standard in, you
+might otherwise do:
+
+.. code-block:: python
+
+ import requests
+ import sys
+
+ r = requests.post(url, data=sys.stdin)
+
+This would stream the data but would use a chunked transfer-encoding. If
+instead, you know the length of the data that is being sent to ``stdin`` and
+you want to prevent the data from being uploaded in chunks, you can use the
+:class:`~requests_toolbelt.streaming_iterator.StreamingIterator` to stream the
+contents of the file without relying on chunking.
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.streaming_iterator import StreamingIterator
+ import sys
+
+ stream = StreamingIterator(size, sys.stdin)
+ r = requests.post(url, data=stream,
+ headers={'Content-Type': content_type})
+
+.. autoclass:: requests_toolbelt.streaming_iterator.StreamingIterator
diff --git a/docs/user-agent.rst b/docs/user-agent.rst
new file mode 100644
index 0000000..c8e56e9
--- /dev/null
+++ b/docs/user-agent.rst
@@ -0,0 +1,94 @@
+.. _user-agent:
+
+User-Agent Constructor
+======================
+
+Having well-formed user-agent strings is important for the proper functioning
+of the web. Make server administators happy by generating yourself a nice
+user-agent string, just like Requests does! The output of the user-agent
+generator looks like this::
+
+ >>> import requests_toolbelt
+ >>> requests_toolbelt.user_agent('mypackage', '0.0.1')
+ 'mypackage/0.0.1 CPython/2.7.5 Darwin/13.0.0'
+
+The Python type and version, and the platform type and version, will accurately
+reflect the system that your program is running on. You can drop this easily
+into your program like this::
+
+ from requests_toolbelt import user_agent
+ from requests import Session
+
+ s = Session()
+ s.headers = {
+ 'User-Agent': user_agent('my_package', '0.0.1')
+ }
+
+ r = s.get('https://api.github.com/users')
+
+This will override the default Requests user-agent string for all of your HTTP
+requests, replacing it with your own.
+
+Adding Extra Information to Your User-Agent String
+--------------------------------------------------
+
+.. versionadded:: 0.5.0
+
+If you feel it necessary, you can also include versions for other things that
+your client is using. For example if you were building a package and wanted to
+include the package name and version number as well as the version of requests
+and requests-toolbelt you were using you could do the following:
+
+.. code-block:: python
+
+ import requests
+ import requests_toolbelt
+ from requests_toolbelt.utils import user_agent as ua
+
+ user_agent = ua.user_agent('mypackage', '0.0.1',
+ extras=[('requests', requests.__version__),
+ ('requests-toolbelt', requests_toolbelt.__version__)])
+
+ s = requests.Session()
+ s.headers['User-Agent'] = user_agent
+
+
+Your user agent will now look like::
+
+ mypackage/0.0.1 requests/2.7.0 requests-toolbelt/0.5.0 CPython/2.7.10 Darwin/13.0.0
+
+Selecting Only What You Want
+----------------------------
+
+.. versionadded:: 0.8.0
+
+While most people will find the ``user_agent`` function sufficient for their
+usage, others will want to control exactly what information is included in the
+User-Agent. For those people, the
+:class:`~requests_toolbelt.utils.user_agent.UserAgentBuilder` is the correct
+tool. This is the tool that the toolbelt uses inside of
+:func:`~requests_toolbelt.utils.user_agent.user_agent`. For example, let's say
+you *only* want your package, its versions, and some extra information, in
+that case you would do:
+
+.. code-block:: python
+
+ import requests
+ from requests_toolbelt.utils import user_agent as ua
+
+ s = requests.Session()
+ s.headers['User-Agent'] = ua.UserAgentBuilder(
+ 'mypackage', '0.0.1',
+ ).include_extras([
+ ('requests', requests.__version__),
+ ]).build()
+
+Your user agent will now look like::
+
+ mypackage/0.0.1 requests/2.7.0
+
+You can also optionally include the Python version information and System
+information the same way that our ``user_agent`` function does.
+
+.. autoclass:: requests_toolbelt.utils.user_agent.UserAgentBuilder
+ :members:
diff --git a/docs/user.rst b/docs/user.rst
new file mode 100644
index 0000000..9493d33
--- /dev/null
+++ b/docs/user.rst
@@ -0,0 +1,3 @@
+.. _user:
+
+.. include:: ../README.rst
diff --git a/requests_toolbelt.egg-info/PKG-INFO b/requests_toolbelt.egg-info/PKG-INFO
new file mode 100644
index 0000000..23e3aac
--- /dev/null
+++ b/requests_toolbelt.egg-info/PKG-INFO
@@ -0,0 +1,434 @@
+Metadata-Version: 1.1
+Name: requests-toolbelt
+Version: 0.8.0
+Summary: A utility belt for advanced users of python-requests
+Home-page: https://toolbelt.readthedocs.org
+Author: Ian Cordasco, Cory Benfield
+Author-email: graffatcolmingov@gmail.com
+License: Apache 2.0
+Description: requests toolbelt
+ =================
+
+ This is just a collection of utilities for `python-requests`_, but don't
+ really belong in ``requests`` proper. The minimum tested requests version is
+ ``2.1.0``. In reality, the toolbelt should work with ``2.0.1`` as well, but
+ some idiosyncracies prevent effective or sane testing on that version.
+
+ ``pip install requests-toolbelt`` to get started!
+
+
+ multipart/form-data Encoder
+ ---------------------------
+
+ The main attraction is a streaming multipart form-data object, ``MultipartEncoder``.
+ Its API looks like this:
+
+ .. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(
+ fields={'field0': 'value', 'field1': 'value',
+ 'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
+ )
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+ You can also use ``multipart/form-data`` encoding for requests that don't
+ require files:
+
+ .. code-block:: python
+
+ from requests_toolbelt import MultipartEncoder
+ import requests
+
+ m = MultipartEncoder(fields={'field0': 'value', 'field1': 'value'})
+
+ r = requests.post('http://httpbin.org/post', data=m,
+ headers={'Content-Type': m.content_type})
+
+
+ Or, you can just create the string and examine the data:
+
+ .. code-block:: python
+
+ # Assuming `m` is one of the above
+ m.to_string() # Always returns unicode
+
+
+ User-Agent constructor
+ ----------------------
+
+ You can easily construct a requests-style ``User-Agent`` string::
+
+ from requests_toolbelt import user_agent
+
+ headers = {
+ 'User-Agent': user_agent('my_package', '0.0.1')
+ }
+
+ r = requests.get('https://api.github.com/users', headers=headers)
+
+
+ SSLAdapter
+ ----------
+
+ The ``SSLAdapter`` was originally published on `Cory Benfield's blog`_.
+ This adapter allows the user to choose one of the SSL protocols made available
+ in Python's ``ssl`` module for outgoing HTTPS connections:
+
+ .. code-block:: python
+
+ from requests_toolbelt import SSLAdapter
+ import requests
+ import ssl
+
+ s = requests.Session()
+ s.mount('https://', SSLAdapter(ssl.PROTOCOL_TLSv1))
+
+ cookies/ForgetfulCookieJar
+ --------------------------
+
+ The ``ForgetfulCookieJar`` prevents a particular requests session from storing
+ cookies:
+
+ .. code-block:: python
+
+ from requests_toolbelt.cookies.forgetful import ForgetfulCookieJar
+
+ session = requests.Session()
+ session.cookies = ForgetfulCookieJar()
+
+ Known Issues
+ ------------
+
+ On Python 3.3.0 and 3.3.1, the standard library's ``http`` module will fail
+ when passing an instance of the ``MultipartEncoder``. This is fixed in later
+ minor releases of Python 3.3. Please consider upgrading to a later minor
+ version or Python 3.4. *There is absolutely nothing this library can do to
+ work around that bug.*
+
+ Contributing
+ ------------
+
+ Please read the `suggested workflow
+ <https://toolbelt.readthedocs.org/en/latest/contributing.html>`_ for
+ contributing to this project.
+
+ .. _Cory Benfield's blog: https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
+ .. _python-requests: https://github.com/kennethreitz/requests
+
+
+ History
+ =======
+
+ 0.8.0 -- 2017-05-20
+ -------------------
+
+ More information about this release can be found on the `0.8.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``UserAgentBuilder`` to provide more control over generated User-Agent
+ strings.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Include ``_validate_certificate`` in the lits of picked attributes on the
+ ``AppEngineAdapter``.
+ - Fix backwards incompatibility in ``get_encodings_from_content``
+
+ .. _0.8.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.8.0
+
+ 0.7.1 -- 2017-02-13
+ -------------------
+
+ More information about this release can be found on the `0.7.1 milestone`_.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Fixed monkey-patching for the AppEngineAdapter.
+
+ - Make it easier to disable certificate verification when monkey-patching
+ AppEngine.
+
+ - Handle ``multipart/form-data`` bodies without a trailing ``CRLF``.
+
+
+ .. links
+ .. _0.7.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestone/9
+
+ 0.7.0 -- 2016-07-21
+ -------------------
+
+ More information about this release can be found on the `0.7.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``BaseUrlSession`` to allow developers to have a session that has a
+ "Base" URL. See the documentation for more details and examples.
+
+ - Split the logic of ``stream_response_to_file`` into two separate functions:
+
+ * ``get_download_file_path`` to generate the file name from the Response.
+
+ * ``stream_response_to_file`` which will use ``get_download_file_path`` if
+ necessary
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Fixed the issue for people using *very* old versions of Requests where they
+ would see an ImportError from ``requests_toolbelt._compat`` when trying to
+ import ``connection``.
+
+
+ .. _0.7.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.7.0
+
+ 0.6.2 -- 2016-05-10
+ -------------------
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - When passing a timeout via Requests, it was not appropriately translated to
+ the timeout that the urllib3 code was expecting.
+
+ 0.6.1 -- 2016-05-05
+ -------------------
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Remove assertion about request URLs in the AppEngineAdapter.
+
+ - Prevent pip from installing requests 3.0.0 when that is released until we
+ are ready to handle it.
+
+ 0.6.0 -- 2016-01-27
+ -------------------
+
+ More information about this release can be found on the `0.6.0 milestone`_.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - Add ``AppEngineAdapter`` to support developers using Google's AppEngine
+ platform with Requests.
+
+ - Add ``GuessProxyAuth`` class to support guessing between Basic and Digest
+ Authentication for proxies.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Ensure that proxies use the correct TLS version when using the
+ ``SSLAdapter``.
+
+ - Fix an ``AttributeError`` when using the ``HTTPProxyDigestAuth`` class.
+
+ Miscellaneous
+ ~~~~~~~~~~~~~
+
+ - Drop testing support for Python 3.2. virtualenv and pip have stopped
+ supporting it meaning that it is harder to test for this with our CI
+ infrastructure. Moving forward we will make a best-effort attempt to
+ support 3.2 but will not test for it.
+
+
+ .. _0.6.0 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.6.0
+
+ 0.5.1 -- 2015-12-16
+ -------------------
+
+ More information about this release can be found on the `0.5.1 milestone`_.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Now papers over the differences in requests' ``super_len`` function from
+ versions prior to 2.9.0 and versions 2.9.0 and later.
+
+
+ .. _0.5.1 milestone:
+ https://github.com/sigmavirus24/requests-toolbelt/milestones/0.5.1
+
+ 0.5.0 -- 2015-11-24
+ -------------------
+
+ More information about this release can be found on the `milestone
+ <https://github.com/sigmavirus24/requests-toolbelt/issues?utf8=%E2%9C%93&q=is%3Aall+milestone%3A0.5+>`_
+ for 0.5.0.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - The ``tee`` submodule was added to ``requests_toolbelt.downloadutils``. It
+ allows you to iterate over the bytes of a response while also writing them
+ to a file. The ``tee.tee`` function, expects you to pass an open file
+ object, while ``tee.tee_to_file`` will use the provided file name to open
+ the file for you.
+
+ - Added a new parameter to ``requests_toolbelt.utils.user_agent`` that allows
+ the user to specify additional items.
+
+ - Added nested form-data helper,
+ ``requests_toolbelt.utils.formdata.urlencode``.
+
+ - Added the ``ForgetfulCookieJar`` to ``requests_toolbelt.cookies``.
+
+ - Added utilities for dumping the information about a request-response cycle
+ in ``requests_toolbelt.utils.dump``.
+
+ - Implemented the API described in the ``requests_toolbelt.threaded`` module
+ docstring, i.e., added ``requests_toolbelt.threaded.map`` as an available
+ function.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - Now papers over the API differences in versions of requests installed from
+ system packages versus versions of requests installed from PyPI.
+
+ - Allow string types for ``SourceAddressAdapter``.
+
+ 0.4.0 -- 2015-04-03
+ -------------------
+
+ For more information about this release, please see `milestone 0.4.0
+ <https://github.com/sigmavirus24/requests-toolbelt/issues?q=milestone%3A0.4>`_
+ on the project's page.
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - A naive implemenation of a thread pool is now included in the toolbelt. See
+ the docs in ``docs/threading.rst`` or on `Read The Docs
+ <https://toolbelt.readthedocs.org>`_.
+
+ - The ``StreamingIterator`` now accepts files (such as ``sys.stdin``) without
+ a specific length and will properly stream them.
+
+ - The ``MultipartEncoder`` now accepts exactly the same format of fields as
+ requests' ``files`` parameter does. In other words, you can now also pass in
+ extra headers to add to a part in the body. You can also now specify a
+ custom ``Content-Type`` for a part.
+
+ - An implementation of HTTP Digest Authentication for Proxies is now included.
+
+ - A transport adapter that allows a user to specify a specific Certificate
+ Fingerprint is now included in the toolbelt.
+
+ - A transport adapter that simplifies how users specify socket options is now
+ included.
+
+ - A transport adapter that simplifies how users can specify TCP Keep-Alive
+ options is now included in the toolbelt.
+
+ - Deprecated functions from ``requests.utils`` are now included and
+ maintained.
+
+ - An authentication tool that allows users to specify how to authenticate to
+ several different domains at once is now included.
+
+ - A function to save streamed responses to disk by analyzing the
+ ``Content-Disposition`` header is now included in the toolbelt.
+
+ Fixed Bugs
+ ~~~~~~~~~~
+
+ - The ``MultipartEncoder`` will now allow users to upload files larger than
+ 4GB on 32-bit systems.
+
+ - The ``MultipartEncoder`` will now accept empty unicode strings for form
+ values.
+
+ 0.3.1 -- 2014-06-23
+ -------------------
+
+ - Fix the fact that 0.3.0 bundle did not include the ``StreamingIterator``
+
+ 0.3.0 -- 2014-05-21
+ -------------------
+
+ Bug Fixes
+ ~~~~~~~~~
+
+ - Complete rewrite of ``MultipartEncoder`` fixes bug where bytes were lost in
+ uploads
+
+ New Features
+ ~~~~~~~~~~~~
+
+ - ``MultipartDecoder`` to accept ``multipart/form-data`` response bodies and
+ parse them into an easy to use object.
+
+ - ``SourceAddressAdapter`` to allow users to choose a local address to bind
+ connections to.
+
+ - ``GuessAuth`` which accepts a username and password and uses the
+ ``WWW-Authenticate`` header to determine how to authenticate against a
+ server.
+
+ - ``MultipartEncoderMonitor`` wraps an instance of the ``MultipartEncoder``
+ and keeps track of how many bytes were read and will call the provided
+ callback.
+
+ - ``StreamingIterator`` will wrap an iterator and stream the upload instead of
+ chunk it, provided you also provide the length of the content you wish to
+ upload.
+
+ 0.2.0 -- 2014-02-24
+ -------------------
+
+ - Add ability to tell ``MultipartEncoder`` which encoding to use. By default
+ it uses 'utf-8'.
+
+ - Fix #10 - allow users to install with pip
+
+ - Fix #9 - Fix ``MultipartEncoder#to_string`` so that it properly handles file
+ objects as fields
+
+ 0.1.2 -- 2014-01-19
+ -------------------
+
+ - At some point during development we broke how we handle normal file objects.
+ Thanks to @konomae this is now fixed.
+
+ 0.1.1 -- 2014-01-19
+ -------------------
+
+ - Handle ``io.BytesIO``-like objects better
+
+ 0.1.0 -- 2014-01-18
+ -------------------
+
+ - Add initial implementation of the streaming ``MultipartEncoder``
+
+ - Add initial implementation of the ``user_agent`` function
+
+ - Add the ``SSLAdapter``
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: CPython
diff --git a/requests_toolbelt.egg-info/SOURCES.txt b/requests_toolbelt.egg-info/SOURCES.txt
new file mode 100644
index 0000000..fed460e
--- /dev/null
+++ b/requests_toolbelt.egg-info/SOURCES.txt
@@ -0,0 +1,99 @@
+AUTHORS.rst
+CODE_OF_CONDUCT.rst
+HISTORY.rst
+LICENSE
+MANIFEST.in
+README.rst
+dev-requirements.txt
+setup.cfg
+setup.py
+tox.ini
+docs/Makefile
+docs/adapters.rst
+docs/authentication.rst
+docs/conf.py
+docs/contributing.rst
+docs/deprecated.rst
+docs/downloadutils.rst
+docs/dumputils.rst
+docs/exceptions.rst
+docs/formdata.rst
+docs/index.rst
+docs/make.bat
+docs/sessions.rst
+docs/threading.rst
+docs/uploading-data.rst
+docs/user-agent.rst
+docs/user.rst
+requests_toolbelt/__init__.py
+requests_toolbelt/_compat.py
+requests_toolbelt/exceptions.py
+requests_toolbelt/sessions.py
+requests_toolbelt/streaming_iterator.py
+requests_toolbelt.egg-info/PKG-INFO
+requests_toolbelt.egg-info/SOURCES.txt
+requests_toolbelt.egg-info/dependency_links.txt
+requests_toolbelt.egg-info/requires.txt
+requests_toolbelt.egg-info/top_level.txt
+requests_toolbelt/adapters/__init__.py
+requests_toolbelt/adapters/appengine.py
+requests_toolbelt/adapters/fingerprint.py
+requests_toolbelt/adapters/host_header_ssl.py
+requests_toolbelt/adapters/socket_options.py
+requests_toolbelt/adapters/source.py
+requests_toolbelt/adapters/ssl.py
+requests_toolbelt/auth/__init__.py
+requests_toolbelt/auth/_digest_auth_compat.py
+requests_toolbelt/auth/guess.py
+requests_toolbelt/auth/handler.py
+requests_toolbelt/auth/http_proxy_digest.py
+requests_toolbelt/cookies/__init__.py
+requests_toolbelt/cookies/forgetful.py
+requests_toolbelt/downloadutils/__init__.py
+requests_toolbelt/downloadutils/stream.py
+requests_toolbelt/downloadutils/tee.py
+requests_toolbelt/multipart/__init__.py
+requests_toolbelt/multipart/decoder.py
+requests_toolbelt/multipart/encoder.py
+requests_toolbelt/threaded/__init__.py
+requests_toolbelt/threaded/pool.py
+requests_toolbelt/threaded/thread.py
+requests_toolbelt/utils/__init__.py
+requests_toolbelt/utils/deprecated.py
+requests_toolbelt/utils/dump.py
+requests_toolbelt/utils/formdata.py
+requests_toolbelt/utils/user_agent.py
+tests/__init__.py
+tests/conftest.py
+tests/test_appengine_adapter.py
+tests/test_auth.py
+tests/test_auth_handler.py
+tests/test_downloadutils.py
+tests/test_dump.py
+tests/test_fingerprintadapter.py
+tests/test_forgetfulcookiejar.py
+tests/test_formdata.py
+tests/test_host_header_ssl_adapter.py
+tests/test_multipart_decoder.py
+tests/test_multipart_encoder.py
+tests/test_multipart_monitor.py
+tests/test_proxy_digest_auth.py
+tests/test_sessions.py
+tests/test_socket_options_adapter.py
+tests/test_source_adapter.py
+tests/test_ssladapter.py
+tests/test_streaming_iterator.py
+tests/test_user_agent.py
+tests/cassettes/http2bin_cookies.json
+tests/cassettes/http2bin_fingerprint.json
+tests/cassettes/httpbin_guess_auth_basic.json
+tests/cassettes/httpbin_guess_auth_digest.json
+tests/cassettes/httpbin_guess_auth_none.json
+tests/cassettes/klevas_vu_lt_ssl3.json
+tests/cassettes/redirect_request_for_dump_all.json
+tests/cassettes/simple_get_request.json
+tests/cassettes/stream_response_to_file.json
+tests/threaded/__init__.py
+tests/threaded/test_api.py
+tests/threaded/test_pool.py
+tests/threaded/test_thread.py \ No newline at end of file
diff --git a/requests_toolbelt.egg-info/dependency_links.txt b/requests_toolbelt.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/requests_toolbelt.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/requests_toolbelt.egg-info/requires.txt b/requests_toolbelt.egg-info/requires.txt
new file mode 100644
index 0000000..3702438
--- /dev/null
+++ b/requests_toolbelt.egg-info/requires.txt
@@ -0,0 +1 @@
+requests>=2.0.1,<3.0.0
diff --git a/requests_toolbelt.egg-info/top_level.txt b/requests_toolbelt.egg-info/top_level.txt
new file mode 100644
index 0000000..976bdfe
--- /dev/null
+++ b/requests_toolbelt.egg-info/top_level.txt
@@ -0,0 +1 @@
+requests_toolbelt
diff --git a/requests_toolbelt/__init__.py b/requests_toolbelt/__init__.py
new file mode 100644
index 0000000..5536246
--- /dev/null
+++ b/requests_toolbelt/__init__.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""
+requests-toolbelt
+=================
+
+See http://toolbelt.rtfd.org/ for documentation
+
+:copyright: (c) 2014 by Ian Cordasco and Cory Benfield
+:license: Apache v2.0, see LICENSE for more details
+"""
+
+from .adapters import SSLAdapter, SourceAddressAdapter
+from .auth.guess import GuessAuth
+from .multipart import (
+ MultipartEncoder, MultipartEncoderMonitor, MultipartDecoder,
+ ImproperBodyPartContentException, NonMultipartContentTypeException
+ )
+from .streaming_iterator import StreamingIterator
+from .utils.user_agent import user_agent
+
+__title__ = 'requests-toolbelt'
+__authors__ = 'Ian Cordasco, Cory Benfield'
+__license__ = 'Apache v2.0'
+__copyright__ = 'Copyright 2014 Ian Cordasco, Cory Benfield'
+__version__ = '0.8.0'
+__version_info__ = tuple(int(i) for i in __version__.split('.'))
+
+__all__ = [
+ 'GuessAuth', 'MultipartEncoder', 'MultipartEncoderMonitor',
+ 'MultipartDecoder', 'SSLAdapter', 'SourceAddressAdapter',
+ 'StreamingIterator', 'user_agent', 'ImproperBodyPartContentException',
+ 'NonMultipartContentTypeException', '__title__', '__authors__',
+ '__license__', '__copyright__', '__version__', '__version_info__',
+]
diff --git a/requests_toolbelt/_compat.py b/requests_toolbelt/_compat.py
new file mode 100644
index 0000000..f0a7706
--- /dev/null
+++ b/requests_toolbelt/_compat.py
@@ -0,0 +1,310 @@
+"""Private module full of compatibility hacks.
+
+Primarily this is for downstream redistributions of requests that unvendor
+urllib3 without providing a shim.
+
+.. warning::
+
+ This module is private. If you use it, and something breaks, you were
+ warned
+"""
+from collections import Mapping, MutableMapping
+import sys
+
+import requests
+
+try:
+ from requests.packages.urllib3 import fields
+ from requests.packages.urllib3 import filepost
+ from requests.packages.urllib3 import poolmanager
+except ImportError:
+ from urllib3 import fields
+ from urllib3 import filepost
+ from urllib3 import poolmanager
+
+try:
+ from requests.packages.urllib3.connection import HTTPConnection
+ from requests.packages.urllib3 import connection
+except ImportError:
+ try:
+ from urllib3.connection import HTTPConnection
+ from urllib3 import connection
+ except ImportError:
+ HTTPConnection = None
+ connection = None
+
+
+if requests.__build__ < 0x020300:
+ timeout = None
+else:
+ try:
+ from requests.packages.urllib3.util import timeout
+ except ImportError:
+ from urllib3.util import timeout
+
+if requests.__build__ < 0x021000:
+ gaecontrib = None
+else:
+ try:
+ from requests.packages.urllib3.contrib import appengine as gaecontrib
+ except ImportError:
+ from urllib3.contrib import appengine as gaecontrib
+
+PY3 = sys.version_info > (3, 0)
+
+if PY3:
+ import queue
+ from urllib.parse import urlencode, urljoin
+else:
+ import Queue as queue
+ from urllib import urlencode
+ from urlparse import urljoin
+
+try:
+ basestring = basestring
+except NameError:
+ basestring = (str, bytes)
+
+
+class HTTPHeaderDict(MutableMapping):
+ """
+ :param headers:
+ An iterable of field-value pairs. Must not contain multiple field names
+ when compared case-insensitively.
+
+ :param kwargs:
+ Additional field-value pairs to pass in to ``dict.update``.
+
+ A ``dict`` like container for storing HTTP Headers.
+
+ Field names are stored and compared case-insensitively in compliance with
+ RFC 7230. Iteration provides the first case-sensitive key seen for each
+ case-insensitive pair.
+
+ Using ``__setitem__`` syntax overwrites fields that compare equal
+ case-insensitively in order to maintain ``dict``'s api. For fields that
+ compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
+ in a loop.
+
+ If multiple fields that are equal case-insensitively are passed to the
+ constructor or ``.update``, the behavior is undefined and some will be
+ lost.
+
+ >>> headers = HTTPHeaderDict()
+ >>> headers.add('Set-Cookie', 'foo=bar')
+ >>> headers.add('set-cookie', 'baz=quxx')
+ >>> headers['content-length'] = '7'
+ >>> headers['SET-cookie']
+ 'foo=bar, baz=quxx'
+ >>> headers['Content-Length']
+ '7'
+ """
+
+ def __init__(self, headers=None, **kwargs):
+ super(HTTPHeaderDict, self).__init__()
+ self._container = {}
+ if headers is not None:
+ if isinstance(headers, HTTPHeaderDict):
+ self._copy_from(headers)
+ else:
+ self.extend(headers)
+ if kwargs:
+ self.extend(kwargs)
+
+ def __setitem__(self, key, val):
+ self._container[key.lower()] = (key, val)
+ return self._container[key.lower()]
+
+ def __getitem__(self, key):
+ val = self._container[key.lower()]
+ return ', '.join(val[1:])
+
+ def __delitem__(self, key):
+ del self._container[key.lower()]
+
+ def __contains__(self, key):
+ return key.lower() in self._container
+
+ def __eq__(self, other):
+ if not isinstance(other, Mapping) and not hasattr(other, 'keys'):
+ return False
+ if not isinstance(other, type(self)):
+ other = type(self)(other)
+ return (dict((k.lower(), v) for k, v in self.itermerged()) ==
+ dict((k.lower(), v) for k, v in other.itermerged()))
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ if not PY3: # Python 2
+ iterkeys = MutableMapping.iterkeys
+ itervalues = MutableMapping.itervalues
+
+ __marker = object()
+
+ def __len__(self):
+ return len(self._container)
+
+ def __iter__(self):
+ # Only provide the originally cased names
+ for vals in self._container.values():
+ yield vals[0]
+
+ def pop(self, key, default=__marker):
+ """D.pop(k[,d]) -> v, remove specified key and return its value.
+
+ If key is not found, d is returned if given, otherwise KeyError is
+ raised.
+ """
+ # Using the MutableMapping function directly fails due to the private
+ # marker.
+ # Using ordinary dict.pop would expose the internal structures.
+ # So let's reinvent the wheel.
+ try:
+ value = self[key]
+ except KeyError:
+ if default is self.__marker:
+ raise
+ return default
+ else:
+ del self[key]
+ return value
+
+ def discard(self, key):
+ try:
+ del self[key]
+ except KeyError:
+ pass
+
+ def add(self, key, val):
+ """Adds a (name, value) pair, doesn't overwrite the value if it already
+ exists.
+
+ >>> headers = HTTPHeaderDict(foo='bar')
+ >>> headers.add('Foo', 'baz')
+ >>> headers['foo']
+ 'bar, baz'
+ """
+ key_lower = key.lower()
+ new_vals = key, val
+ # Keep the common case aka no item present as fast as possible
+ vals = self._container.setdefault(key_lower, new_vals)
+ if new_vals is not vals:
+ # new_vals was not inserted, as there was a previous one
+ if isinstance(vals, list):
+ # If already several items got inserted, we have a list
+ vals.append(val)
+ else:
+ # vals should be a tuple then, i.e. only one item so far
+ # Need to convert the tuple to list for further extension
+ self._container[key_lower] = [vals[0], vals[1], val]
+
+ def extend(self, *args, **kwargs):
+ """Generic import function for any type of header-like object.
+ Adapted version of MutableMapping.update in order to insert items
+ with self.add instead of self.__setitem__
+ """
+ if len(args) > 1:
+ raise TypeError("extend() takes at most 1 positional "
+ "arguments ({} given)".format(len(args)))
+ other = args[0] if len(args) >= 1 else ()
+
+ if isinstance(other, HTTPHeaderDict):
+ for key, val in other.iteritems():
+ self.add(key, val)
+ elif isinstance(other, Mapping):
+ for key in other:
+ self.add(key, other[key])
+ elif hasattr(other, "keys"):
+ for key in other.keys():
+ self.add(key, other[key])
+ else:
+ for key, value in other:
+ self.add(key, value)
+
+ for key, value in kwargs.items():
+ self.add(key, value)
+
+ def getlist(self, key):
+ """Returns a list of all the values for the named field. Returns an
+ empty list if the key doesn't exist."""
+ try:
+ vals = self._container[key.lower()]
+ except KeyError:
+ return []
+ else:
+ if isinstance(vals, tuple):
+ return [vals[1]]
+ else:
+ return vals[1:]
+
+ # Backwards compatibility for httplib
+ getheaders = getlist
+ getallmatchingheaders = getlist
+ iget = getlist
+
+ def __repr__(self):
+ return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
+
+ def _copy_from(self, other):
+ for key in other:
+ val = other.getlist(key)
+ if isinstance(val, list):
+ # Don't need to convert tuples
+ val = list(val)
+ self._container[key.lower()] = [key] + val
+
+ def copy(self):
+ clone = type(self)()
+ clone._copy_from(self)
+ return clone
+
+ def iteritems(self):
+ """Iterate over all header lines, including duplicate ones."""
+ for key in self:
+ vals = self._container[key.lower()]
+ for val in vals[1:]:
+ yield vals[0], val
+
+ def itermerged(self):
+ """Iterate over all headers, merging duplicate ones together."""
+ for key in self:
+ val = self._container[key.lower()]
+ yield val[0], ', '.join(val[1:])
+
+ def items(self):
+ return list(self.iteritems())
+
+ @classmethod
+ def from_httplib(cls, message): # Python 2
+ """Read headers from a Python 2 httplib message object."""
+ # python2.7 does not expose a proper API for exporting multiheaders
+ # efficiently. This function re-reads raw lines from the message
+ # object and extracts the multiheaders properly.
+ headers = []
+
+ for line in message.headers:
+ if line.startswith((' ', '\t')):
+ key, value = headers[-1]
+ headers[-1] = (key, value + '\r\n' + line.rstrip())
+ continue
+
+ key, value = line.split(':', 1)
+ headers.append((key, value.strip()))
+
+ return cls(headers)
+
+
+__all__ = (
+ 'basestring',
+ 'connection',
+ 'fields',
+ 'filepost',
+ 'poolmanager',
+ 'timeout',
+ 'HTTPHeaderDict',
+ 'queue',
+ 'urlencode',
+ 'gaecontrib',
+ 'urljoin',
+)
diff --git a/requests_toolbelt/adapters/__init__.py b/requests_toolbelt/adapters/__init__.py
new file mode 100644
index 0000000..ddfe4fe
--- /dev/null
+++ b/requests_toolbelt/adapters/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+requests-toolbelt.adapters
+==========================
+
+See http://toolbelt.rtfd.org/ for documentation
+
+:copyright: (c) 2014 by Ian Cordasco and Cory Benfield
+:license: Apache v2.0, see LICENSE for more details
+"""
+
+from .ssl import SSLAdapter
+from .source import SourceAddressAdapter
+
+__all__ = ['SSLAdapter', 'SourceAddressAdapter']
diff --git a/requests_toolbelt/adapters/appengine.py b/requests_toolbelt/adapters/appengine.py
new file mode 100644
index 0000000..2af2bbb
--- /dev/null
+++ b/requests_toolbelt/adapters/appengine.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+"""The App Engine Transport Adapter for requests.
+
+.. versionadded:: 0.6.0
+
+This requires a version of requests >= 2.10.0 and Python 2.
+
+There are two ways to use this library:
+
+#. If you're using requests directly, you can use code like:
+
+ .. code-block:: python
+
+ >>> import requests
+ >>> import ssl
+ >>> import requests.packages.urllib3.contrib.appengine as ul_appengine
+ >>> from requests_toolbelt.adapters import appengine
+ >>> s = requests.Session()
+ >>> if ul_appengine.is_appengine_sandbox():
+ ... s.mount('http://', appengine.AppEngineAdapter())
+ ... s.mount('https://', appengine.AppEngineAdapter())
+
+#. If you depend on external libraries which use requests, you can use code
+ like:
+
+ .. code-block:: python
+
+ >>> from requests_toolbelt.adapters import appengine
+ >>> appengine.monkeypatch()
+
+which will ensure all requests.Session objects use AppEngineAdapter properly.
+
+You are also able to :ref:`disable certificate validation <insecure_appengine>`
+when monkey-patching.
+"""
+import requests
+import warnings
+from requests import adapters
+from requests import sessions
+
+from .. import exceptions as exc
+from .._compat import gaecontrib
+from .._compat import timeout
+
+
+class AppEngineMROHack(adapters.HTTPAdapter):
+ """Resolves infinite recursion when monkeypatching.
+
+ This works by injecting itself as the base class of both the
+ :class:`AppEngineAdapter` and Requests' default HTTPAdapter, which needs to
+ be done because default HTTPAdapter's MRO is recompiled when we
+ monkeypatch, at which point this class becomes HTTPAdapter's base class.
+ In addition, we use an instantiation flag to avoid infinite recursion.
+ """
+ _initialized = False
+
+ def __init__(self, *args, **kwargs):
+ if not self._initialized:
+ self._initialized = True
+ super(AppEngineMROHack, self).__init__(*args, **kwargs)
+
+
+class AppEngineAdapter(AppEngineMROHack, adapters.HTTPAdapter):
+ """The transport adapter for Requests to use urllib3's GAE support.
+
+ Implements Requests's HTTPAdapter API.
+
+ When deploying to Google's App Engine service, some of Requests'
+ functionality is broken. There is underlying support for GAE in urllib3.
+ This functionality, however, is opt-in and needs to be enabled explicitly
+ for Requests to be able to use it.
+ """
+
+ __attrs__ = adapters.HTTPAdapter.__attrs__ + ['_validate_certificate']
+
+ def __init__(self, validate_certificate=True, *args, **kwargs):
+ _check_version()
+ self._validate_certificate = validate_certificate
+ super(AppEngineAdapter, self).__init__(*args, **kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = _AppEnginePoolManager(self._validate_certificate)
+
+
+class InsecureAppEngineAdapter(AppEngineAdapter):
+ """An always-insecure GAE adapter for Requests.
+
+ This is a variant of the the transport adapter for Requests to use
+ urllib3's GAE support that does not validate certificates. Use with
+ caution!
+
+ .. note::
+ The ``validate_certificate`` keyword argument will not be honored here
+ and is not part of the signature because we always force it to
+ ``False``.
+
+ See :class:`AppEngineAdapter` for further details.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if kwargs.pop("validate_certificate", False):
+ warnings.warn("Certificate validation cannot be specified on the "
+ "InsecureAppEngineAdapter, but was present. This "
+ "will be ignored and certificate validation will "
+ "remain off.", exc.IgnoringGAECertificateValidation)
+
+ super(InsecureAppEngineAdapter, self).__init__(
+ validate_certificate=False, *args, **kwargs)
+
+
+class _AppEnginePoolManager(object):
+ """Implements urllib3's PoolManager API expected by requests.
+
+ While a real PoolManager map hostnames to reusable Connections,
+ AppEngine has no concept of a reusable connection to a host.
+ So instead, this class constructs a small Connection per request,
+ that is returned to the Adapter and used to access the URL.
+ """
+
+ def __init__(self, validate_certificate=True):
+ self.appengine_manager = gaecontrib.AppEngineManager(
+ validate_certificate=validate_certificate)
+
+ def connection_from_url(self, url):
+ return _AppEngineConnection(self.appengine_manager, url)
+
+ def clear(self):
+ pass
+
+
+class _AppEngineConnection(object):
+ """Implements urllib3's HTTPConnectionPool API's urlopen().
+
+ This Connection's urlopen() is called with a host-relative path,
+ so in order to properly support opening the URL, we need to store
+ the full URL when this Connection is constructed from the PoolManager.
+
+ This code wraps AppEngineManager.urlopen(), which exposes a different
+ API than in the original urllib3 urlopen(), and thus needs this adapter.
+ """
+
+ def __init__(self, appengine_manager, url):
+ self.appengine_manager = appengine_manager
+ self.url = url
+
+ def urlopen(self, method, url, body=None, headers=None, retries=None,
+ redirect=True, assert_same_host=True,
+ timeout=timeout.Timeout.DEFAULT_TIMEOUT,
+ pool_timeout=None, release_conn=None, **response_kw):
+ # This function's url argument is a host-relative URL,
+ # but the AppEngineManager expects an absolute URL.
+ # So we saved out the self.url when the AppEngineConnection
+ # was constructed, which we then can use down below instead.
+
+ # We once tried to verify our assumptions here, but sometimes the
+ # passed-in URL differs on url fragments, or "http://a.com" vs "/".
+
+ # urllib3's App Engine adapter only uses Timeout.total, not read or
+ # connect.
+ if not timeout.total:
+ timeout.total = timeout._read or timeout._connect
+
+ # Jump through the hoops necessary to call AppEngineManager's API.
+ return self.appengine_manager.urlopen(
+ method,
+ self.url,
+ body=body,
+ headers=headers,
+ retries=retries,
+ redirect=redirect,
+ timeout=timeout,
+ **response_kw)
+
+
+def monkeypatch(validate_certificate=True):
+ """Sets up all Sessions to use AppEngineAdapter by default.
+
+ If you don't want to deal with configuring your own Sessions,
+ or if you use libraries that use requests directly (ie requests.post),
+ then you may prefer to monkeypatch and auto-configure all Sessions.
+
+ .. warning: :
+
+ If ``validate_certificate`` is ``False``, certification validation will
+ effectively be disabled for all requests.
+ """
+ _check_version()
+ # HACK: We should consider modifying urllib3 to support this cleanly,
+ # so that we can set a module-level variable in the sessions module,
+ # instead of overriding an imported HTTPAdapter as is done here.
+ adapter = AppEngineAdapter
+ if not validate_certificate:
+ adapter = InsecureAppEngineAdapter
+
+ sessions.HTTPAdapter = adapter
+ adapters.HTTPAdapter = adapter
+
+
+def _check_version():
+ if gaecontrib is None:
+ raise exc.VersionMismatchError(
+ "The toolbelt requires at least Requests 2.10.0 to be "
+ "installed. Version {0} was found instead.".format(
+ requests.__version__
+ )
+ )
diff --git a/requests_toolbelt/adapters/fingerprint.py b/requests_toolbelt/adapters/fingerprint.py
new file mode 100644
index 0000000..6645d34
--- /dev/null
+++ b/requests_toolbelt/adapters/fingerprint.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+"""Submodule containing the implementation for the FingerprintAdapter.
+
+This file contains an implementation of a Transport Adapter that validates
+the fingerprints of SSL certificates presented upon connection.
+"""
+from requests.adapters import HTTPAdapter
+
+from .._compat import poolmanager
+
+
+class FingerprintAdapter(HTTPAdapter):
+ """
+ A HTTPS Adapter for Python Requests that verifies certificate fingerprints,
+ instead of certificate hostnames.
+
+ Example usage:
+
+ .. code-block:: python
+
+ import requests
+ import ssl
+ from requests_toolbelt.adapters.fingerprint import FingerprintAdapter
+
+ twitter_fingerprint = '...'
+ s = requests.Session()
+ s.mount(
+ 'https://twitter.com',
+ FingerprintAdapter(twitter_fingerprint)
+ )
+
+ The fingerprint should be provided as a hexadecimal string, optionally
+ containing colons.
+ """
+
+ __attrs__ = HTTPAdapter.__attrs__ + ['fingerprint']
+
+ def __init__(self, fingerprint, **kwargs):
+ self.fingerprint = fingerprint
+
+ super(FingerprintAdapter, self).__init__(**kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = poolmanager.PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ assert_fingerprint=self.fingerprint)
diff --git a/requests_toolbelt/adapters/host_header_ssl.py b/requests_toolbelt/adapters/host_header_ssl.py
new file mode 100644
index 0000000..f34ed1a
--- /dev/null
+++ b/requests_toolbelt/adapters/host_header_ssl.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+requests_toolbelt.adapters.host_header_ssl
+==========================================
+
+This file contains an implementation of the HostHeaderSSLAdapter.
+"""
+
+from requests.adapters import HTTPAdapter
+
+
+class HostHeaderSSLAdapter(HTTPAdapter):
+ """
+ A HTTPS Adapter for Python Requests that sets the hostname for certificate
+ verification based on the Host header.
+
+ This allows requesting the IP address directly via HTTPS without getting
+ a "hostname doesn't match" exception.
+
+ Example usage:
+
+ >>> s.mount('https://', HostHeaderSSLAdapter())
+ >>> s.get("https://93.184.216.34", headers={"Host": "example.org"})
+
+ """
+
+ def send(self, request, **kwargs):
+ # HTTP headers are case-insensitive (RFC 7230)
+ host_header = None
+ for header in request.headers:
+ if header.lower() == "host":
+ host_header = request.headers[header]
+ break
+
+ connection_pool_kwargs = self.poolmanager.connection_pool_kw
+
+ if host_header:
+ connection_pool_kwargs["assert_hostname"] = host_header
+ elif "assert_hostname" in connection_pool_kwargs:
+ # an assert_hostname from a previous request may have been left
+ connection_pool_kwargs.pop("assert_hostname", None)
+
+ return super(HostHeaderSSLAdapter, self).send(request, **kwargs)
diff --git a/requests_toolbelt/adapters/socket_options.py b/requests_toolbelt/adapters/socket_options.py
new file mode 100644
index 0000000..962fe15
--- /dev/null
+++ b/requests_toolbelt/adapters/socket_options.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+"""The implementation of the SocketOptionsAdapter."""
+import socket
+import warnings
+
+import requests
+from requests import adapters
+
+from .._compat import connection
+from .._compat import poolmanager
+from .. import exceptions as exc
+
+
+class SocketOptionsAdapter(adapters.HTTPAdapter):
+ """An adapter for requests that allows users to specify socket options.
+
+ Since version 2.4.0 of requests, it is possible to specify a custom list
+ of socket options that need to be set before establishing the connection.
+
+ Example usage::
+
+ >>> import socket
+ >>> import requests
+ >>> from requests_toolbelt.adapters import socket_options
+ >>> s = requests.Session()
+ >>> opts = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)]
+ >>> adapter = socket_options.SocketOptionsAdapter(socket_options=opts)
+ >>> s.mount('http://', adapter)
+
+ You can also take advantage of the list of default options on this class
+ to keep using the original options in addition to your custom options. In
+ that case, ``opts`` might look like::
+
+ >>> opts = socket_options.SocketOptionsAdapter.default_options + opts
+
+ """
+
+ if connection is not None:
+ default_options = getattr(
+ connection.HTTPConnection,
+ 'default_socket_options',
+ [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
+ )
+ else:
+ default_options = []
+ warnings.warn(exc.RequestsVersionTooOld,
+ "This version of Requests is only compatible with a "
+ "version of urllib3 which is too old to support "
+ "setting options on a socket. This adapter is "
+ "functionally useless.")
+
+ def __init__(self, **kwargs):
+ self.socket_options = kwargs.pop('socket_options',
+ self.default_options)
+
+ super(SocketOptionsAdapter, self).__init__(**kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ if requests.__build__ >= 0x020400:
+ # NOTE(Ian): Perhaps we should raise a warning
+ self.poolmanager = poolmanager.PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ socket_options=self.socket_options
+ )
+ else:
+ super(SocketOptionsAdapter, self).init_poolmanager(
+ connections, maxsize, block
+ )
+
+
+class TCPKeepAliveAdapter(SocketOptionsAdapter):
+ """An adapter for requests that turns on TCP Keep-Alive by default.
+
+ The adapter sets 4 socket options:
+
+ - ``SOL_SOCKET`` ``SO_KEEPALIVE`` - This turns on TCP Keep-Alive
+ - ``IPPROTO_TCP`` ``TCP_KEEPINTVL`` 20 - Sets the keep alive interval
+ - ``IPPROTO_TCP`` ``TCP_KEEPCNT`` 5 - Sets the number of keep alive probes
+ - ``IPPROTO_TCP`` ``TCP_KEEPIDLE`` 60 - Sets the keep alive time if the
+ socket library has the ``TCP_KEEPIDLE`` constant
+
+ The latter three can be overridden by keyword arguments (respectively):
+
+ - ``idle``
+ - ``interval``
+ - ``count``
+
+ You can use this adapter like so::
+
+ >>> from requests_toolbelt.adapters import socket_options
+ >>> tcp = socket_options.TCPKeepAliveAdapter(idle=120, interval=10)
+ >>> s = requests.Session()
+ >>> s.mount('http://', tcp)
+
+ """
+
+ def __init__(self, **kwargs):
+ socket_options = kwargs.pop('socket_options',
+ SocketOptionsAdapter.default_options)
+ idle = kwargs.pop('idle', 60)
+ interval = kwargs.pop('interval', 20)
+ count = kwargs.pop('count', 5)
+ socket_options = socket_options + [
+ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, count),
+ ]
+
+ # NOTE(Ian): Apparently OSX does not have this constant defined, so we
+ # set it conditionally.
+ if getattr(socket, 'TCP_KEEPIDLE', None) is not None:
+ socket_options += [(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idle)]
+
+ super(TCPKeepAliveAdapter, self).__init__(
+ socket_options=socket_options, **kwargs
+ )
diff --git a/requests_toolbelt/adapters/source.py b/requests_toolbelt/adapters/source.py
new file mode 100644
index 0000000..82f65e3
--- /dev/null
+++ b/requests_toolbelt/adapters/source.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+"""
+requests_toolbelt.source_adapter
+================================
+
+This file contains an implementation of the SourceAddressAdapter originally
+demonstrated on the Requests GitHub page.
+"""
+from requests.adapters import HTTPAdapter
+
+from .._compat import poolmanager, basestring
+
+
+class SourceAddressAdapter(HTTPAdapter):
+ """
+ A Source Address Adapter for Python Requests that enables you to choose the
+ local address to bind to. This allows you to send your HTTP requests from a
+ specific interface and IP address.
+
+ Two address formats are accepted. The first is a string: this will set the
+ local IP address to the address given in the string, and will also choose a
+ semi-random high port for the local port number.
+
+ The second is a two-tuple of the form (ip address, port): for example,
+ ``('10.10.10.10', 8999)``. This will set the local IP address to the first
+ element, and the local port to the second element. If ``0`` is used as the
+ port number, a semi-random high port will be selected.
+
+ .. warning:: Setting an explicit local port can have negative interactions
+ with connection-pooling in Requests: in particular, it risks
+ the possibility of getting "Address in use" errors. The
+ string-only argument is generally preferred to the tuple-form.
+
+ Example usage:
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.adapters.source import SourceAddressAdapter
+
+ s = requests.Session()
+ s.mount('http://', SourceAddressAdapter('10.10.10.10'))
+ s.mount('https://', SourceAddressAdapter(('10.10.10.10', 8999))
+ """
+ def __init__(self, source_address, **kwargs):
+ if isinstance(source_address, basestring):
+ self.source_address = (source_address, 0)
+ elif isinstance(source_address, tuple):
+ self.source_address = source_address
+ else:
+ raise TypeError(
+ "source_address must be IP address string or (ip, port) tuple"
+ )
+
+ super(SourceAddressAdapter, self).__init__(**kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = poolmanager.PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ source_address=self.source_address)
+
+ def proxy_manager_for(self, *args, **kwargs):
+ kwargs['source_address'] = self.source_address
+ return super(SourceAddressAdapter, self).proxy_manager_for(
+ *args, **kwargs)
diff --git a/requests_toolbelt/adapters/ssl.py b/requests_toolbelt/adapters/ssl.py
new file mode 100644
index 0000000..c4a76ae
--- /dev/null
+++ b/requests_toolbelt/adapters/ssl.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+"""
+
+requests_toolbelt.ssl_adapter
+=============================
+
+This file contains an implementation of the SSLAdapter originally demonstrated
+in this blog post:
+https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
+
+"""
+import requests
+
+from requests.adapters import HTTPAdapter
+
+from .._compat import poolmanager
+
+
+class SSLAdapter(HTTPAdapter):
+ """
+ A HTTPS Adapter for Python Requests that allows the choice of the SSL/TLS
+ version negotiated by Requests. This can be used either to enforce the
+ choice of high-security TLS versions (where supported), or to work around
+ misbehaving servers that fail to correctly negotiate the default TLS
+ version being offered.
+
+ Example usage:
+
+ >>> import requests
+ >>> import ssl
+ >>> from requests_toolbelt import SSLAdapter
+ >>> s = requests.Session()
+ >>> s.mount('https://', SSLAdapter(ssl.PROTOCOL_TLSv1))
+
+ You can replace the chosen protocol with any that are available in the
+ default Python SSL module. All subsequent requests that match the adapter
+ prefix will use the chosen SSL version instead of the default.
+
+ This adapter will also attempt to change the SSL/TLS version negotiated by
+ Requests when using a proxy. However, this may not always be possible:
+ prior to Requests v2.4.0 the adapter did not have access to the proxy setup
+ code. In earlier versions of Requests, this adapter will not function
+ properly when used with proxies.
+ """
+
+ __attrs__ = HTTPAdapter.__attrs__ + ['ssl_version']
+
+ def __init__(self, ssl_version=None, **kwargs):
+ self.ssl_version = ssl_version
+
+ super(SSLAdapter, self).__init__(**kwargs)
+
+ def init_poolmanager(self, connections, maxsize, block=False):
+ self.poolmanager = poolmanager.PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ ssl_version=self.ssl_version)
+
+ if requests.__build__ >= 0x020400:
+ # Earlier versions of requests either don't have this method or, worse,
+ # don't allow passing arbitrary keyword arguments. As a result, only
+ # conditionally define this method.
+ def proxy_manager_for(self, *args, **kwargs):
+ kwargs['ssl_version'] = self.ssl_version
+ return super(SSLAdapter, self).proxy_manager_for(*args, **kwargs)
diff --git a/requests_toolbelt/auth/__init__.py b/requests_toolbelt/auth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requests_toolbelt/auth/__init__.py
diff --git a/requests_toolbelt/auth/_digest_auth_compat.py b/requests_toolbelt/auth/_digest_auth_compat.py
new file mode 100644
index 0000000..285a6a7
--- /dev/null
+++ b/requests_toolbelt/auth/_digest_auth_compat.py
@@ -0,0 +1,29 @@
+"""Provide a compatibility layer for requests.auth.HTTPDigestAuth."""
+import requests
+
+
+class _ThreadingDescriptor(object):
+ def __init__(self, prop, default):
+ self.prop = prop
+ self.default = default
+
+ def __get__(self, obj, objtype=None):
+ return getattr(obj._thread_local, self.prop, self.default)
+
+ def __set__(self, obj, value):
+ setattr(obj._thread_local, self.prop, value)
+
+
+class _HTTPDigestAuth(requests.auth.HTTPDigestAuth):
+ init = _ThreadingDescriptor('init', True)
+ last_nonce = _ThreadingDescriptor('last_nonce', '')
+ nonce_count = _ThreadingDescriptor('nonce_count', 0)
+ chal = _ThreadingDescriptor('chal', {})
+ pos = _ThreadingDescriptor('pos', None)
+ num_401_calls = _ThreadingDescriptor('num_401_calls', 1)
+
+
+if requests.__build__ < 0x020800:
+ HTTPDigestAuth = requests.auth.HTTPDigestAuth
+else:
+ HTTPDigestAuth = _HTTPDigestAuth
diff --git a/requests_toolbelt/auth/guess.py b/requests_toolbelt/auth/guess.py
new file mode 100644
index 0000000..ba6de50
--- /dev/null
+++ b/requests_toolbelt/auth/guess.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+"""The module containing the code for GuessAuth."""
+from requests import auth
+from requests import cookies
+
+from . import _digest_auth_compat as auth_compat, http_proxy_digest
+
+
+class GuessAuth(auth.AuthBase):
+ """Guesses the auth type by the WWW-Authentication header."""
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+ self.auth = None
+ self.pos = None
+
+ def _handle_basic_auth_401(self, r, kwargs):
+ if self.pos is not None:
+ r.request.body.seek(self.pos)
+
+ # Consume content and release the original connection
+ # to allow our new request to reuse the same one.
+ r.content
+ r.raw.release_conn()
+ prep = r.request.copy()
+ if not hasattr(prep, '_cookies'):
+ prep._cookies = cookies.RequestsCookieJar()
+ cookies.extract_cookies_to_jar(prep._cookies, r.request, r.raw)
+ prep.prepare_cookies(prep._cookies)
+
+ self.auth = auth.HTTPBasicAuth(self.username, self.password)
+ prep = self.auth(prep)
+ _r = r.connection.send(prep, **kwargs)
+ _r.history.append(r)
+ _r.request = prep
+
+ return _r
+
+ def _handle_digest_auth_401(self, r, kwargs):
+ self.auth = auth_compat.HTTPDigestAuth(self.username, self.password)
+ try:
+ self.auth.init_per_thread_state()
+ except AttributeError:
+ # If we're not on requests 2.8.0+ this method does not exist and
+ # is not relevant.
+ pass
+
+ # Check that the attr exists because much older versions of requests
+ # set this attribute lazily. For example:
+ # https://github.com/kennethreitz/requests/blob/33735480f77891754304e7f13e3cdf83aaaa76aa/requests/auth.py#L59
+ if (hasattr(self.auth, 'num_401_calls') and
+ self.auth.num_401_calls is None):
+ self.auth.num_401_calls = 1
+ # Digest auth would resend the request by itself. We can take a
+ # shortcut here.
+ return self.auth.handle_401(r, **kwargs)
+
+ def handle_401(self, r, **kwargs):
+ """Resends a request with auth headers, if needed."""
+
+ www_authenticate = r.headers.get('www-authenticate', '').lower()
+
+ if 'basic' in www_authenticate:
+ return self._handle_basic_auth_401(r, kwargs)
+
+ if 'digest' in www_authenticate:
+ return self._handle_digest_auth_401(r, kwargs)
+
+ def __call__(self, request):
+ if self.auth is not None:
+ return self.auth(request)
+
+ try:
+ self.pos = request.body.tell()
+ except AttributeError:
+ pass
+
+ request.register_hook('response', self.handle_401)
+ return request
+
+
+class GuessProxyAuth(GuessAuth):
+ """
+ Guesses the auth type by WWW-Authentication and Proxy-Authentication
+ headers
+ """
+ def __init__(self, username=None, password=None,
+ proxy_username=None, proxy_password=None):
+ super(GuessProxyAuth, self).__init__(username, password)
+ self.proxy_username = proxy_username
+ self.proxy_password = proxy_password
+ self.proxy_auth = None
+
+ def _handle_basic_auth_407(self, r, kwargs):
+ if self.pos is not None:
+ r.request.body.seek(self.pos)
+
+ r.content
+ r.raw.release_conn()
+ prep = r.request.copy()
+ if not hasattr(prep, '_cookies'):
+ prep._cookies = cookies.RequestsCookieJar()
+ cookies.extract_cookies_to_jar(prep._cookies, r.request, r.raw)
+ prep.prepare_cookies(prep._cookies)
+
+ self.proxy_auth = auth.HTTPProxyAuth(self.proxy_username,
+ self.proxy_password)
+ prep = self.proxy_auth(prep)
+ _r = r.connection.send(prep, **kwargs)
+ _r.history.append(r)
+ _r.request = prep
+
+ return _r
+
+ def _handle_digest_auth_407(self, r, kwargs):
+ self.proxy_auth = http_proxy_digest.HTTPProxyDigestAuth(
+ username=self.proxy_username,
+ password=self.proxy_password)
+
+ try:
+ self.auth.init_per_thread_state()
+ except AttributeError:
+ pass
+
+ return self.proxy_auth.handle_407(r, **kwargs)
+
+ def handle_407(self, r, **kwargs):
+ proxy_authenticate = r.headers.get('Proxy-Authenticate', '').lower()
+
+ if 'basic' in proxy_authenticate:
+ return self._handle_basic_auth_407(r, kwargs)
+
+ if 'digest' in proxy_authenticate:
+ return self._handle_digest_auth_407(r, kwargs)
+
+ def __call__(self, request):
+ if self.proxy_auth is not None:
+ request = self.proxy_auth(request)
+
+ try:
+ self.pos = request.body.tell()
+ except AttributeError:
+ pass
+
+ request.register_hook('response', self.handle_407)
+ return super(GuessProxyAuth, self).__call__(request)
diff --git a/requests_toolbelt/auth/handler.py b/requests_toolbelt/auth/handler.py
new file mode 100644
index 0000000..7729c11
--- /dev/null
+++ b/requests_toolbelt/auth/handler.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+"""
+
+requests_toolbelt.auth.handler
+==============================
+
+This holds all of the implementation details of the Authentication Handler.
+
+"""
+
+from requests.auth import AuthBase, HTTPBasicAuth
+from requests.compat import urlparse, urlunparse
+
+
+class AuthHandler(AuthBase):
+
+ """
+
+ The ``AuthHandler`` object takes a dictionary of domains paired with
+ authentication strategies and will use this to determine which credentials
+ to use when making a request. For example, you could do the following:
+
+ .. code-block:: python
+
+ from requests import HTTPDigestAuth
+ from requests_toolbelt.auth.handler import AuthHandler
+
+ import requests
+
+ auth = AuthHandler({
+ 'https://api.github.com': ('sigmavirus24', 'fakepassword'),
+ 'https://example.com': HTTPDigestAuth('username', 'password')
+ })
+
+ r = requests.get('https://api.github.com/user', auth=auth)
+ # => <Response [200]>
+ r = requests.get('https://example.com/some/path', auth=auth)
+ # => <Response [200]>
+
+ s = requests.Session()
+ s.auth = auth
+ r = s.get('https://api.github.com/user')
+ # => <Response [200]>
+
+ .. warning::
+
+ :class:`requests.auth.HTTPDigestAuth` is not yet thread-safe. If you
+ use :class:`AuthHandler` across multiple threads you should
+ instantiate a new AuthHandler for each thread with a new
+ HTTPDigestAuth instance for each thread.
+
+ """
+
+ def __init__(self, strategies):
+ self.strategies = dict(strategies)
+ self._make_uniform()
+
+ def __call__(self, request):
+ auth = self.get_strategy_for(request.url)
+ return auth(request)
+
+ def __repr__(self):
+ return '<AuthHandler({0!r})>'.format(self.strategies)
+
+ def _make_uniform(self):
+ existing_strategies = list(self.strategies.items())
+ self.strategies = {}
+
+ for (k, v) in existing_strategies:
+ self.add_strategy(k, v)
+
+ @staticmethod
+ def _key_from_url(url):
+ parsed = urlparse(url)
+ return urlunparse((parsed.scheme.lower(),
+ parsed.netloc.lower(),
+ '', '', '', ''))
+
+ def add_strategy(self, domain, strategy):
+ """Add a new domain and authentication strategy.
+
+ :param str domain: The domain you wish to match against. For example:
+ ``'https://api.github.com'``
+ :param str strategy: The authentication strategy you wish to use for
+ that domain. For example: ``('username', 'password')`` or
+ ``requests.HTTPDigestAuth('username', 'password')``
+
+ .. code-block:: python
+
+ a = AuthHandler({})
+ a.add_strategy('https://api.github.com', ('username', 'password'))
+
+ """
+ # Turn tuples into Basic Authentication objects
+ if isinstance(strategy, tuple):
+ strategy = HTTPBasicAuth(*strategy)
+
+ key = self._key_from_url(domain)
+ self.strategies[key] = strategy
+
+ def get_strategy_for(self, url):
+ """Retrieve the authentication strategy for a specified URL.
+
+ :param str url: The full URL you will be making a request against. For
+ example, ``'https://api.github.com/user'``
+ :returns: Callable that adds authentication to a request.
+
+ .. code-block:: python
+
+ import requests
+ a = AuthHandler({'example.com', ('foo', 'bar')})
+ strategy = a.get_strategy_for('http://example.com/example')
+ assert isinstance(strategy, requests.auth.HTTPBasicAuth)
+
+ """
+ key = self._key_from_url(url)
+ return self.strategies.get(key, NullAuthStrategy())
+
+ def remove_strategy(self, domain):
+ """Remove the domain and strategy from the collection of strategies.
+
+ :param str domain: The domain you wish remove. For example,
+ ``'https://api.github.com'``.
+
+ .. code-block:: python
+
+ a = AuthHandler({'example.com', ('foo', 'bar')})
+ a.remove_strategy('example.com')
+ assert a.strategies == {}
+
+ """
+ key = self._key_from_url(domain)
+ if key in self.strategies:
+ del self.strategies[key]
+
+
+class NullAuthStrategy(AuthBase):
+ def __repr__(self):
+ return '<NullAuthStrategy>'
+
+ def __call__(self, r):
+ return r
diff --git a/requests_toolbelt/auth/http_proxy_digest.py b/requests_toolbelt/auth/http_proxy_digest.py
new file mode 100644
index 0000000..d414380
--- /dev/null
+++ b/requests_toolbelt/auth/http_proxy_digest.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+"""The module containing HTTPProxyDigestAuth."""
+import re
+
+from requests import cookies, utils
+
+from . import _digest_auth_compat as auth
+
+
+class HTTPProxyDigestAuth(auth.HTTPDigestAuth):
+ """HTTP digest authentication between proxy
+
+ :param stale_rejects: The number of rejects indicate that:
+ the client may wish to simply retry the request
+ with a new encrypted response, without reprompting the user for a
+ new username and password. i.e., retry build_digest_header
+ :type stale_rejects: int
+ """
+ _pat = re.compile(r'digest ', flags=re.IGNORECASE)
+
+ def __init__(self, *args, **kwargs):
+ super(HTTPProxyDigestAuth, self).__init__(*args, **kwargs)
+ self.stale_rejects = 0
+
+ self.init_per_thread_state()
+
+ @property
+ def stale_rejects(self):
+ thread_local = getattr(self, '_thread_local', None)
+ if thread_local is None:
+ return self._stale_rejects
+ return thread_local.stale_rejects
+
+ @stale_rejects.setter
+ def stale_rejects(self, value):
+ thread_local = getattr(self, '_thread_local', None)
+ if thread_local is None:
+ self._stale_rejects = value
+ else:
+ thread_local.stale_rejects = value
+
+ def init_per_thread_state(self):
+ try:
+ super(HTTPProxyDigestAuth, self).init_per_thread_state()
+ except AttributeError:
+ # If we're not on requests 2.8.0+ this method does not exist
+ pass
+
+ def handle_407(self, r, **kwargs):
+ """Handle HTTP 407 only once, otherwise give up
+
+ :param r: current response
+ :returns: responses, along with the new response
+ """
+ if r.status_code == 407 and self.stale_rejects < 2:
+ s_auth = r.headers.get("proxy-authenticate")
+ if s_auth is None:
+ raise IOError(
+ "proxy server violated RFC 7235:"
+ "407 response MUST contain header proxy-authenticate")
+ elif not self._pat.match(s_auth):
+ return r
+
+ self.chal = utils.parse_dict_header(
+ self._pat.sub('', s_auth, count=1))
+
+ # if we present the user/passwd and still get rejected
+ # http://tools.ietf.org/html/rfc2617#section-3.2.1
+ if ('Proxy-Authorization' in r.request.headers and
+ 'stale' in self.chal):
+ if self.chal['stale'].lower() == 'true': # try again
+ self.stale_rejects += 1
+ # wrong user/passwd
+ elif self.chal['stale'].lower() == 'false':
+ raise IOError("User or password is invalid")
+
+ # Consume content and release the original connection
+ # to allow our new request to reuse the same one.
+ r.content
+ r.close()
+ prep = r.request.copy()
+ cookies.extract_cookies_to_jar(prep._cookies, r.request, r.raw)
+ prep.prepare_cookies(prep._cookies)
+
+ prep.headers['Proxy-Authorization'] = self.build_digest_header(
+ prep.method, prep.url)
+ _r = r.connection.send(prep, **kwargs)
+ _r.history.append(r)
+ _r.request = prep
+
+ return _r
+ else: # give up authenticate
+ return r
+
+ def __call__(self, r):
+ self.init_per_thread_state()
+ # if we have nonce, then just use it, otherwise server will tell us
+ if self.last_nonce:
+ r.headers['Proxy-Authorization'] = self.build_digest_header(
+ r.method, r.url
+ )
+ r.register_hook('response', self.handle_407)
+ return r
diff --git a/requests_toolbelt/cookies/__init__.py b/requests_toolbelt/cookies/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requests_toolbelt/cookies/__init__.py
diff --git a/requests_toolbelt/cookies/forgetful.py b/requests_toolbelt/cookies/forgetful.py
new file mode 100644
index 0000000..3320363
--- /dev/null
+++ b/requests_toolbelt/cookies/forgetful.py
@@ -0,0 +1,7 @@
+"""The module containing the code for ForgetfulCookieJar."""
+from requests.cookies import RequestsCookieJar
+
+
+class ForgetfulCookieJar(RequestsCookieJar):
+ def set_cookie(self, *args, **kwargs):
+ return
diff --git a/requests_toolbelt/downloadutils/__init__.py b/requests_toolbelt/downloadutils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requests_toolbelt/downloadutils/__init__.py
diff --git a/requests_toolbelt/downloadutils/stream.py b/requests_toolbelt/downloadutils/stream.py
new file mode 100644
index 0000000..eed60a7
--- /dev/null
+++ b/requests_toolbelt/downloadutils/stream.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+"""Utilities for dealing with streamed requests."""
+import collections
+import os.path
+import re
+
+from .. import exceptions as exc
+
+# Regular expressions stolen from werkzeug/http.py
+# cd2c97bb0a076da2322f11adce0b2731f9193396 L62-L64
+_QUOTED_STRING_RE = r'"[^"\\]*(?:\\.[^"\\]*)*"'
+_OPTION_HEADER_PIECE_RE = re.compile(
+ r';\s*(%s|[^\s;=]+)\s*(?:=\s*(%s|[^;]+))?\s*' % (_QUOTED_STRING_RE,
+ _QUOTED_STRING_RE)
+)
+_DEFAULT_CHUNKSIZE = 512
+
+
+def _get_filename(content_disposition):
+ for match in _OPTION_HEADER_PIECE_RE.finditer(content_disposition):
+ k, v = match.groups()
+ if k == 'filename':
+ # ignore any directory paths in the filename
+ return os.path.split(v)[1]
+ return None
+
+
+def get_download_file_path(response, path):
+ """
+ Given a response and a path, return a file path for a download.
+
+ If a ``path`` parameter is a directory, this function will parse the
+ ``Content-Disposition`` header on the response to determine the name of the
+ file as reported by the server, and return a file path in the specified
+ directory.
+
+ If ``path`` is empty or None, this function will return a path relative
+ to the process' current working directory.
+
+ If path is a full file path, return it.
+
+ :param response: A Response object from requests
+ :type response: requests.models.Response
+ :param str path: Directory or file path.
+ :returns: full file path to download as
+ :rtype: str
+ :raises: :class:`requests_toolbelt.exceptions.StreamingError`
+ """
+ path_is_dir = path and os.path.isdir(path)
+
+ if path and not path_is_dir:
+ # fully qualified file path
+ filepath = path
+ else:
+ response_filename = _get_filename(
+ response.headers.get('content-disposition', '')
+ )
+ if not response_filename:
+ raise exc.StreamingError('No filename given to stream response to')
+
+ if path_is_dir:
+ # directory to download to
+ filepath = os.path.join(path, response_filename)
+ else:
+ # fallback to downloading to current working directory
+ filepath = response_filename
+
+ return filepath
+
+
+def stream_response_to_file(response, path=None, chunksize=_DEFAULT_CHUNKSIZE):
+ """Stream a response body to the specified file.
+
+ Either use the ``path`` provided or use the name provided in the
+ ``Content-Disposition`` header.
+
+ .. warning::
+
+ If you pass this function an open file-like object as the ``path``
+ parameter, the function will not close that file for you.
+
+ .. warning::
+
+ This function will not automatically close the response object
+ passed in as the ``response`` parameter.
+
+ If a ``path`` parameter is a directory, this function will parse the
+ ``Content-Disposition`` header on the response to determine the name of the
+ file as reported by the server, and return a file path in the specified
+ directory. If no ``path`` parameter is supplied, this function will default
+ to the process' current working directory.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt import exceptions
+ from requests_toolbelt.downloadutils import stream
+
+ r = requests.get(url, stream=True)
+ try:
+ filename = stream.stream_response_to_file(r)
+ except exceptions.StreamingError as e:
+ # The toolbelt could not find the filename in the
+ # Content-Disposition
+ print(e.message)
+
+ You can also specify the filename as a string. This will be passed to
+ the built-in :func:`open` and we will read the content into the file.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.downloadutils import stream
+
+ r = requests.get(url, stream=True)
+ filename = stream.stream_response_to_file(r, path='myfile')
+
+ If the calculated download file path already exists, this function will
+ raise a StreamingError.
+
+ Instead, if you want to manage the file object yourself, you need to
+ provide either a :class:`io.BytesIO` object or a file opened with the
+ `'b'` flag. See the two examples below for more details.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.downloadutils import stream
+
+ with open('myfile', 'wb') as fd:
+ r = requests.get(url, stream=True)
+ filename = stream.stream_response_to_file(r, path=fd)
+
+ print('{0} saved to {1}'.format(url, filename))
+
+ .. code-block:: python
+
+ import io
+ import requests
+ from requests_toolbelt.downloadutils import stream
+
+ b = io.BytesIO()
+ r = requests.get(url, stream=True)
+ filename = stream.stream_response_to_file(r, path=b)
+ assert filename is None
+
+ :param response: A Response object from requests
+ :type response: requests.models.Response
+ :param path: *(optional)*, Either a string with the path to the location
+ to save the response content, or a file-like object expecting bytes.
+ :type path: :class:`str`, or object with a :meth:`write`
+ :param int chunksize: (optional), Size of chunk to attempt to stream
+ (default 512B).
+ :returns: The name of the file, if one can be determined, else None
+ :rtype: str
+ :raises: :class:`requests_toolbelt.exceptions.StreamingError`
+ """
+ pre_opened = False
+ fd = None
+ filename = None
+ if path and isinstance(getattr(path, 'write', None), collections.Callable):
+ pre_opened = True
+ fd = path
+ filename = getattr(fd, 'name', None)
+ else:
+ filename = get_download_file_path(response, path)
+ if os.path.exists(filename):
+ raise exc.StreamingError("File already exists: %s" % filename)
+ fd = open(filename, 'wb')
+
+ for chunk in response.iter_content(chunk_size=chunksize):
+ fd.write(chunk)
+
+ if not pre_opened:
+ fd.close()
+
+ return filename
diff --git a/requests_toolbelt/downloadutils/tee.py b/requests_toolbelt/downloadutils/tee.py
new file mode 100644
index 0000000..ecc7d0c
--- /dev/null
+++ b/requests_toolbelt/downloadutils/tee.py
@@ -0,0 +1,123 @@
+"""Tee function implementations."""
+import io
+
+_DEFAULT_CHUNKSIZE = 65536
+
+__all__ = ['tee', 'tee_to_file', 'tee_to_bytearray']
+
+
+def _tee(response, callback, chunksize, decode_content):
+ for chunk in response.raw.stream(amt=chunksize,
+ decode_content=decode_content):
+ callback(chunk)
+ yield chunk
+
+
+def tee(response, fileobject, chunksize=_DEFAULT_CHUNKSIZE,
+ decode_content=None):
+ """Stream the response both to the generator and a file.
+
+ This will stream the response body while writing the bytes to
+ ``fileobject``.
+
+ Example usage:
+
+ .. code-block:: python
+
+ resp = requests.get(url, stream=True)
+ with open('save_file', 'wb') as save_file:
+ for chunk in tee(resp, save_file):
+ # do stuff with chunk
+
+ .. code-block:: python
+
+ import io
+
+ resp = requests.get(url, stream=True)
+ fileobject = io.BytesIO()
+
+ for chunk in tee(resp, fileobject):
+ # do stuff with chunk
+
+ :param response: Response from requests.
+ :type response: requests.Response
+ :param fileobject: Writable file-like object.
+ :type fileobject: file, io.BytesIO
+ :param int chunksize: (optional), Size of chunk to attempt to stream.
+ :param bool decode_content: (optional), If True, this will decode the
+ compressed content of the response.
+ :raises: TypeError if the fileobject wasn't opened with the right mode
+ or isn't a BytesIO object.
+ """
+ # We will be streaming the raw bytes from over the wire, so we need to
+ # ensure that writing to the fileobject will preserve those bytes. On
+ # Python3, if the user passes an io.StringIO, this will fail, so we need
+ # to check for BytesIO instead.
+ if not ('b' in getattr(fileobject, 'mode', '') or
+ isinstance(fileobject, io.BytesIO)):
+ raise TypeError('tee() will write bytes directly to this fileobject'
+ ', it must be opened with the "b" flag if it is a file'
+ ' or inherit from io.BytesIO.')
+
+ return _tee(response, fileobject.write, chunksize, decode_content)
+
+
+def tee_to_file(response, filename, chunksize=_DEFAULT_CHUNKSIZE,
+ decode_content=None):
+ """Stream the response both to the generator and a file.
+
+ This will open a file named ``filename`` and stream the response body
+ while writing the bytes to the opened file object.
+
+ Example usage:
+
+ .. code-block:: python
+
+ resp = requests.get(url, stream=True)
+ for chunk in tee_to_file(resp, 'save_file'):
+ # do stuff with chunk
+
+ :param response: Response from requests.
+ :type response: requests.Response
+ :param str filename: Name of file in which we write the response content.
+ :param int chunksize: (optional), Size of chunk to attempt to stream.
+ :param bool decode_content: (optional), If True, this will decode the
+ compressed content of the response.
+ """
+ with open(filename, 'wb') as fd:
+ for chunk in tee(response, fd, chunksize, decode_content):
+ yield chunk
+
+
+def tee_to_bytearray(response, bytearr, chunksize=_DEFAULT_CHUNKSIZE,
+ decode_content=None):
+ """Stream the response both to the generator and a bytearray.
+
+ This will stream the response provided to the function, add them to the
+ provided :class:`bytearray` and yield them to the user.
+
+ .. note::
+
+ This uses the :meth:`bytearray.extend` by default instead of passing
+ the bytearray into the ``readinto`` method.
+
+ Example usage:
+
+ .. code-block:: python
+
+ b = bytearray()
+ resp = requests.get(url, stream=True)
+ for chunk in tee_to_bytearray(resp, b):
+ # do stuff with chunk
+
+ :param response: Response from requests.
+ :type response: requests.Response
+ :param bytearray bytearr: Array to add the streamed bytes to.
+ :param int chunksize: (optional), Size of chunk to attempt to stream.
+ :param bool decode_content: (optional), If True, this will decode the
+ compressed content of the response.
+ """
+ if not isinstance(bytearr, bytearray):
+ raise TypeError('tee_to_bytearray() expects bytearr to be a '
+ 'bytearray')
+ return _tee(response, bytearr.extend, chunksize, decode_content)
diff --git a/requests_toolbelt/exceptions.py b/requests_toolbelt/exceptions.py
new file mode 100644
index 0000000..32ade21
--- /dev/null
+++ b/requests_toolbelt/exceptions.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+"""Collection of exceptions raised by requests-toolbelt."""
+
+
+class StreamingError(Exception):
+ """Used in :mod:`requests_toolbelt.downloadutils.stream`."""
+ pass
+
+
+class VersionMismatchError(Exception):
+ """Used to indicate a version mismatch in the version of requests required.
+
+ The feature in use requires a newer version of Requests to function
+ appropriately but the version installed is not sufficient.
+ """
+ pass
+
+
+class RequestsVersionTooOld(Warning):
+ """Used to indicate that the Requests version is too old.
+
+ If the version of Requests is too old to support a feature, we will issue
+ this warning to the user.
+ """
+ pass
+
+
+class IgnoringGAECertificateValidation(Warning):
+ """Used to indicate that given GAE validation behavior will be ignored.
+
+ If the user has tried to specify certificate validation when using the
+ insecure AppEngine adapter, it will be ignored (certificate validation will
+ remain off), so we will issue this warning to the user.
+
+ In :class:`requests_toolbelt.adapters.appengine.InsecureAppEngineAdapter`.
+ """
+ pass
diff --git a/requests_toolbelt/multipart/__init__.py b/requests_toolbelt/multipart/__init__.py
new file mode 100644
index 0000000..4bc4966
--- /dev/null
+++ b/requests_toolbelt/multipart/__init__.py
@@ -0,0 +1,31 @@
+"""
+requests_toolbelt.multipart
+===========================
+
+See http://toolbelt.rtfd.org/ for documentation
+
+:copyright: (c) 2014 by Ian Cordasco and Cory Benfield
+:license: Apache v2.0, see LICENSE for more details
+"""
+
+from .encoder import MultipartEncoder, MultipartEncoderMonitor
+from .decoder import MultipartDecoder
+from .decoder import ImproperBodyPartContentException
+from .decoder import NonMultipartContentTypeException
+
+__title__ = 'requests-toolbelt'
+__authors__ = 'Ian Cordasco, Cory Benfield'
+__license__ = 'Apache v2.0'
+__copyright__ = 'Copyright 2014 Ian Cordasco, Cory Benfield'
+
+__all__ = [
+ 'MultipartEncoder',
+ 'MultipartEncoderMonitor',
+ 'MultipartDecoder',
+ 'ImproperBodyPartContentException',
+ 'NonMultipartContentTypeException',
+ '__title__',
+ '__authors__',
+ '__license__',
+ '__copyright__',
+]
diff --git a/requests_toolbelt/multipart/decoder.py b/requests_toolbelt/multipart/decoder.py
new file mode 100644
index 0000000..8ec9c48
--- /dev/null
+++ b/requests_toolbelt/multipart/decoder.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+"""
+
+requests_toolbelt.multipart.decoder
+===================================
+
+This holds all the implementation details of the MultipartDecoder
+
+"""
+
+import sys
+import email.parser
+from .encoder import encode_with
+from requests.structures import CaseInsensitiveDict
+
+
+def _split_on_find(content, bound):
+ point = content.find(bound)
+ return content[:point], content[point + len(bound):]
+
+
+class ImproperBodyPartContentException(Exception):
+ pass
+
+
+class NonMultipartContentTypeException(Exception):
+ pass
+
+
+def _header_parser(string, encoding):
+ major = sys.version_info[0]
+ if major == 3:
+ string = string.decode(encoding)
+ headers = email.parser.HeaderParser().parsestr(string).items()
+ return (
+ (encode_with(k, encoding), encode_with(v, encoding))
+ for k, v in headers
+ )
+
+
+class BodyPart(object):
+ """
+
+ The ``BodyPart`` object is a ``Response``-like interface to an individual
+ subpart of a multipart response. It is expected that these will
+ generally be created by objects of the ``MultipartDecoder`` class.
+
+ Like ``Response``, there is a ``CaseInsensitiveDict`` object named header,
+ ``content`` to access bytes, ``text`` to access unicode, and ``encoding``
+ to access the unicode codec.
+
+ """
+
+ def __init__(self, content, encoding):
+ self.encoding = encoding
+ headers = {}
+ # Split into header section (if any) and the content
+ if b'\r\n\r\n' in content:
+ first, self.content = _split_on_find(content, b'\r\n\r\n')
+ if first != b'':
+ headers = _header_parser(first.lstrip(), encoding)
+ else:
+ raise ImproperBodyPartContentException(
+ 'content does not contain CR-LF-CR-LF'
+ )
+ self.headers = CaseInsensitiveDict(headers)
+
+ @property
+ def text(self):
+ """Content of the ``BodyPart`` in unicode."""
+ return self.content.decode(self.encoding)
+
+
+class MultipartDecoder(object):
+ """
+
+ The ``MultipartDecoder`` object parses the multipart payload of
+ a bytestring into a tuple of ``Response``-like ``BodyPart`` objects.
+
+ The basic usage is::
+
+ import requests
+ from requests_toolbelt import MultipartDecoder
+
+ response = request.get(url)
+ decoder = MultipartDecoder.from_response(response)
+ for part in decoder.parts:
+ print(part.header['content-type'])
+
+ If the multipart content is not from a response, basic usage is::
+
+ from requests_toolbelt import MultipartDecoder
+
+ decoder = MultipartDecoder(content, content_type)
+ for part in decoder.parts:
+ print(part.header['content-type'])
+
+ For both these usages, there is an optional ``encoding`` parameter. This is
+ a string, which is the name of the unicode codec to use (default is
+ ``'utf-8'``).
+
+ """
+ def __init__(self, content, content_type, encoding='utf-8'):
+ #: Original content
+ self.content = content
+ #: Original Content-Type header
+ self.content_type = content_type
+ #: Response body encoding
+ self.encoding = encoding
+ #: Parsed parts of the multipart response body
+ self.parts = tuple()
+ self._find_boundary()
+ self._parse_body()
+
+ def _find_boundary(self):
+ ct_info = tuple(x.strip() for x in self.content_type.split(';'))
+ mimetype = ct_info[0]
+ if mimetype.split('/')[0] != 'multipart':
+ raise NonMultipartContentTypeException(
+ "Unexpected mimetype in content-type: '{0}'".format(mimetype)
+ )
+ for item in ct_info[1:]:
+ attr, value = _split_on_find(
+ item,
+ '='
+ )
+ if attr.lower() == 'boundary':
+ self.boundary = encode_with(value.strip('"'), self.encoding)
+
+ @staticmethod
+ def _fix_first_part(part, boundary_marker):
+ bm_len = len(boundary_marker)
+ if boundary_marker == part[:bm_len]:
+ return part[bm_len:]
+ else:
+ return part
+
+ def _parse_body(self):
+ boundary = b''.join((b'--', self.boundary))
+
+ def body_part(part):
+ fixed = MultipartDecoder._fix_first_part(part, boundary)
+ return BodyPart(fixed, self.encoding)
+
+ def test_part(part):
+ return (part != b'' and
+ part != b'\r\n' and
+ part[:4] != b'--\r\n' and
+ part != b'--')
+
+ parts = self.content.split(b''.join((b'\r\n', boundary)))
+ self.parts = tuple(body_part(x) for x in parts if test_part(x))
+
+ @classmethod
+ def from_response(cls, response, encoding='utf-8'):
+ content = response.content
+ content_type = response.headers.get('content-type', None)
+ return cls(content, content_type, encoding)
diff --git a/requests_toolbelt/multipart/encoder.py b/requests_toolbelt/multipart/encoder.py
new file mode 100644
index 0000000..876ac61
--- /dev/null
+++ b/requests_toolbelt/multipart/encoder.py
@@ -0,0 +1,570 @@
+# -*- coding: utf-8 -*-
+"""
+
+requests_toolbelt.multipart.encoder
+===================================
+
+This holds all of the implementation details of the MultipartEncoder
+
+"""
+import contextlib
+import io
+import os
+from uuid import uuid4
+
+from .._compat import fields
+
+
+class MultipartEncoder(object):
+
+ """
+
+ The ``MultipartEncoder`` oject is a generic interface to the engine that
+ will create a ``multipart/form-data`` body for you.
+
+ The basic usage is:
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt import MultipartEncoder
+
+ encoder = MultipartEncoder({'field': 'value',
+ 'other_field', 'other_value'})
+ r = requests.post('https://httpbin.org/post', data=encoder,
+ headers={'Content-Type': encoder.content_type})
+
+ If you do not need to take advantage of streaming the post body, you can
+ also do:
+
+ .. code-block:: python
+
+ r = requests.post('https://httpbin.org/post',
+ data=encoder.to_string(),
+ headers={'Content-Type': encoder.content_type})
+
+ If you want the encoder to use a specific order, you can use an
+ OrderedDict or more simply, a list of tuples:
+
+ .. code-block:: python
+
+ encoder = MultipartEncoder([('field', 'value'),
+ ('other_field', 'other_value')])
+
+ .. versionchanged:: 0.4.0
+
+ You can also provide tuples as part values as you would provide them to
+ requests' ``files`` parameter.
+
+ .. code-block:: python
+
+ encoder = MultipartEncoder({
+ 'field': ('file_name', b'{"a": "b"}', 'application/json',
+ {'X-My-Header': 'my-value'})
+ ])
+
+ .. warning::
+
+ This object will end up directly in :mod:`httplib`. Currently,
+ :mod:`httplib` has a hard-coded read size of **8192 bytes**. This
+ means that it will loop until the file has been read and your upload
+ could take a while. This is **not** a bug in requests. A feature is
+ being considered for this object to allow you, the user, to specify
+ what size should be returned on a read. If you have opinions on this,
+ please weigh in on `this issue`_.
+
+ .. _this issue:
+ https://github.com/sigmavirus24/requests-toolbelt/issues/75
+
+ """
+
+ def __init__(self, fields, boundary=None, encoding='utf-8'):
+ #: Boundary value either passed in by the user or created
+ self.boundary_value = boundary or uuid4().hex
+
+ # Computed boundary
+ self.boundary = '--{0}'.format(self.boundary_value)
+
+ #: Encoding of the data being passed in
+ self.encoding = encoding
+
+ # Pre-encoded boundary
+ self._encoded_boundary = b''.join([
+ encode_with(self.boundary, self.encoding),
+ encode_with('\r\n', self.encoding)
+ ])
+
+ #: Fields provided by the user
+ self.fields = fields
+
+ #: Whether or not the encoder is finished
+ self.finished = False
+
+ #: Pre-computed parts of the upload
+ self.parts = []
+
+ # Pre-computed parts iterator
+ self._iter_parts = iter([])
+
+ # The part we're currently working with
+ self._current_part = None
+
+ # Cached computation of the body's length
+ self._len = None
+
+ # Our buffer
+ self._buffer = CustomBytesIO(encoding=encoding)
+
+ # Pre-compute each part's headers
+ self._prepare_parts()
+
+ # Load boundary into buffer
+ self._write_boundary()
+
+ @property
+ def len(self):
+ """Length of the multipart/form-data body.
+
+ requests will first attempt to get the length of the body by calling
+ ``len(body)`` and then by checking for the ``len`` attribute.
+
+ On 32-bit systems, the ``__len__`` method cannot return anything
+ larger than an integer (in C) can hold. If the total size of the body
+ is even slightly larger than 4GB users will see an OverflowError. This
+ manifested itself in `bug #80`_.
+
+ As such, we now calculate the length lazily as a property.
+
+ .. _bug #80:
+ https://github.com/sigmavirus24/requests-toolbelt/issues/80
+ """
+ # If _len isn't already calculated, calculate, return, and set it
+ return self._len or self._calculate_length()
+
+ def __repr__(self):
+ return '<MultipartEncoder: {0!r}>'.format(self.fields)
+
+ def _calculate_length(self):
+ """
+ This uses the parts to calculate the length of the body.
+
+ This returns the calculated length so __len__ can be lazy.
+ """
+ boundary_len = len(self.boundary) # Length of --{boundary}
+ # boundary length + header length + body length + len('\r\n') * 2
+ self._len = sum(
+ (boundary_len + total_len(p) + 4) for p in self.parts
+ ) + boundary_len + 4
+ return self._len
+
+ def _calculate_load_amount(self, read_size):
+ """This calculates how many bytes need to be added to the buffer.
+
+ When a consumer read's ``x`` from the buffer, there are two cases to
+ satisfy:
+
+ 1. Enough data in the buffer to return the requested amount
+ 2. Not enough data
+
+ This function uses the amount of unread bytes in the buffer and
+ determines how much the Encoder has to load before it can return the
+ requested amount of bytes.
+
+ :param int read_size: the number of bytes the consumer requests
+ :returns: int -- the number of bytes that must be loaded into the
+ buffer before the read can be satisfied. This will be strictly
+ non-negative
+ """
+ amount = read_size - total_len(self._buffer)
+ return amount if amount > 0 else 0
+
+ def _load(self, amount):
+ """Load ``amount`` number of bytes into the buffer."""
+ self._buffer.smart_truncate()
+ part = self._current_part or self._next_part()
+ while amount == -1 or amount > 0:
+ written = 0
+ if not part.bytes_left_to_write():
+ written += self._write(b'\r\n')
+ written += self._write_boundary()
+ part = self._next_part()
+
+ if not part:
+ written += self._write_closing_boundary()
+ self.finished = True
+ break
+
+ written += part.write_to(self._buffer, amount)
+
+ if amount != -1:
+ amount -= written
+
+ def _next_part(self):
+ try:
+ p = self._current_part = next(self._iter_parts)
+ except StopIteration:
+ p = None
+ return p
+
+ def _iter_fields(self):
+ _fields = self.fields
+ if hasattr(self.fields, 'items'):
+ _fields = list(self.fields.items())
+ for k, v in _fields:
+ file_name = None
+ file_type = None
+ file_headers = None
+ if isinstance(v, (list, tuple)):
+ if len(v) == 2:
+ file_name, file_pointer = v
+ elif len(v) == 3:
+ file_name, file_pointer, file_type = v
+ else:
+ file_name, file_pointer, file_type, file_headers = v
+ else:
+ file_pointer = v
+
+ field = fields.RequestField(name=k, data=file_pointer,
+ filename=file_name,
+ headers=file_headers)
+ field.make_multipart(content_type=file_type)
+ yield field
+
+ def _prepare_parts(self):
+ """This uses the fields provided by the user and creates Part objects.
+
+ It populates the `parts` attribute and uses that to create a
+ generator for iteration.
+ """
+ enc = self.encoding
+ self.parts = [Part.from_field(f, enc) for f in self._iter_fields()]
+ self._iter_parts = iter(self.parts)
+
+ def _write(self, bytes_to_write):
+ """Write the bytes to the end of the buffer.
+
+ :param bytes bytes_to_write: byte-string (or bytearray) to append to
+ the buffer
+ :returns: int -- the number of bytes written
+ """
+ return self._buffer.append(bytes_to_write)
+
+ def _write_boundary(self):
+ """Write the boundary to the end of the buffer."""
+ return self._write(self._encoded_boundary)
+
+ def _write_closing_boundary(self):
+ """Write the bytes necessary to finish a multipart/form-data body."""
+ with reset(self._buffer):
+ self._buffer.seek(-2, 2)
+ self._buffer.write(b'--\r\n')
+ return 2
+
+ def _write_headers(self, headers):
+ """Write the current part's headers to the buffer."""
+ return self._write(encode_with(headers, self.encoding))
+
+ @property
+ def content_type(self):
+ return str(
+ 'multipart/form-data; boundary={0}'.format(self.boundary_value)
+ )
+
+ def to_string(self):
+ """Return the entirety of the data in the encoder.
+
+ .. note::
+
+ This simply reads all of the data it can. If you have started
+ streaming or reading data from the encoder, this method will only
+ return whatever data is left in the encoder.
+
+ .. note::
+
+ This method affects the internal state of the encoder. Calling
+ this method will exhaust the encoder.
+
+ :returns: the multipart message
+ :rtype: bytes
+ """
+
+ return self.read()
+
+ def read(self, size=-1):
+ """Read data from the streaming encoder.
+
+ :param int size: (optional), If provided, ``read`` will return exactly
+ that many bytes. If it is not provided, it will return the
+ remaining bytes.
+ :returns: bytes
+ """
+ if self.finished:
+ return self._buffer.read(size)
+
+ bytes_to_load = size
+ if bytes_to_load != -1 and bytes_to_load is not None:
+ bytes_to_load = self._calculate_load_amount(int(size))
+
+ self._load(bytes_to_load)
+ return self._buffer.read(size)
+
+
+def IDENTITY(monitor):
+ return monitor
+
+
+class MultipartEncoderMonitor(object):
+
+ """
+ An object used to monitor the progress of a :class:`MultipartEncoder`.
+
+ The :class:`MultipartEncoder` should only be responsible for preparing and
+ streaming the data. For anyone who wishes to monitor it, they shouldn't be
+ using that instance to manage that as well. Using this class, they can
+ monitor an encoder and register a callback. The callback receives the
+ instance of the monitor.
+
+ To use this monitor, you construct your :class:`MultipartEncoder` as you
+ normally would.
+
+ .. code-block:: python
+
+ from requests_toolbelt import (MultipartEncoder,
+ MultipartEncoderMonitor)
+ import requests
+
+ def callback(encoder, bytes_read):
+ # Do something with this information
+ pass
+
+ m = MultipartEncoder(fields={'field0': 'value0'})
+ monitor = MultipartEncoderMonitor(m, callback)
+ headers = {'Content-Type': montior.content_type}
+ r = requests.post('https://httpbin.org/post', data=monitor,
+ headers=headers)
+
+ Alternatively, if your use case is very simple, you can use the following
+ pattern.
+
+ .. code-block:: python
+
+ from requests_toolbelt import MultipartEncoderMonitor
+ import requests
+
+ def callback(encoder, bytes_read):
+ # Do something with this information
+ pass
+
+ monitor = MultipartEncoderMonitor.from_fields(
+ fields={'field0': 'value0'}, callback
+ )
+ headers = {'Content-Type': montior.content_type}
+ r = requests.post('https://httpbin.org/post', data=monitor,
+ headers=headers)
+
+ """
+
+ def __init__(self, encoder, callback=None):
+ #: Instance of the :class:`MultipartEncoder` being monitored
+ self.encoder = encoder
+
+ #: Optionally function to call after a read
+ self.callback = callback or IDENTITY
+
+ #: Number of bytes already read from the :class:`MultipartEncoder`
+ #: instance
+ self.bytes_read = 0
+
+ #: Avoid the same problem in bug #80
+ self.len = self.encoder.len
+
+ @classmethod
+ def from_fields(cls, fields, boundary=None, encoding='utf-8',
+ callback=None):
+ encoder = MultipartEncoder(fields, boundary, encoding)
+ return cls(encoder, callback)
+
+ @property
+ def content_type(self):
+ return self.encoder.content_type
+
+ def to_string(self):
+ return self.read()
+
+ def read(self, size=-1):
+ string = self.encoder.read(size)
+ self.bytes_read += len(string)
+ self.callback(self)
+ return string
+
+
+def encode_with(string, encoding):
+ """Encoding ``string`` with ``encoding`` if necessary.
+
+ :param str string: If string is a bytes object, it will not encode it.
+ Otherwise, this function will encode it with the provided encoding.
+ :param str encoding: The encoding with which to encode string.
+ :returns: encoded bytes object
+ """
+ if not (string is None or isinstance(string, bytes)):
+ return string.encode(encoding)
+ return string
+
+
+def readable_data(data, encoding):
+ """Coerce the data to an object with a ``read`` method."""
+ if hasattr(data, 'read'):
+ return data
+
+ return CustomBytesIO(data, encoding)
+
+
+def total_len(o):
+ if hasattr(o, '__len__'):
+ return len(o)
+
+ if hasattr(o, 'len'):
+ return o.len
+
+ if hasattr(o, 'fileno'):
+ try:
+ fileno = o.fileno()
+ except io.UnsupportedOperation:
+ pass
+ else:
+ return os.fstat(fileno).st_size
+
+ if hasattr(o, 'getvalue'):
+ # e.g. BytesIO, cStringIO.StringIO
+ return len(o.getvalue())
+
+
+@contextlib.contextmanager
+def reset(buffer):
+ """Keep track of the buffer's current position and write to the end.
+
+ This is a context manager meant to be used when adding data to the buffer.
+ It eliminates the need for every function to be concerned with the
+ position of the cursor in the buffer.
+ """
+ original_position = buffer.tell()
+ buffer.seek(0, 2)
+ yield
+ buffer.seek(original_position, 0)
+
+
+def coerce_data(data, encoding):
+ """Ensure that every object's __len__ behaves uniformly."""
+ if not isinstance(data, CustomBytesIO):
+ if hasattr(data, 'getvalue'):
+ return CustomBytesIO(data.getvalue(), encoding)
+
+ if hasattr(data, 'fileno'):
+ return FileWrapper(data)
+
+ if not hasattr(data, 'read'):
+ return CustomBytesIO(data, encoding)
+
+ return data
+
+
+def to_list(fields):
+ if hasattr(fields, 'items'):
+ return list(fields.items())
+ return list(fields)
+
+
+class Part(object):
+ def __init__(self, headers, body):
+ self.headers = headers
+ self.body = body
+ self.headers_unread = True
+ self.len = len(self.headers) + total_len(self.body)
+
+ @classmethod
+ def from_field(cls, field, encoding):
+ """Create a part from a Request Field generated by urllib3."""
+ headers = encode_with(field.render_headers(), encoding)
+ body = coerce_data(field.data, encoding)
+ return cls(headers, body)
+
+ def bytes_left_to_write(self):
+ """Determine if there are bytes left to write.
+
+ :returns: bool -- ``True`` if there are bytes left to write, otherwise
+ ``False``
+ """
+ to_read = 0
+ if self.headers_unread:
+ to_read += len(self.headers)
+
+ return (to_read + total_len(self.body)) > 0
+
+ def write_to(self, buffer, size):
+ """Write the requested amount of bytes to the buffer provided.
+
+ The number of bytes written may exceed size on the first read since we
+ load the headers ambitiously.
+
+ :param CustomBytesIO buffer: buffer we want to write bytes to
+ :param int size: number of bytes requested to be written to the buffer
+ :returns: int -- number of bytes actually written
+ """
+ written = 0
+ if self.headers_unread:
+ written += buffer.append(self.headers)
+ self.headers_unread = False
+
+ while total_len(self.body) > 0 and (size == -1 or written < size):
+ amount_to_read = size
+ if size != -1:
+ amount_to_read = size - written
+ written += buffer.append(self.body.read(amount_to_read))
+
+ return written
+
+
+class CustomBytesIO(io.BytesIO):
+ def __init__(self, buffer=None, encoding='utf-8'):
+ buffer = encode_with(buffer, encoding)
+ super(CustomBytesIO, self).__init__(buffer)
+
+ def _get_end(self):
+ current_pos = self.tell()
+ self.seek(0, 2)
+ length = self.tell()
+ self.seek(current_pos, 0)
+ return length
+
+ @property
+ def len(self):
+ length = self._get_end()
+ return length - self.tell()
+
+ def append(self, bytes):
+ with reset(self):
+ written = self.write(bytes)
+ return written
+
+ def smart_truncate(self):
+ to_be_read = total_len(self)
+ already_read = self._get_end() - to_be_read
+
+ if already_read >= to_be_read:
+ old_bytes = self.read()
+ self.seek(0, 0)
+ self.truncate()
+ self.write(old_bytes)
+ self.seek(0, 0) # We want to be at the beginning
+
+
+class FileWrapper(object):
+ def __init__(self, file_object):
+ self.fd = file_object
+
+ @property
+ def len(self):
+ return total_len(self.fd) - self.fd.tell()
+
+ def read(self, length=-1):
+ return self.fd.read(length)
diff --git a/requests_toolbelt/sessions.py b/requests_toolbelt/sessions.py
new file mode 100644
index 0000000..7beac96
--- /dev/null
+++ b/requests_toolbelt/sessions.py
@@ -0,0 +1,70 @@
+import requests
+
+from ._compat import urljoin
+
+
+class BaseUrlSession(requests.Session):
+ """A Session with a URL that all requests will use as a base.
+
+ Let's start by looking at an example:
+
+ .. code-block:: python
+
+ >>> from requests_toolbelt import sessions
+ >>> s = sessions.BaseUrlSession(
+ ... base_url='https://example.com/resource/')
+ >>> r = s.get('sub-resource/' params={'foo': 'bar'})
+ >>> print(r.request.url)
+ https://example.com/resource/sub-resource/?foo=bar
+
+ Our call to the ``get`` method will make a request to the URL passed in
+ when we created the Session and the partial resource name we provide.
+
+ We implement this by overriding the ``request`` method so most uses of a
+ Session are covered. (This, however, precludes the use of PreparedRequest
+ objects).
+
+ .. note::
+
+ The base URL that you provide and the path you provide are **very**
+ important.
+
+ Let's look at another *similar* example
+
+ .. code-block:: python
+
+ >>> from requests_toolbelt import sessions
+ >>> s = sessions.BaseUrlSession(
+ ... base_url='https://example.com/resource/')
+ >>> r = s.get('/sub-resource/' params={'foo': 'bar'})
+ >>> print(r.request.url)
+ https://example.com/sub-resource/?foo=bar
+
+ The key difference here is that we called ``get`` with ``/sub-resource/``,
+ i.e., there was a leading ``/``. This changes how we create the URL
+ because we rely on :mod:`urllib.parse.urljoin`.
+
+ To override how we generate the URL, sub-class this method and override the
+ ``create_url`` method.
+
+ Based on implementation from
+ https://github.com/kennethreitz/requests/issues/2554#issuecomment-109341010
+ """
+
+ base_url = None
+
+ def __init__(self, base_url=None):
+ if base_url:
+ self.base_url = base_url
+ super(BaseUrlSession, self).__init__()
+
+ def request(self, method, url, *args, **kwargs):
+ """Send the request after generating the complete URL."""
+ url = self.create_url(url)
+ return super(BaseUrlSession, self).request(
+ method, url, *args, **kwargs
+ )
+
+ def create_url(self, url):
+ """Create the URL based off this partial path."""
+ return urljoin(self.base_url, url)
diff --git a/requests_toolbelt/streaming_iterator.py b/requests_toolbelt/streaming_iterator.py
new file mode 100644
index 0000000..64fd75f
--- /dev/null
+++ b/requests_toolbelt/streaming_iterator.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+"""
+
+requests_toolbelt.streaming_iterator
+====================================
+
+This holds the implementation details for the :class:`StreamingIterator`. It
+is designed for the case where you, the user, know the size of the upload but
+need to provide the data as an iterator. This class will allow you to specify
+the size and stream the data without using a chunked transfer-encoding.
+
+"""
+from requests.utils import super_len
+
+from .multipart.encoder import CustomBytesIO, encode_with
+
+
+class StreamingIterator(object):
+
+ """
+ This class provides a way of allowing iterators with a known size to be
+ streamed instead of chunked.
+
+ In requests, if you pass in an iterator it assumes you want to use
+ chunked transfer-encoding to upload the data, which not all servers
+ support well. Additionally, you may want to set the content-length
+ yourself to avoid this but that will not work. The only way to preempt
+ requests using a chunked transfer-encoding and forcing it to stream the
+ uploads is to mimic a very specific interace. Instead of having to know
+ these details you can instead just use this class. You simply provide the
+ size and iterator and pass the instance of StreamingIterator to requests
+ via the data parameter like so:
+
+ .. code-block:: python
+
+ from requests_toolbelt import StreamingIterator
+
+ import requests
+
+ # Let iterator be some generator that you already have and size be
+ # the size of the data produced by the iterator
+
+ r = requests.post(url, data=StreamingIterator(size, iterator))
+
+ You can also pass file-like objects to :py:class:`StreamingIterator` in
+ case requests can't determize the filesize itself. This is the case with
+ streaming file objects like ``stdin`` or any sockets. Wrapping e.g. files
+ that are on disk with ``StreamingIterator`` is unnecessary, because
+ requests can determine the filesize itself.
+
+ Naturally, you should also set the `Content-Type` of your upload
+ appropriately because the toolbelt will not attempt to guess that for you.
+ """
+
+ def __init__(self, size, iterator, encoding='utf-8'):
+ #: The expected size of the upload
+ self.size = int(size)
+
+ if self.size < 0:
+ raise ValueError(
+ 'The size of the upload must be a positive integer'
+ )
+
+ #: Attribute that requests will check to determine the length of the
+ #: body. See bug #80 for more details
+ self.len = self.size
+
+ #: Encoding the input data is using
+ self.encoding = encoding
+
+ #: The iterator used to generate the upload data
+ self.iterator = iterator
+
+ if hasattr(iterator, 'read'):
+ self._file = iterator
+ else:
+ self._file = _IteratorAsBinaryFile(iterator, encoding)
+
+ def read(self, size=-1):
+ return encode_with(self._file.read(size), self.encoding)
+
+
+class _IteratorAsBinaryFile(object):
+ def __init__(self, iterator, encoding='utf-8'):
+ #: The iterator used to generate the upload data
+ self.iterator = iterator
+
+ #: Encoding the iterator is using
+ self.encoding = encoding
+
+ # The buffer we use to provide the correct number of bytes requested
+ # during a read
+ self._buffer = CustomBytesIO()
+
+ def _get_bytes(self):
+ try:
+ return encode_with(next(self.iterator), self.encoding)
+ except StopIteration:
+ return b''
+
+ def _load_bytes(self, size):
+ self._buffer.smart_truncate()
+ amount_to_load = size - super_len(self._buffer)
+ bytes_to_append = True
+
+ while amount_to_load > 0 and bytes_to_append:
+ bytes_to_append = self._get_bytes()
+ amount_to_load -= self._buffer.append(bytes_to_append)
+
+ def read(self, size=-1):
+ size = int(size)
+ if size == -1:
+ return b''.join(self.iterator)
+
+ self._load_bytes(size)
+ return self._buffer.read(size)
diff --git a/requests_toolbelt/threaded/__init__.py b/requests_toolbelt/threaded/__init__.py
new file mode 100644
index 0000000..cd0ab1c
--- /dev/null
+++ b/requests_toolbelt/threaded/__init__.py
@@ -0,0 +1,97 @@
+"""
+This module provides the API for ``requests_toolbelt.threaded``.
+
+The module provides a clean and simple API for making requests via a thread
+pool. The thread pool will use sessions for increased performance.
+
+A simple use-case is:
+
+.. code-block:: python
+
+ from requests_toolbelt import threaded
+
+ urls_to_get = [{
+ 'url': 'https://api.github.com/users/sigmavirus24',
+ 'method': 'GET',
+ }, {
+ 'url': 'https://api.github.com/repos/sigmavirus24/requests-toolbelt',
+ 'method': 'GET',
+ }, {
+ 'url': 'https://google.com',
+ 'method': 'GET',
+ }]
+ responses, errors = threaded.map(urls_to_get)
+
+By default, the threaded submodule will detect the number of CPUs your
+computer has and use that if no other number of processes is selected. To
+change this, always use the keyword argument ``num_processes``. Using the
+above example, we would expand it like so:
+
+.. code-block:: python
+
+ responses, errors = threaded.map(urls_to_get, num_processes=10)
+
+You can also customize how a :class:`requests.Session` is initialized by
+creating a callback function:
+
+.. code-block:: python
+
+ from requests_toolbelt import user_agent
+
+ def initialize_session(session):
+ session.headers['User-Agent'] = user_agent('my-scraper', '0.1')
+ session.headers['Accept'] = 'application/json'
+
+ responses, errors = threaded.map(urls_to_get,
+ initializer=initialize_session)
+
+.. autofunction:: requests_toolbelt.threaded.map
+
+Inspiration is blatantly drawn from the standard library's multiprocessing
+library. See the following references:
+
+- multiprocessing's `pool source`_
+
+- map and map_async `inspiration`_
+
+.. _pool source:
+ https://hg.python.org/cpython/file/8ef4f75a8018/Lib/multiprocessing/pool.py
+.. _inspiration:
+ https://hg.python.org/cpython/file/8ef4f75a8018/Lib/multiprocessing/pool.py#l340
+"""
+from . import pool
+from .._compat import queue
+
+
+def map(requests, **kwargs):
+ r"""Simple interface to the threaded Pool object.
+
+ This function takes a list of dictionaries representing requests to make
+ using Sessions in threads and returns a tuple where the first item is
+ a generator of successful responses and the second is a generator of
+ exceptions.
+
+ :param list requests:
+ Collection of dictionaries representing requests to make with the Pool
+ object.
+ :param \*\*kwargs:
+ Keyword arguments that are passed to the
+ :class:`~requests_toolbelt.threaded.pool.Pool` object.
+ :returns: Tuple of responses and exceptions from the pool
+ :rtype: (:class:`~requests_toolbelt.threaded.pool.ThreadResponse`,
+ :class:`~requests_toolbelt.threaded.pool.ThreadException`)
+ """
+ if not (requests and all(isinstance(r, dict) for r in requests)):
+ raise ValueError('map expects a list of dictionaries.')
+
+ # Build our queue of requests
+ job_queue = queue.Queue()
+ for request in requests:
+ job_queue.put(request)
+
+ # Ensure the user doesn't try to pass their own job_queue
+ kwargs['job_queue'] = job_queue
+
+ threadpool = pool.Pool(**kwargs)
+ threadpool.join_all()
+ return threadpool.responses(), threadpool.exceptions()
diff --git a/requests_toolbelt/threaded/pool.py b/requests_toolbelt/threaded/pool.py
new file mode 100644
index 0000000..fc24dcc
--- /dev/null
+++ b/requests_toolbelt/threaded/pool.py
@@ -0,0 +1,211 @@
+"""Module implementing the Pool for :mod:``requests_toolbelt.threaded``."""
+import multiprocessing
+import requests
+
+from . import thread
+from .._compat import queue
+
+
+class Pool(object):
+ """Pool that manages the threads containing sessions.
+
+ :param queue:
+ The queue you're expected to use to which you should add items.
+ :type queue: queue.Queue
+ :param initializer:
+ Function used to initialize an instance of ``session``.
+ :type initializer: collections.Callable
+ :param auth_generator:
+ Function used to generate new auth credentials for the session.
+ :type auth_generator: collections.Callable
+ :param int num_threads:
+ Number of threads to create.
+ :param session:
+ :type session: requests.Session
+ """
+
+ def __init__(self, job_queue, initializer=None, auth_generator=None,
+ num_processes=None, session=requests.Session):
+ if num_processes is None:
+ num_processes = multiprocessing.cpu_count() or 1
+
+ if num_processes < 1:
+ raise ValueError("Number of processes should at least be 1.")
+
+ self._job_queue = job_queue
+ self._response_queue = queue.Queue()
+ self._exc_queue = queue.Queue()
+ self._processes = num_processes
+ self._initializer = initializer or _identity
+ self._auth = auth_generator or _identity
+ self._session = session
+ self._pool = [
+ thread.SessionThread(self._new_session(), self._job_queue,
+ self._response_queue, self._exc_queue)
+ for _ in range(self._processes)
+ ]
+
+ def _new_session(self):
+ return self._auth(self._initializer(self._session()))
+
+ @classmethod
+ def from_exceptions(cls, exceptions, **kwargs):
+ r"""Create a :class:`~Pool` from an :class:`~ThreadException`\ s.
+
+ Provided an iterable that provides :class:`~ThreadException` objects,
+ this classmethod will generate a new pool to retry the requests that
+ caused the exceptions.
+
+ :param exceptions:
+ Iterable that returns :class:`~ThreadException`
+ :type exceptions: iterable
+ :param kwargs:
+ Keyword arguments passed to the :class:`~Pool` initializer.
+ :returns: An initialized :class:`~Pool` object.
+ :rtype: :class:`~Pool`
+ """
+ job_queue = queue.Queue()
+ for exc in exceptions:
+ job_queue.put(exc.request_kwargs)
+
+ return cls(job_queue=job_queue, **kwargs)
+
+ @classmethod
+ def from_urls(cls, urls, request_kwargs=None, **kwargs):
+ """Create a :class:`~Pool` from an iterable of URLs.
+
+ :param urls:
+ Iterable that returns URLs with which we create a pool.
+ :type urls: iterable
+ :param dict request_kwargs:
+ Dictionary of other keyword arguments to provide to the request
+ method.
+ :param kwargs:
+ Keyword arguments passed to the :class:`~Pool` initializer.
+ :returns: An initialized :class:`~Pool` object.
+ :rtype: :class:`~Pool`
+ """
+ request_dict = {'method': 'GET'}
+ request_dict.update(request_kwargs or {})
+ job_queue = queue.Queue()
+ for url in urls:
+ job = request_dict.copy()
+ job.update({'url': url})
+ job_queue.put(job)
+
+ return cls(job_queue=job_queue, **kwargs)
+
+ def exceptions(self):
+ """Iterate over all the exceptions in the pool.
+
+ :returns: Generator of :class:`~ThreadException`
+ """
+ while True:
+ exc = self.get_exception()
+ if exc is None:
+ break
+ yield exc
+
+ def get_exception(self):
+ """Get an exception from the pool.
+
+ :rtype: :class:`~ThreadException`
+ """
+ try:
+ (request, exc) = self._exc_queue.get_nowait()
+ except queue.Empty:
+ return None
+ else:
+ return ThreadException(request, exc)
+
+ def get_response(self):
+ """Get a response from the pool.
+
+ :rtype: :class:`~ThreadResponse`
+ """
+ try:
+ (request, response) = self._response_queue.get_nowait()
+ except queue.Empty:
+ return None
+ else:
+ return ThreadResponse(request, response)
+
+ def responses(self):
+ """Iterate over all the responses in the pool.
+
+ :returns: Generator of :class:`~ThreadResponse`
+ """
+ while True:
+ resp = self.get_response()
+ if resp is None:
+ break
+ yield resp
+
+ def join_all(self):
+ """Join all the threads to the master thread."""
+ for session_thread in self._pool:
+ session_thread.join()
+
+
+class ThreadProxy(object):
+ proxied_attr = None
+
+ def __getattr__(self, attr):
+ """Proxy attribute accesses to the proxied object."""
+ get = object.__getattribute__
+ if attr not in self.attrs:
+ response = get(self, self.proxied_attr)
+ return getattr(response, attr)
+ else:
+ return get(self, attr)
+
+
+class ThreadResponse(ThreadProxy):
+ """A wrapper around a requests Response object.
+
+ This will proxy most attribute access actions to the Response object. For
+ example, if you wanted the parsed JSON from the response, you might do:
+
+ .. code-block:: python
+
+ thread_response = pool.get_response()
+ json = thread_response.json()
+
+ """
+ proxied_attr = 'response'
+ attrs = frozenset(['request_kwargs', 'response'])
+
+ def __init__(self, request_kwargs, response):
+ #: The original keyword arguments provided to the queue
+ self.request_kwargs = request_kwargs
+ #: The wrapped response
+ self.response = response
+
+
+class ThreadException(ThreadProxy):
+ """A wrapper around an exception raised during a request.
+
+ This will proxy most attribute access actions to the exception object. For
+ example, if you wanted the message from the exception, you might do:
+
+ .. code-block:: python
+
+ thread_exc = pool.get_exception()
+ msg = thread_exc.message
+
+ """
+ proxied_attr = 'exception'
+ attrs = frozenset(['request_kwargs', 'exception'])
+
+ def __init__(self, request_kwargs, exception):
+ #: The original keyword arguments provided to the queue
+ self.request_kwargs = request_kwargs
+ #: The captured and wrapped exception
+ self.exception = exception
+
+
+def _identity(session_obj):
+ return session_obj
+
+
+__all__ = ['ThreadException', 'ThreadResponse', 'Pool']
diff --git a/requests_toolbelt/threaded/thread.py b/requests_toolbelt/threaded/thread.py
new file mode 100644
index 0000000..542813c
--- /dev/null
+++ b/requests_toolbelt/threaded/thread.py
@@ -0,0 +1,53 @@
+"""Module containing the SessionThread class."""
+import threading
+import uuid
+
+import requests.exceptions as exc
+
+from .._compat import queue
+
+
+class SessionThread(object):
+ def __init__(self, initialized_session, job_queue, response_queue,
+ exception_queue):
+ self._session = initialized_session
+ self._jobs = job_queue
+ self._create_worker()
+ self._responses = response_queue
+ self._exceptions = exception_queue
+
+ def _create_worker(self):
+ self._worker = threading.Thread(
+ target=self._make_request,
+ name=uuid.uuid4(),
+ )
+ self._worker.daemon = True
+ self._worker._state = 0
+ self._worker.start()
+
+ def _handle_request(self, kwargs):
+ try:
+ response = self._session.request(**kwargs)
+ except exc.RequestException as e:
+ self._exceptions.put((kwargs, e))
+ else:
+ self._responses.put((kwargs, response))
+ finally:
+ self._jobs.task_done()
+
+ def _make_request(self):
+ while True:
+ try:
+ kwargs = self._jobs.get_nowait()
+ except queue.Empty:
+ break
+
+ self._handle_request(kwargs)
+
+ def is_alive(self):
+ """Proxy to the thread's ``is_alive`` method."""
+ return self._worker.is_alive()
+
+ def join(self):
+ """Join this thread to the master thread."""
+ self._worker.join()
diff --git a/requests_toolbelt/utils/__init__.py b/requests_toolbelt/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requests_toolbelt/utils/__init__.py
diff --git a/requests_toolbelt/utils/deprecated.py b/requests_toolbelt/utils/deprecated.py
new file mode 100644
index 0000000..c935783
--- /dev/null
+++ b/requests_toolbelt/utils/deprecated.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+"""A collection of functions deprecated in requests.utils."""
+import re
+import sys
+
+from requests import utils
+
+find_charset = re.compile(
+ br'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I
+).findall
+
+find_pragma = re.compile(
+ br'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I
+).findall
+
+find_xml = re.compile(
+ br'^<\?xml.*?encoding=["\']*(.+?)["\'>]'
+).findall
+
+
+def get_encodings_from_content(content):
+ """Return encodings from given content string.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.utils import deprecated
+
+ r = requests.get(url)
+ encodings = deprecated.get_encodings_from_content(r)
+
+ :param content: bytestring to extract encodings from
+ :type content: bytes
+ :return: encodings detected in the provided content
+ :rtype: list(str)
+ """
+ encodings = (find_charset(content) + find_pragma(content)
+ + find_xml(content))
+ if (3, 0) <= sys.version_info < (4, 0):
+ encodings = [encoding.decode('utf8') for encoding in encodings]
+ return encodings
+
+
+def get_unicode_from_response(response):
+ """Return the requested content back in unicode.
+
+ This will first attempt to retrieve the encoding from the response
+ headers. If that fails, it will use
+ :func:`requests_toolbelt.utils.deprecated.get_encodings_from_content`
+ to determine encodings from HTML elements.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.utils import deprecated
+
+ r = requests.get(url)
+ text = deprecated.get_unicode_from_response(r)
+
+ :param response: Response object to get unicode content from.
+ :type response: requests.models.Response
+ """
+ tried_encodings = set()
+
+ # Try charset from content-type
+ encoding = utils.get_encoding_from_headers(response.headers)
+
+ if encoding:
+ try:
+ return str(response.content, encoding)
+ except UnicodeError:
+ tried_encodings.add(encoding.lower())
+
+ encodings = get_encodings_from_content(response.content)
+
+ for _encoding in encodings:
+ _encoding = _encoding.lower()
+ if _encoding in tried_encodings:
+ continue
+ try:
+ return str(response.content, _encoding)
+ except UnicodeError:
+ tried_encodings.add(_encoding)
+
+ # Fall back:
+ if encoding:
+ try:
+ return str(response.content, encoding, errors='replace')
+ except TypeError:
+ pass
+ return response.text
diff --git a/requests_toolbelt/utils/dump.py b/requests_toolbelt/utils/dump.py
new file mode 100644
index 0000000..06d7303
--- /dev/null
+++ b/requests_toolbelt/utils/dump.py
@@ -0,0 +1,195 @@
+"""This module provides functions for dumping information about responses."""
+import collections
+
+from requests import compat
+
+
+__all__ = ('dump_response', 'dump_all')
+
+HTTP_VERSIONS = {
+ 9: b'0.9',
+ 10: b'1.0',
+ 11: b'1.1',
+}
+
+_PrefixSettings = collections.namedtuple('PrefixSettings',
+ ['request', 'response'])
+
+
+class PrefixSettings(_PrefixSettings):
+ def __new__(cls, request, response):
+ request = _coerce_to_bytes(request)
+ response = _coerce_to_bytes(response)
+ return super(PrefixSettings, cls).__new__(cls, request, response)
+
+
+def _get_proxy_information(response):
+ if getattr(response.connection, 'proxy_manager', False):
+ proxy_info = {}
+ request_url = response.request.url
+ if request_url.startswith('https://'):
+ proxy_info['method'] = 'CONNECT'
+
+ proxy_info['request_path'] = request_url
+ return proxy_info
+ return None
+
+
+def _format_header(name, value):
+ return (_coerce_to_bytes(name) + b': ' + _coerce_to_bytes(value) +
+ b'\r\n')
+
+
+def _build_request_path(url, proxy_info):
+ uri = compat.urlparse(url)
+ proxy_url = proxy_info.get('request_path')
+ if proxy_url is not None:
+ return proxy_url, uri
+
+ request_path = _coerce_to_bytes(uri.path)
+ if uri.query:
+ request_path += b'?' + _coerce_to_bytes(uri.query)
+
+ return request_path, uri
+
+
+def _dump_request_data(request, prefixes, bytearr, proxy_info=None):
+ if proxy_info is None:
+ proxy_info = {}
+
+ prefix = prefixes.request
+ method = _coerce_to_bytes(proxy_info.pop('method', request.method))
+ request_path, uri = _build_request_path(request.url, proxy_info)
+
+ # <prefix><METHOD> <request-path> HTTP/1.1
+ bytearr.extend(prefix + method + b' ' + request_path + b' HTTP/1.1\r\n')
+
+ # <prefix>Host: <request-host> OR host header specified by user
+ headers = request.headers.copy()
+ host_header = _coerce_to_bytes(headers.pop('Host', uri.netloc))
+ bytearr.extend(prefix + b'Host: ' + host_header + b'\r\n')
+
+ for name, value in headers.items():
+ bytearr.extend(prefix + _format_header(name, value))
+
+ bytearr.extend(prefix + b'\r\n')
+ if request.body:
+ if isinstance(request.body, compat.basestring):
+ bytearr.extend(prefix + _coerce_to_bytes(request.body))
+ else:
+ # In the event that the body is a file-like object, let's not try
+ # to read everything into memory.
+ bytearr.extend('<< Request body is not a string-like type >>')
+ bytearr.extend(b'\r\n')
+
+
+def _dump_response_data(response, prefixes, bytearr):
+ prefix = prefixes.response
+ # Let's interact almost entirely with urllib3's response
+ raw = response.raw
+
+ # Let's convert the version int from httplib to bytes
+ version_str = HTTP_VERSIONS.get(raw.version, b'?')
+
+ # <prefix>HTTP/<version_str> <status_code> <reason>
+ bytearr.extend(prefix + b'HTTP/' + version_str + b' ' +
+ str(raw.status).encode('ascii') + b' ' +
+ _coerce_to_bytes(response.reason) + b'\r\n')
+
+ headers = raw.headers
+ for name in headers.keys():
+ for value in headers.getlist(name):
+ bytearr.extend(prefix + _format_header(name, value))
+
+ bytearr.extend(prefix + b'\r\n')
+
+ bytearr.extend(response.content)
+
+
+def _coerce_to_bytes(data):
+ if not isinstance(data, bytes) and hasattr(data, 'encode'):
+ data = data.encode('utf-8')
+ return data
+
+
+def dump_response(response, request_prefix=b'< ', response_prefix=b'> ',
+ data_array=None):
+ """Dump a single request-response cycle's information.
+
+ This will take a response object and dump only the data that requests can
+ see for that single request-response cycle.
+
+ Example::
+
+ import requests
+ from requests_toolbelt.utils import dump
+
+ resp = requests.get('https://api.github.com/users/sigmavirus24')
+ data = dump.dump_response(resp)
+ print(data.decode('utf-8'))
+
+ :param response:
+ The response to format
+ :type response: :class:`requests.Response`
+ :param request_prefix: (*optional*)
+ Bytes to prefix each line of the request data
+ :type request_prefix: :class:`bytes`
+ :param response_prefix: (*optional*)
+ Bytes to prefix each line of the response data
+ :type response_prefix: :class:`bytes`
+ :param data_array: (*optional*)
+ Bytearray to which we append the request-response cycle data
+ :type data_array: :class:`bytearray`
+ :returns: Formatted bytes of request and response information.
+ :rtype: :class:`bytearray`
+ """
+ data = data_array if data_array is not None else bytearray()
+ prefixes = PrefixSettings(request_prefix, response_prefix)
+
+ if not hasattr(response, 'request'):
+ raise ValueError('Response has no associated request')
+
+ proxy_info = _get_proxy_information(response)
+ _dump_request_data(response.request, prefixes, data,
+ proxy_info=proxy_info)
+ _dump_response_data(response, prefixes, data)
+ return data
+
+
+def dump_all(response, request_prefix=b'< ', response_prefix=b'> '):
+ """Dump all requests and responses including redirects.
+
+ This takes the response returned by requests and will dump all
+ request-response pairs in the redirect history in order followed by the
+ final request-response.
+
+ Example::
+
+ import requests
+ from requests_toolbelt.utils import dump
+
+ resp = requests.get('https://httpbin.org/redirect/5')
+ data = dump.dump_all(resp)
+ print(data.decode('utf-8'))
+
+ :param response:
+ The response to format
+ :type response: :class:`requests.Response`
+ :param request_prefix: (*optional*)
+ Bytes to prefix each line of the request data
+ :type request_prefix: :class:`bytes`
+ :param response_prefix: (*optional*)
+ Bytes to prefix each line of the response data
+ :type response_prefix: :class:`bytes`
+ :returns: Formatted bytes of request and response information.
+ :rtype: :class:`bytearray`
+ """
+ data = bytearray()
+
+ history = list(response.history[:])
+ history.append(response)
+
+ for response in history:
+ dump_response(response, request_prefix, response_prefix, data)
+
+ return data
diff --git a/requests_toolbelt/utils/formdata.py b/requests_toolbelt/utils/formdata.py
new file mode 100644
index 0000000..b0a909d
--- /dev/null
+++ b/requests_toolbelt/utils/formdata.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+"""Implementation of nested form-data encoding function(s)."""
+from .._compat import basestring
+from .._compat import urlencode as _urlencode
+
+
+__all__ = ('urlencode',)
+
+
+def urlencode(query, *args, **kwargs):
+ """Handle nested form-data queries and serialize them appropriately.
+
+ There are times when a website expects a nested form data query to be sent
+ but, the standard library's urlencode function does not appropriately
+ handle the nested structures. In that case, you need this function which
+ will flatten the structure first and then properly encode it for you.
+
+ When using this to send data in the body of a request, make sure you
+ specify the appropriate Content-Type header for the request.
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.utils import formdata
+
+ query = {
+ 'my_dict': {
+ 'foo': 'bar',
+ 'biz': 'baz",
+ },
+ 'a': 'b',
+ }
+
+ resp = requests.get(url, params=formdata.urlencode(query))
+ # or
+ resp = requests.post(
+ url,
+ data=formdata.urlencode(query),
+ headers={
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ )
+
+ Similarly, you can specify a list of nested tuples, e.g.,
+
+ .. code-block:: python
+
+ import requests
+ from requests_toolbelt.utils import formdata
+
+ query = [
+ ('my_list', [
+ ('foo', 'bar'),
+ ('biz', 'baz'),
+ ]),
+ ('a', 'b'),
+ ]
+
+ resp = requests.get(url, params=formdata.urlencode(query))
+ # or
+ resp = requests.post(
+ url,
+ data=formdata.urlencode(query),
+ headers={
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ )
+
+ For additional parameter and return information, see the official
+ `urlencode`_ documentation.
+
+ .. _urlencode:
+ https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode
+ """
+ expand_classes = (dict, list, tuple)
+ original_query_list = _to_kv_list(query)
+
+ if not all(_is_two_tuple(i) for i in original_query_list):
+ raise ValueError("Expected query to be able to be converted to a "
+ "list comprised of length 2 tuples.")
+
+ query_list = original_query_list
+ while any(isinstance(v, expand_classes) for _, v in query_list):
+ query_list = _expand_query_values(query_list)
+
+ return _urlencode(query_list, *args, **kwargs)
+
+
+def _to_kv_list(dict_or_list):
+ if hasattr(dict_or_list, 'items'):
+ return list(dict_or_list.items())
+ return dict_or_list
+
+
+def _is_two_tuple(item):
+ return isinstance(item, (list, tuple)) and len(item) == 2
+
+
+def _expand_query_values(original_query_list):
+ query_list = []
+ for key, value in original_query_list:
+ if isinstance(value, basestring):
+ query_list.append((key, value))
+ else:
+ key_fmt = key + '[%s]'
+ value_list = _to_kv_list(value)
+ query_list.extend((key_fmt % k, v) for k, v in value_list)
+ return query_list
diff --git a/requests_toolbelt/utils/user_agent.py b/requests_toolbelt/utils/user_agent.py
new file mode 100644
index 0000000..e9636a4
--- /dev/null
+++ b/requests_toolbelt/utils/user_agent.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+import collections
+import platform
+import sys
+
+
+def user_agent(name, version, extras=None):
+ """Return an internet-friendly user_agent string.
+
+ The majority of this code has been wilfully stolen from the equivalent
+ function in Requests.
+
+ :param name: The intended name of the user-agent, e.g. "python-requests".
+ :param version: The version of the user-agent, e.g. "0.0.1".
+ :param extras: List of two-item tuples that are added to the user-agent
+ string.
+ :returns: Formatted user-agent string
+ :rtype: str
+ """
+ if extras is None:
+ extras = []
+
+ return UserAgentBuilder(
+ name, version
+ ).include_extras(
+ extras
+ ).include_implementation(
+ ).include_system().build()
+
+
+class UserAgentBuilder(object):
+ """Class to provide a greater level of control than :func:`user_agent`.
+
+ This is used by :func:`user_agent` to build its User-Agent string.
+
+ .. code-block:: python
+
+ user_agent_str = UserAgentBuilder(
+ name='requests-toolbelt',
+ version='17.4.0',
+ ).include_implementation(
+ ).include_system(
+ ).include_extras([
+ ('requests', '2.14.2'),
+ ('urllib3', '1.21.2'),
+ ]).build()
+
+ """
+
+ format_string = '%s/%s'
+
+ def __init__(self, name, version):
+ """Initialize our builder with the name and version of our user agent.
+
+ :param str name:
+ Name of our user-agent.
+ :param str version:
+ The version string for user-agent.
+ """
+ self._pieces = collections.deque([(name, version)])
+
+ def build(self):
+ """Finalize the User-Agent string.
+
+ :returns:
+ Formatted User-Agent string.
+ :rtype:
+ str
+ """
+ return " ".join([self.format_string % piece for piece in self._pieces])
+
+ def include_extras(self, extras):
+ """Include extra portions of the User-Agent.
+
+ :param list extras:
+ list of tuples of extra-name and extra-version
+ """
+ if any(len(extra) != 2 for extra in extras):
+ raise ValueError('Extras should be a sequence of two item tuples.')
+
+ self._pieces.extend(extras)
+ return self
+
+ def include_implementation(self):
+ """Append the implementation string to the user-agent string.
+
+ This adds the the information that you're using CPython 2.7.13 to the
+ User-Agent.
+ """
+ self._pieces.append(_implementation_tuple())
+ return self
+
+ def include_system(self):
+ """Append the information about the Operating System."""
+ self._pieces.append(_platform_tuple())
+ return self
+
+
+def _implementation_tuple():
+ """Return the tuple of interpreter name and version.
+
+ Returns a string that provides both the name and the version of the Python
+ implementation currently running. For example, on CPython 2.7.5 it will
+ return "CPython/2.7.5".
+
+ This function works best on CPython and PyPy: in particular, it probably
+ doesn't work for Jython or IronPython. Future investigation should be done
+ to work out the correct shape of the code for those platforms.
+ """
+ implementation = platform.python_implementation()
+
+ if implementation == 'CPython':
+ implementation_version = platform.python_version()
+ elif implementation == 'PyPy':
+ implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major,
+ sys.pypy_version_info.minor,
+ sys.pypy_version_info.micro)
+ if sys.pypy_version_info.releaselevel != 'final':
+ implementation_version = ''.join([
+ implementation_version, sys.pypy_version_info.releaselevel
+ ])
+ elif implementation == 'Jython':
+ implementation_version = platform.python_version() # Complete Guess
+ elif implementation == 'IronPython':
+ implementation_version = platform.python_version() # Complete Guess
+ else:
+ implementation_version = 'Unknown'
+
+ return (implementation, implementation_version)
+
+
+def _implementation_string():
+ return "%s/%s" % _implementation_tuple()
+
+
+def _platform_tuple():
+ try:
+ p_system = platform.system()
+ p_release = platform.release()
+ except IOError:
+ p_system = 'Unknown'
+ p_release = 'Unknown'
+ return (p_system, p_release)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..1e3eb36
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,7 @@
+[wheel]
+universal = 1
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ae77f53
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import sys
+
+from setuptools import setup
+
+if sys.argv[-1].lower() in ("submit", "publish"):
+ os.system("python setup.py bdist_wheel sdist upload")
+ sys.exit()
+
+
+def get_version():
+ version = ''
+ with open('requests_toolbelt/__init__.py', 'r') as fd:
+ reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
+ for line in fd:
+ m = reg.match(line)
+ if m:
+ version = m.group(1)
+ break
+ return version
+
+__version__ = get_version()
+
+if not __version__:
+ raise RuntimeError('Cannot find version information')
+
+
+packages = [
+ 'requests_toolbelt',
+ 'requests_toolbelt.adapters',
+ 'requests_toolbelt.auth',
+ 'requests_toolbelt.downloadutils',
+ 'requests_toolbelt.multipart',
+ 'requests_toolbelt.threaded',
+ 'requests_toolbelt.utils',
+]
+
+setup(
+ name="requests-toolbelt",
+ version=__version__,
+ description="A utility belt for advanced users of python-requests",
+ long_description="\n\n".join([open("README.rst").read(),
+ open("HISTORY.rst").read()]),
+ license='Apache 2.0',
+ author='Ian Cordasco, Cory Benfield',
+ author_email="graffatcolmingov@gmail.com",
+ url="https://toolbelt.readthedocs.org",
+ packages=packages,
+ package_data={'': ['LICENSE', 'AUTHORS.rst']},
+ include_package_data=True,
+ install_requires=['requests>=2.0.1,<3.0.0'],
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ ],
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..a4bf96a
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+import betamax
+
+
+def get_betamax(session):
+ return betamax.Betamax(
+ session,
+ cassette_library_dir='tests/cassettes')
diff --git a/tests/cassettes/http2bin_cookies.json b/tests/cassettes/http2bin_cookies.json
new file mode 100644
index 0000000..a8368ed
--- /dev/null
+++ b/tests/cassettes/http2bin_cookies.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.5.1", "http_interactions": [{"response": {"status": {"code": 302, "message": "FOUND"}, "body": {"string": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<title>Redirecting...</title>\n<h1>Redirecting...</h1>\n<p>You should be redirected automatically to target URL: <a href=\"/cookies\">/cookies</a>. If not click the link.", "encoding": "utf-8"}, "url": "https://httpbin.org/cookies/set?cookie0=value0", "headers": {"Location": ["/cookies"], "Content-Length": ["223"], "Date": ["Fri, 13 Nov 2015 00:23:20 GMT"], "Access-Control-Allow-Credentials": ["true"], "Access-Control-Allow-Origin": ["*"], "Connection": ["keep-alive"], "Server": ["nginx"], "Set-Cookie": ["cookie0=value0; Path=/"], "Content-Type": ["text/html; charset=utf-8"]}}, "recorded_at": "2015-11-13T00:23:19", "request": {"uri": "https://httpbin.org/cookies/set?cookie0=value0", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "User-Agent": ["python-requests/2.8.1"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"]}}}, {"response": {"status": {"code": 200, "message": "OK"}, "body": {"string": "{\n \"cookies\": {\n \"cookie0\": \"value0\"\n }\n}\n", "encoding": null}, "url": "https://httpbin.org/cookies", "headers": {"Access-Control-Allow-Credentials": ["true"], "Content-Length": ["47"], "Date": ["Fri, 13 Nov 2015 00:23:20 GMT"], "Content-Type": ["application/json"], "Connection": ["keep-alive"], "Server": ["nginx"], "Access-Control-Allow-Origin": ["*"]}}, "recorded_at": "2015-11-13T00:23:19", "request": {"uri": "https://httpbin.org/cookies", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "User-Agent": ["python-requests/2.8.1"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "Cookie": ["cookie0=value0"]}}}]} \ No newline at end of file
diff --git a/tests/cassettes/http2bin_fingerprint.json b/tests/cassettes/http2bin_fingerprint.json
new file mode 100644
index 0000000..fd2fd35
--- /dev/null
+++ b/tests/cassettes/http2bin_fingerprint.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.4.1", "http_interactions": [{"response": {"status": {"message": "OK", "code": 200}, "body": {"string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Connection\": \"keep-alive\", \n \"Host\": \"http2bin.org\", \n \"User-Agent\": \"python-requests/2.5.3 CPython/2.7.9 Darwin/14.1.0\"\n }, \n \"origin\": \"77.99.146.203\", \n \"url\": \"https://http2bin.org/get\"\n}\n", "encoding": null}, "headers": {"access-control-allow-origin": ["*"], "date": ["Tue, 03 Mar 2015 21:29:55 GMT"], "server": ["h2o/1.0.2-alpha1"], "content-length": ["301"], "access-control-allow-credentials": ["true"], "connection": ["keep-alive"], "content-type": ["application/json"]}, "url": "https://http2bin.org/get"}, "recorded_at": "2015-03-03T21:29:55", "request": {"method": "GET", "uri": "https://http2bin.org/get", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"], "User-Agent": ["python-requests/2.5.3 CPython/2.7.9 Darwin/14.1.0"]}}}]} \ No newline at end of file
diff --git a/tests/cassettes/httpbin_guess_auth_basic.json b/tests/cassettes/httpbin_guess_auth_basic.json
new file mode 100644
index 0000000..db72722
--- /dev/null
+++ b/tests/cassettes/httpbin_guess_auth_basic.json
@@ -0,0 +1 @@
+{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate, compress"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH"]}, "method": "GET", "uri": "http://httpbin.org/basic-auth/user/passwd"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"content-length": ["0"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Sat, 03 May 2014 17:23:06 GMT"], "access-control-allow-origin": ["*"], "www-authenticate": ["Basic realm=\"Fake Realm\""]}, "status": {"message": "UNAUTHORIZED", "code": 401}, "url": "http://httpbin.org/basic-auth/user/passwd"}, "recorded_at": "2014-05-03T17:23:06"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate, compress"], "Authorization": ["Basic dXNlcjpwYXNzd2Q="], "User-Agent": ["python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH"]}, "method": "GET", "uri": "http://httpbin.org/basic-auth/user/passwd"}, "response": {"body": {"string": "{\n \"user\": \"user\",\n \"authenticated\": true\n}", "encoding": null}, "headers": {"content-length": ["45"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Sat, 03 May 2014 17:23:06 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/basic-auth/user/passwd"}, "recorded_at": "2014-05-03T17:23:06"}], "recorded_with": "betamax/{version}"} \ No newline at end of file
diff --git a/tests/cassettes/httpbin_guess_auth_digest.json b/tests/cassettes/httpbin_guess_auth_digest.json
new file mode 100644
index 0000000..94cf3a3
--- /dev/null
+++ b/tests/cassettes/httpbin_guess_auth_digest.json
@@ -0,0 +1 @@
+{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate, compress"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH"]}, "method": "GET", "uri": "http://httpbin.org/digest-auth/auth/user/passwd"}, "response": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"content-length": ["0"], "set-cookie": ["fake=fake_value"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Sat, 03 May 2014 17:23:07 GMT"], "access-control-allow-origin": ["*"], "content-type": ["text/html; charset=utf-8"], "www-authenticate": ["Digest qop=auth, nonce=\"713b4eb6d0ad0ac25d75b50c4d044d5e\", realm=\"me@kennethreitz.com\", opaque=\"d0033bc1960ca78a2fc4497c1e8a8cbd\""]}, "status": {"message": "UNAUTHORIZED", "code": 401}, "url": "http://httpbin.org/digest-auth/auth/user/passwd"}, "recorded_at": "2014-05-03T17:23:07"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Cookie": ["fake=fake_value"], "Accept-Encoding": ["gzip, deflate, compress"], "Authorization": ["Digest username=\"user\", realm=\"me@kennethreitz.com\", nonce=\"713b4eb6d0ad0ac25d75b50c4d044d5e\", uri=\"/digest-auth/auth/user/passwd\", response=\"30276b25ef0031e65e3bccc719031388\", opaque=\"d0033bc1960ca78a2fc4497c1e8a8cbd\", qop=\"auth\", nc=00000001, cnonce=\"e94e00be64d66bcb\""], "User-Agent": ["python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH"]}, "method": "GET", "uri": "http://httpbin.org/digest-auth/auth/user/passwd"}, "response": {"body": {"string": "{\n \"user\": \"user\",\n \"authenticated\": true\n}", "encoding": null}, "headers": {"content-length": ["45"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Sat, 03 May 2014 17:23:07 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/digest-auth/auth/user/passwd"}, "recorded_at": "2014-05-03T17:23:07"}], "recorded_with": "betamax/{version}"} \ No newline at end of file
diff --git a/tests/cassettes/httpbin_guess_auth_none.json b/tests/cassettes/httpbin_guess_auth_none.json
new file mode 100644
index 0000000..2ebbb0f
--- /dev/null
+++ b/tests/cassettes/httpbin_guess_auth_none.json
@@ -0,0 +1 @@
+{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate, compress"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH"]}, "method": "GET", "uri": "http://httpbin.org/get?a=1"}, "response": {"body": {"string": "{\n \"args\": {\n \"a\": \"1\"\n },\n \"url\": \"http://httpbin.org/get?a=1\",\n \"headers\": {\n \"Connection\": \"close\",\n \"Host\": \"httpbin.org\",\n \"Accept-Encoding\": \"gzip, deflate, compress\",\n \"X-Request-Id\": \"f9f71f12-5705-4a0f-85d4-3d63f9140b1f\",\n \"User-Agent\": \"python-requests/2.2.1 CPython/2.7.6 Linux/3.14.1-1-ARCH\",\n \"Accept\": \"*/*\"\n },\n \"origin\": \"62.47.252.115\"\n}", "encoding": null}, "headers": {"content-length": ["381"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Sat, 03 May 2014 17:23:07 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/get?a=1"}, "recorded_at": "2014-05-03T17:23:07"}], "recorded_with": "betamax/{version}"} \ No newline at end of file
diff --git a/tests/cassettes/klevas_vu_lt_ssl3.json b/tests/cassettes/klevas_vu_lt_ssl3.json
new file mode 100644
index 0000000..3c667ba
--- /dev/null
+++ b/tests/cassettes/klevas_vu_lt_ssl3.json
@@ -0,0 +1 @@
+{"http_interactions": [{"request": {"body": "", "headers": {"Accept-Encoding": "gzip, deflate, compress", "Accept": "*/*", "User-Agent": "python-requests/2.1.0 CPython/2.7.3 Linux/3.2.29"}, "method": "GET", "uri": "https://klevas.vu.lt/"}, "response": {"body": {"string": "<html>\n<title>\nKlevas\n</title>\n<head>\n<script language=\"javascript\" type=\"text/javascript\">\n <!--\n window.location=\"https://klevas.vu.lt/pls/klevas/logon\";\n // -->\n </script>\n</head>\n</html>\n\n", "encoding": "ISO-8859-1"}, "headers": {"content-length": "204", "accept-ranges": "bytes", "server": "Oracle-Application-Server-10g/10.1.3.1.0 Oracle-HTTP-Server", "last-modified": "Wed, 13 Apr 2011 05:00:23 GMT", "etag": "\"7f9b-cc-4da52de7\"", "date": "Sun, 05 Jan 2014 01:35:40 GMT", "content-type": "text/html"}, "url": "https://klevas.vu.lt/", "status_code": 200}, "recorded_at": "2014-01-05T01:34:40"}], "recorded_with": "betamax"} \ No newline at end of file
diff --git a/tests/cassettes/redirect_request_for_dump_all.json b/tests/cassettes/redirect_request_for_dump_all.json
new file mode 100644
index 0000000..38b037d
--- /dev/null
+++ b/tests/cassettes/redirect_request_for_dump_all.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.5.1", "http_interactions": [{"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/redirect/5", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/redirect/5", "status": {"code": 302, "message": "FOUND"}, "body": {"string": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<title>Redirecting...</title>\n<h1>Redirecting...</h1>\n<p>You should be redirected automatically to target URL: <a href=\"/relative-redirect/4\">/relative-redirect/4</a>. If not click the link.", "encoding": "utf-8"}, "headers": {"Location": "/relative-redirect/4", "Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "247", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "text/html; charset=utf-8"}}}, {"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/relative-redirect/4", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/relative-redirect/4", "status": {"code": 302, "message": "FOUND"}, "body": {"string": "", "encoding": "utf-8"}, "headers": {"Location": "/relative-redirect/3", "Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "0", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "text/html; charset=utf-8"}}}, {"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/relative-redirect/3", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/relative-redirect/3", "status": {"code": 302, "message": "FOUND"}, "body": {"string": "", "encoding": "utf-8"}, "headers": {"Location": "/relative-redirect/2", "Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "0", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "text/html; charset=utf-8"}}}, {"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/relative-redirect/2", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/relative-redirect/2", "status": {"code": 302, "message": "FOUND"}, "body": {"string": "", "encoding": "utf-8"}, "headers": {"Location": "/relative-redirect/1", "Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "0", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "text/html; charset=utf-8"}}}, {"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/relative-redirect/1", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/relative-redirect/1", "status": {"code": 302, "message": "FOUND"}, "body": {"string": "", "encoding": "utf-8"}, "headers": {"Location": "/get", "Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "0", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "text/html; charset=utf-8"}}}, {"recorded_at": "2015-11-14T22:53:20", "request": {"uri": "https://httpbin.org/get", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": "keep-alive", "Accept": "*/*", "User-Agent": "python-requests/2.8.1", "Accept-Encoding": "gzip, deflate"}}, "response": {"url": "https://httpbin.org/get", "status": {"code": 200, "message": "OK"}, "body": {"string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"python-requests/2.8.1\"\n }, \n \"origin\": \"<IPADDR>\", \n \"url\": \"https://httpbin.org/get\"\n}\n", "encoding": null}, "headers": {"Access-Control-Allow-Credentials": "true", "Server": "nginx", "Date": "Sat, 14 Nov 2015 22:53:18 GMT", "Content-Length": "239", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", "Content-Type": "application/json"}}}]} \ No newline at end of file
diff --git a/tests/cassettes/simple_get_request.json b/tests/cassettes/simple_get_request.json
new file mode 100644
index 0000000..a61fb5f
--- /dev/null
+++ b/tests/cassettes/simple_get_request.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.5.1", "http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "uri": "https://httpbin.org/get", "headers": {"Connection": ["keep-alive"], "User-Agent": ["python-requests/2.8.1"], "Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"]}, "method": "GET"}, "recorded_at": "2015-11-14T22:33:32", "response": {"status": {"code": 200, "message": "OK"}, "url": "https://httpbin.org/get", "body": {"encoding": null, "string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"python-requests/2.8.1\"\n }, \n \"origin\": \"<IPADDR>\", \n \"url\": \"https://httpbin.org/get\"\n}\n"}, "headers": {"Content-Type": ["application/json"], "Date": ["Sat, 14 Nov 2015 22:33:30 GMT"], "Connection": ["keep-alive"], "Server": ["nginx"], "Access-Control-Allow-Credentials": ["true"], "Content-Length": ["239"], "Access-Control-Allow-Origin": ["*"]}}}]}
diff --git a/tests/cassettes/stream_response_to_file.json b/tests/cassettes/stream_response_to_file.json
new file mode 100644
index 0000000..9807e41
--- /dev/null
+++ b/tests/cassettes/stream_response_to_file.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.4.1", "http_interactions": [{"request": {"uri": "https://api.github.com/repos/sigmavirus24/github3.py/releases/assets/37944", "method": "GET", "headers": {"Accept": ["application/octet-stream"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"], "User-Agent": ["python-requests/2.5.3 CPython/2.7.9 Darwin/14.1.0"]}, "body": {"base64_string": "", "encoding": "utf-8"}}, "response": {"status": {"code": 302, "message": "Found"}, "url": "https://api.github.com/repos/sigmavirus24/github3.py/releases/assets/37944", "headers": {"access-control-allow-credentials": ["true"], "x-xss-protection": ["1; mode=block"], "vary": ["Accept-Encoding"], "location": ["https://s3.amazonaws.com/github-cloud/releases/3710711/365425c2-4e46-11e3-86fb-bb0d50a886e7.whl?response-content-disposition=attachment%3B%20filename%3Dgithub3.py-0.7.1-py2.py3-none-any.whl&response-content-type=application/octet-stream&AWSAccessKeyId=AKIAISTNZFOVBIJMK3TQ&Expires=1426166613&Signature=78anFgNgXLm3TIbo%2FbTEEk7m%2F34%3D"], "x-content-type-options": ["nosniff"], "content-security-policy": ["default-src 'none'"], "x-ratelimit-limit": ["60"], "content-length": ["0"], "status": ["302 Found"], "x-frame-options": ["deny"], "x-served-by": ["8dd185e423974a7e13abbbe6e060031e"], "server": ["GitHub.com"], "access-control-allow-origin": ["*"], "strict-transport-security": ["max-age=31536000; includeSubdomains; preload"], "x-github-request-id": ["48A0C951:54E7:48B5311:55019319"], "date": ["Thu, 12 Mar 2015 13:22:33 GMT"], "access-control-expose-headers": ["ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "x-ratelimit-remaining": ["58"], "content-type": ["text/html;charset=utf-8"], "x-ratelimit-reset": ["1426170017"]}, "body": {"base64_string": "", "encoding": "utf-8"}}, "recorded_at": "2015-03-12T13:22:33"}, {"request": {"uri": "https://s3.amazonaws.com/github-cloud/releases/3710711/365425c2-4e46-11e3-86fb-bb0d50a886e7.whl?response-content-disposition=attachment%3B%20filename%3Dgithub3.py-0.7.1-py2.py3-none-any.whl&response-content-type=application/octet-stream&AWSAccessKeyId=AKIAISTNZFOVBIJMK3TQ&Expires=1426166613&Signature=78anFgNgXLm3TIbo%2FbTEEk7m%2F34%3D", "method": "GET", "headers": {"Accept": ["application/octet-stream"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"], "User-Agent": ["python-requests/2.5.3 CPython/2.7.9 Darwin/14.1.0"]}, "body": {"base64_string": "", "encoding": "utf-8"}}, "response": {"status": {"code": 200, "message": "OK"}, "url": "https://s3.amazonaws.com/github-cloud/releases/3710711/365425c2-4e46-11e3-86fb-bb0d50a886e7.whl?response-content-disposition=attachment%3B%20filename%3Dgithub3.py-0.7.1-py2.py3-none-any.whl&response-content-type=application/octet-stream&AWSAccessKeyId=AKIAISTNZFOVBIJMK3TQ&Expires=1426166613&Signature=78anFgNgXLm3TIbo%2FbTEEk7m%2F34%3D", "headers": {"accept-ranges": ["bytes"], "content-disposition": ["attachment; filename=github3.py-0.7.1-py2.py3-none-any.whl"], "x-amz-id-2": ["9+TuHhbd7y2BUJaEV+mFpaDgjl1g9uSAPiZxwc6b2cYydhlhZSyKSuB7PQyiPBPD"], "content-length": ["117140"], "x-amz-meta-surrogate-key": ["repository-3710711 user-240830"], "x-amz-request-id": ["4B4BFE6BF5135B8D"], "last-modified": ["Fri, 15 Nov 2013 22:35:23 GMT"], "x-amz-meta-surrogate-control": ["max-age=31557600"], "etag": ["\"6550854f02f7bf10b944070b84f38313\""], "date": ["Thu, 12 Mar 2015 13:22:35 GMT"], "cache-control": ["max-age=31557600"], "content-type": ["application/octet-stream"], "server": ["AmazonS3"]}, "body": {"base64_string": "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", "encoding": null}}, "recorded_at": "2015-03-12T13:22:34"}]} \ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..41ee1b1
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+
+import betamax
+
+sys.path.insert(0, '.')
+
+placeholders = {
+ '<IPADDR>': os.environ.get('IPADDR', '127.0.0.1'),
+}
+
+with betamax.Betamax.configure() as config:
+ for placeholder, value in placeholders.items():
+ config.define_cassette_placeholder(placeholder, value)
diff --git a/tests/test_appengine_adapter.py b/tests/test_appengine_adapter.py
new file mode 100644
index 0000000..8cd802e
--- /dev/null
+++ b/tests/test_appengine_adapter.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""Tests for the AppEngineAdapter."""
+import sys
+
+import mock
+import pytest
+import requests
+
+from requests_toolbelt import exceptions as exc
+
+REQUESTS_SUPPORTS_GAE = requests.__build__ >= 0x021000
+
+if REQUESTS_SUPPORTS_GAE:
+ from requests.packages.urllib3.contrib import appengine as urllib3_appeng
+ from requests_toolbelt.adapters import appengine
+else:
+ appengine = urllib3_appeng = None
+
+
+@pytest.mark.skipif(sys.version_info >= (3,),
+ reason="App Engine doesn't support Python 3 (yet) and "
+ "urllib3's appengine contrib code is Python 2 "
+ "only. Until the latter changes, this test will "
+ "be skipped, unfortunately.")
+@pytest.mark.skipif(not REQUESTS_SUPPORTS_GAE,
+ reason="Requires Requests v2.10.0 or later")
+@mock.patch.object(urllib3_appeng, 'urlfetch')
+def test_get(mock_urlfetch):
+ """Tests a simple requests.get() call.
+
+ App Engine urlfetch docs:
+ https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api.urlfetch
+ """
+ response = mock.Mock(status_code=200, content='asdf', headers={})
+ mock_urlfetch.fetch = mock.Mock(return_value=response)
+
+ session = requests.Session()
+ session.mount('http://', appengine.AppEngineAdapter())
+ resp = session.get('http://url/', timeout=9, headers={'Foo': 'bar'})
+ assert resp.status_code == 200
+ assert resp.content == 'asdf'
+
+ args, kwargs = mock_urlfetch.fetch.call_args
+ assert args == ('http://url/',)
+ assert kwargs['deadline'] == 9
+ assert kwargs['headers']['Foo'] == 'bar'
+
+
+@pytest.mark.skipif(sys.version_info >= (3,),
+ reason="App Engine doesn't support Python 3 (yet) and "
+ "urllib3's appengine contrib code is Python 2 "
+ "only. Until the latter changes, this test will "
+ "be skipped, unfortunately.")
+@pytest.mark.skipif(not REQUESTS_SUPPORTS_GAE,
+ reason="Requires Requests v2.10.0 or later")
+def test_appengine_monkeypatch():
+ """Tests monkeypatching Requests adapters for AppEngine compatibility.
+ """
+ adapter = requests.sessions.HTTPAdapter
+
+ appengine.monkeypatch()
+
+ assert requests.sessions.HTTPAdapter == appengine.AppEngineAdapter
+ assert requests.adapters.HTTPAdapter == appengine.AppEngineAdapter
+
+ appengine.monkeypatch(validate_certificate=False)
+
+ assert requests.sessions.HTTPAdapter == appengine.InsecureAppEngineAdapter
+ assert requests.adapters.HTTPAdapter == appengine.InsecureAppEngineAdapter
+
+ requests.sessions.HTTPAdapter = adapter
+ requests.adapters.HTTPAdapter = adapter
+
+
+@pytest.mark.skipif(sys.version_info >= (3,),
+ reason="App Engine doesn't support Python 3 (yet) and "
+ "urllib3's appengine contrib code is Python 2 "
+ "only. Until the latter changes, this test will "
+ "be skipped, unfortunately.")
+@pytest.mark.skipif(not REQUESTS_SUPPORTS_GAE,
+ reason="Requires Requests v2.10.0 or later")
+@mock.patch.object(urllib3_appeng, 'urlfetch')
+def test_insecure_appengine_adapter(mock_urlfetch):
+ adapter = appengine.InsecureAppEngineAdapter()
+
+ assert not adapter._validate_certificate
+
+ with pytest.warns(exc.IgnoringGAECertificateValidation):
+ adapter = appengine.InsecureAppEngineAdapter(validate_certificate=True)
diff --git a/tests/test_auth.py b/tests/test_auth.py
new file mode 100644
index 0000000..9cc6d35
--- /dev/null
+++ b/tests/test_auth.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+import requests
+import unittest
+import mock
+
+from requests_toolbelt.auth.guess import GuessAuth, GuessProxyAuth
+from . import get_betamax
+
+
+class TestGuessAuth(unittest.TestCase):
+ def setUp(self):
+ self.session = requests.Session()
+ self.recorder = get_betamax(self.session)
+
+ def cassette(self, name):
+ return self.recorder.use_cassette(
+ 'httpbin_guess_auth_' + name,
+ match_requests_on=['method', 'uri', 'digest-auth']
+ )
+
+ def test_basic(self):
+ with self.cassette('basic'):
+ r = self.session.request(
+ 'GET', 'http://httpbin.org/basic-auth/user/passwd',
+ auth=GuessAuth('user', 'passwd'))
+
+ assert r.json() == {'authenticated': True, 'user': 'user'}
+
+ def test_digest(self):
+ with self.cassette('digest'):
+ r = self.session.request(
+ 'GET', 'http://httpbin.org/digest-auth/auth/user/passwd',
+ auth=GuessAuth('user', 'passwd'))
+
+ assert r.json() == {'authenticated': True, 'user': 'user'}
+
+ def test_no_auth(self):
+ with self.cassette('none'):
+ url = 'http://httpbin.org/get?a=1'
+ r = self.session.request('GET', url,
+ auth=GuessAuth('user', 'passwd'))
+
+ j = r.json()
+ assert j['args'] == {'a': '1'}
+ assert j['url'] == url
+ assert 'user' not in r.text
+ assert 'passwd' not in r.text
+
+
+class TestGuessProxyAuth(unittest.TestCase):
+
+ @mock.patch('requests_toolbelt.auth.http_proxy_digest.HTTPProxyDigestAuth.handle_407')
+ def test_handle_407_header_digest(self, mock_handle_407):
+ r = requests.Response()
+ r.headers['Proxy-Authenticate'] = 'Digest nonce="d2b19757d3d656a283c99762cbd1097b", opaque="1c311ad1cc6e6183b83bc75f95a57893", realm="me@kennethreitz.com", qop=auth'
+
+ guess_auth = GuessProxyAuth(None, None, "user", "passwd")
+ guess_auth.handle_407(r)
+
+ mock_handle_407.assert_called_with(r)
+
+ @mock.patch('requests.auth.HTTPProxyAuth.__call__')
+ @mock.patch('requests.cookies.extract_cookies_to_jar')
+ def test_handle_407_header_basic(self, extract_cookies_to_jar, proxy_auth_call):
+ req = mock.Mock()
+ r = mock.Mock()
+ r.headers = dict()
+ r.request.copy.return_value = req
+
+ proxy_auth_call.return_value = requests.Response()
+
+ kwargs = {}
+ r.headers['Proxy-Authenticate'] = 'Basic realm="Fake Realm"'
+ guess_auth = GuessProxyAuth(None, None, "user", "passwd")
+ guess_auth.handle_407(r, *kwargs)
+
+ proxy_auth_call.assert_called_with(req)
diff --git a/tests/test_auth_handler.py b/tests/test_auth_handler.py
new file mode 100644
index 0000000..6ddade7
--- /dev/null
+++ b/tests/test_auth_handler.py
@@ -0,0 +1,58 @@
+import requests
+from requests.auth import HTTPBasicAuth
+from requests_toolbelt.auth.handler import AuthHandler
+from requests_toolbelt.auth.handler import NullAuthStrategy
+
+
+def test_turns_tuples_into_basic_auth():
+ a = AuthHandler({'http://example.com': ('foo', 'bar')})
+ strategy = a.get_strategy_for('http://example.com')
+ assert not isinstance(strategy, NullAuthStrategy)
+ assert isinstance(strategy, HTTPBasicAuth)
+
+
+def test_uses_null_strategy_for_non_matching_domains():
+ a = AuthHandler({'http://api.example.com': ('foo', 'bar')})
+ strategy = a.get_strategy_for('http://example.com')
+ assert isinstance(strategy, NullAuthStrategy)
+
+
+def test_normalizes_domain_keys():
+ a = AuthHandler({'https://API.github.COM': ('foo', 'bar')})
+ assert 'https://api.github.com' in a.strategies
+ assert 'https://API.github.COM' not in a.strategies
+
+
+def test_can_add_new_strategies():
+ a = AuthHandler({'https://example.com': ('foo', 'bar')})
+ a.add_strategy('https://api.github.com', ('fiz', 'baz'))
+ assert isinstance(
+ a.get_strategy_for('https://api.github.com'),
+ HTTPBasicAuth
+ )
+
+
+def test_prepares_auth_correctly():
+ # Set up our Session and AuthHandler
+ auth = AuthHandler({
+ 'https://api.example.com': ('bar', 'baz'),
+ 'https://httpbin.org': ('biz', 'fiz'),
+ })
+ s = requests.Session()
+ s.auth = auth
+ # Set up a valid GET request to https://api.example.com/users
+ r1 = requests.Request('GET', 'https://api.example.com/users')
+ p1 = s.prepare_request(r1)
+ assert p1.headers['Authorization'] == 'Basic YmFyOmJheg=='
+
+ # Set up a valid POST request to https://httpbin.org/post
+ r2 = requests.Request('POST', 'https://httpbin.org/post', data='foo')
+ p2 = s.prepare_request(r2)
+ assert p2.headers['Authorization'] == 'Basic Yml6OmZpeg=='
+
+ # Set up an *invalid* OPTIONS request to http://api.example.com
+ # NOTE(sigmavirus24): This is not because of the verb but instead because
+ # it is the wrong URI scheme.
+ r3 = requests.Request('OPTIONS', 'http://api.example.com/projects')
+ p3 = s.prepare_request(r3)
+ assert p3.headers.get('Authorization') is None
diff --git a/tests/test_downloadutils.py b/tests/test_downloadutils.py
new file mode 100644
index 0000000..7058939
--- /dev/null
+++ b/tests/test_downloadutils.py
@@ -0,0 +1,220 @@
+"""Tests for the utils module."""
+import io
+import os
+import os.path
+import shutil
+import tempfile
+
+import requests
+from requests_toolbelt.downloadutils import stream
+from requests_toolbelt.downloadutils import tee
+import mock
+import pytest
+
+from . import get_betamax
+
+
+preserve_bytes = {'preserve_exact_body_bytes': True}
+
+
+def test_get_download_file_path_uses_content_disposition():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ filename = 'github3.py-0.7.1-py2.py3-none-any.whl'
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'})
+ path = stream.get_download_file_path(r, None)
+ r.close()
+ assert path == filename
+
+def test_get_download_file_path_directory():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ filename = 'github3.py-0.7.1-py2.py3-none-any.whl'
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'})
+ path = stream.get_download_file_path(r, tempfile.tempdir)
+ r.close()
+ assert path == os.path.join(tempfile.tempdir, filename)
+
+
+def test_get_download_file_path_specific_file():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'})
+ path = stream.get_download_file_path(r, '/arbitrary/file.path')
+ r.close()
+ assert path == '/arbitrary/file.path'
+
+
+def test_stream_response_to_file_uses_content_disposition():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ filename = 'github3.py-0.7.1-py2.py3-none-any.whl'
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ stream.stream_response_to_file(r)
+
+ assert os.path.exists(filename)
+ os.unlink(filename)
+
+
+def test_stream_response_to_specific_filename():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ filename = 'github3.py.whl'
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ stream.stream_response_to_file(r, path=filename)
+
+ assert os.path.exists(filename)
+ os.unlink(filename)
+
+
+def test_stream_response_to_directory():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+
+ td = tempfile.mkdtemp()
+ try:
+ filename = 'github3.py-0.7.1-py2.py3-none-any.whl'
+ expected_path = os.path.join(td, filename)
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ stream.stream_response_to_file(r, path=td)
+
+ assert os.path.exists(expected_path)
+ finally:
+ shutil.rmtree(td)
+
+
+def test_stream_response_to_existing_file():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ filename = 'github3.py.whl'
+ with open(filename, 'w') as f_existing:
+ f_existing.write('test')
+
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ try:
+ stream.stream_response_to_file(r, path=filename)
+ except stream.exc.StreamingError as e:
+ assert str(e).startswith('File already exists:')
+ else:
+ assert False, "Should have raised a FileExistsError"
+ finally:
+ os.unlink(filename)
+
+
+def test_stream_response_to_file_like_object():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+ file_obj = io.BytesIO()
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ stream.stream_response_to_file(r, path=file_obj)
+
+ assert 0 < file_obj.tell()
+
+
+def test_stream_response_to_file_chunksize():
+ s = requests.Session()
+ recorder = get_betamax(s)
+ url = ('https://api.github.com/repos/sigmavirus24/github3.py/releases/'
+ 'assets/37944')
+
+ class FileWrapper(io.BytesIO):
+ def __init__(self):
+ super(FileWrapper, self).__init__()
+ self.chunk_sizes = []
+
+ def write(self, data):
+ self.chunk_sizes.append(len(data))
+ return super(FileWrapper, self).write(data)
+
+ file_obj = FileWrapper()
+
+ chunksize = 1231
+
+ with recorder.use_cassette('stream_response_to_file', **preserve_bytes):
+ r = s.get(url, headers={'Accept': 'application/octet-stream'},
+ stream=True)
+ stream.stream_response_to_file(r, path=file_obj, chunksize=chunksize)
+
+ assert 0 < file_obj.tell()
+
+ assert len(file_obj.chunk_sizes) >= 1
+ assert file_obj.chunk_sizes[0] == chunksize
+
+
+@pytest.fixture
+def streamed_response(chunks=None):
+ chunks = chunks or [b'chunk'] * 8
+ response = mock.MagicMock()
+ response.raw.stream.return_value = chunks
+ return response
+
+
+def test_tee(streamed_response):
+ response = streamed_response
+ expected_len = len('chunk') * 8
+ fileobject = io.BytesIO()
+ assert expected_len == sum(len(c) for c in tee.tee(response, fileobject))
+ assert fileobject.getvalue() == b'chunkchunkchunkchunkchunkchunkchunkchunk'
+
+
+def test_tee_rejects_StringIO():
+ fileobject = io.StringIO()
+ with pytest.raises(TypeError):
+ # The generator needs to be iterated over before the exception will be
+ # raised
+ sum(len(c) for c in tee.tee(None, fileobject))
+
+
+def test_tee_to_file(streamed_response):
+ response = streamed_response
+ expected_len = len('chunk') * 8
+ assert expected_len == sum(
+ len(c) for c in tee.tee_to_file(response, 'tee.txt')
+ )
+ assert os.path.exists('tee.txt')
+ os.remove('tee.txt')
+
+
+def test_tee_to_bytearray(streamed_response):
+ response = streamed_response
+ arr = bytearray()
+ expected_arr = bytearray(b'chunk' * 8)
+ expected_len = len(expected_arr)
+ assert expected_len == sum(
+ len(c) for c in tee.tee_to_bytearray(response, arr)
+ )
+ assert expected_arr == arr
+
+
+def test_tee_to_bytearray_only_accepts_bytearrays():
+ with pytest.raises(TypeError):
+ tee.tee_to_bytearray(None, object())
diff --git a/tests/test_dump.py b/tests/test_dump.py
new file mode 100644
index 0000000..f6b3a41
--- /dev/null
+++ b/tests/test_dump.py
@@ -0,0 +1,382 @@
+"""Collection of tests for utils.dump.
+
+The dump utility module only has two public attributes:
+
+- dump_response
+- dump_all
+
+This module, however, tests many of the private implementation details since
+those public functions just wrap them and testing the public functions will be
+very complex and high-level.
+"""
+from requests_toolbelt._compat import HTTPHeaderDict
+from requests_toolbelt.utils import dump
+
+import mock
+import pytest
+import requests
+
+from . import get_betamax
+
+HTTP_1_1 = 11
+HTTP_1_0 = 10
+HTTP_0_9 = 9
+HTTP_UNKNOWN = 5000
+
+
+class TestSimplePrivateFunctions(object):
+
+ """Excercise simple private functions in one logical place."""
+
+ def test_coerce_to_bytes_skips_byte_strings(self):
+ """Show that _coerce_to_bytes skips bytes input."""
+ bytestr = b'some bytes'
+ assert dump._coerce_to_bytes(bytestr) is bytestr
+
+ def test_coerce_to_bytes_converts_text(self):
+ """Show that _coerce_to_bytes handles text input."""
+ bytestr = b'some bytes'
+ text = bytestr.decode('utf-8')
+ assert dump._coerce_to_bytes(text) == bytestr
+
+ def test_format_header(self):
+ """Prove that _format_header correctly formats bytes input."""
+ header = b'Connection'
+ value = b'close'
+ expected = b'Connection: close\r\n'
+ assert dump._format_header(header, value) == expected
+
+ def test_format_header_handles_unicode(self):
+ """Prove that _format_header correctly formats text input."""
+ header = b'Connection'.decode('utf-8')
+ value = b'close'.decode('utf-8')
+ expected = b'Connection: close\r\n'
+ assert dump._format_header(header, value) == expected
+
+ def test_build_request_path(self):
+ """Show we get the right request path for a normal request."""
+ path, _ = dump._build_request_path(
+ 'https://example.com/foo/bar', {}
+ )
+ assert path == b'/foo/bar'
+
+ def test_build_request_path_with_query_string(self):
+ """Show we include query strings appropriately."""
+ path, _ = dump._build_request_path(
+ 'https://example.com/foo/bar?query=data', {}
+ )
+ assert path == b'/foo/bar?query=data'
+
+ def test_build_request_path_with_proxy_info(self):
+ """Show that we defer to the proxy request_path info."""
+ path, _ = dump._build_request_path(
+ 'https://example.com/', {
+ 'request_path': b'https://example.com/test'
+ }
+ )
+ assert path == b'https://example.com/test'
+
+
+class RequestResponseMixin(object):
+
+ """Mix-in for test classes needing mocked requests and responses."""
+
+ response_spec = [
+ 'connection',
+ 'content',
+ 'raw',
+ 'reason',
+ 'request',
+ 'url',
+ ]
+
+ request_spec = [
+ 'body',
+ 'headers',
+ 'method',
+ 'url',
+ ]
+
+ httpresponse_spec = [
+ 'headers',
+ 'reason',
+ 'status',
+ 'version',
+ ]
+
+ adapter_spec = [
+ 'proxy_manager',
+ ]
+
+ @pytest.fixture(autouse=True)
+ def set_up(self):
+ """xUnit style autoused fixture creating mocks."""
+ self.response = mock.Mock(spec=self.response_spec)
+ self.request = mock.Mock(spec=self.request_spec)
+ self.httpresponse = mock.Mock(spec=self.httpresponse_spec)
+ self.adapter = mock.Mock(spec=self.adapter_spec)
+
+ self.response.connection = self.adapter
+ self.response.request = self.request
+ self.response.raw = self.httpresponse
+
+ def configure_response(self, content=b'', proxy_manager=None, url=None,
+ reason=b''):
+ """Helper function to configure a mocked response."""
+ self.adapter.proxy_manager = proxy_manager or {}
+ self.response.content = content
+ self.response.url = url
+ self.response.reason = reason
+
+ def configure_request(self, body=b'', headers=None, method=None,
+ url=None):
+ """Helper function to configure a mocked request."""
+ self.request.body = body
+ self.request.headers = headers or {}
+ self.request.method = method
+ self.request.url = url
+
+ def configure_httpresponse(self, headers=None, reason=b'', status=200,
+ version=HTTP_1_1):
+ """Helper function to configure a mocked urllib3 response."""
+ self.httpresponse.headers = HTTPHeaderDict(headers or {})
+ self.httpresponse.reason = reason
+ self.httpresponse.status = status
+ self.httpresponse.version = version
+
+
+class TestResponsePrivateFunctions(RequestResponseMixin):
+
+ """Excercise private functions using responses."""
+
+ def test_get_proxy_information_sans_proxy(self):
+ """Show no information is returned when not using a proxy."""
+ self.configure_response()
+
+ assert dump._get_proxy_information(self.response) is None
+
+ def test_get_proxy_information_with_proxy_over_http(self):
+ """Show only the request path is returned for HTTP requests.
+
+ Using HTTP over a proxy doesn't alter anything except the request path
+ of the request. The method doesn't change a dictionary with the
+ request_path is the only thing that should be returned.
+ """
+ self.configure_response(
+ proxy_manager={'http://': 'http://local.proxy:3939'},
+ )
+ self.configure_request(
+ url='http://example.com',
+ method='GET',
+ )
+
+ assert dump._get_proxy_information(self.response) == {
+ 'request_path': 'http://example.com'
+ }
+
+ def test_get_proxy_information_with_proxy_over_https(self):
+ """Show that the request path and method are returned for HTTPS reqs.
+
+ Using HTTPS over a proxy changes the method used and the request path.
+ """
+ self.configure_response(
+ proxy_manager={'http://': 'http://local.proxy:3939'},
+ )
+ self.configure_request(
+ url='https://example.com',
+ method='GET',
+ )
+
+ assert dump._get_proxy_information(self.response) == {
+ 'method': 'CONNECT',
+ 'request_path': 'https://example.com'
+ }
+
+ def test_dump_request_data(self):
+ """Build up the request data into a bytearray."""
+ self.configure_request(
+ url='http://example.com/',
+ method='GET',
+ )
+
+ array = bytearray()
+ prefixes = dump.PrefixSettings('request:', 'response:')
+ dump._dump_request_data(
+ request=self.request,
+ prefixes=prefixes,
+ bytearr=array,
+ proxy_info={},
+ )
+
+ assert b'request:GET / HTTP/1.1\r\n' in array
+ assert b'request:Host: example.com\r\n' in array
+
+ def test_dump_request_data_with_proxy_info(self):
+ """Build up the request data into a bytearray."""
+ self.configure_request(
+ url='http://example.com/',
+ method='GET',
+ )
+
+ array = bytearray()
+ prefixes = dump.PrefixSettings('request:', 'response:')
+ dump._dump_request_data(
+ request=self.request,
+ prefixes=prefixes,
+ bytearr=array,
+ proxy_info={
+ 'request_path': b'fake-request-path',
+ 'method': b'CONNECT',
+ },
+ )
+
+ assert b'request:CONNECT fake-request-path HTTP/1.1\r\n' in array
+ assert b'request:Host: example.com\r\n' in array
+
+ def test_dump_response_data(self):
+ """Build up the response data into a bytearray."""
+ self.configure_response(
+ url='https://example.com/redirected',
+ content=b'foobarbogus',
+ reason=b'OK',
+ )
+ self.configure_httpresponse(
+ headers={'Content-Type': 'application/json'},
+ reason=b'OK',
+ status=201,
+ )
+
+ array = bytearray()
+ prefixes = dump.PrefixSettings('request:', 'response:')
+ dump._dump_response_data(
+ response=self.response,
+ prefixes=prefixes,
+ bytearr=array,
+ )
+
+ assert b'response:HTTP/1.1 201 OK\r\n' in array
+ assert b'response:Content-Type: application/json\r\n' in array
+
+ def test_dump_response_data_with_older_http_version(self):
+ """Build up the response data into a bytearray."""
+ self.configure_response(
+ url='https://example.com/redirected',
+ content=b'foobarbogus',
+ reason=b'OK',
+ )
+ self.configure_httpresponse(
+ headers={'Content-Type': 'application/json'},
+ reason=b'OK',
+ status=201,
+ version=HTTP_0_9,
+ )
+
+ array = bytearray()
+ prefixes = dump.PrefixSettings('request:', 'response:')
+ dump._dump_response_data(
+ response=self.response,
+ prefixes=prefixes,
+ bytearr=array,
+ )
+
+ assert b'response:HTTP/0.9 201 OK\r\n' in array
+ assert b'response:Content-Type: application/json\r\n' in array
+
+ def test_dump_response_data_with_unknown_http_version(self):
+ """Build up the response data into a bytearray."""
+ self.configure_response(
+ url='https://example.com/redirected',
+ content=b'foobarbogus',
+ reason=b'OK',
+ )
+ self.configure_httpresponse(
+ headers={'Content-Type': 'application/json'},
+ reason=b'OK',
+ status=201,
+ version=HTTP_UNKNOWN,
+ )
+
+ array = bytearray()
+ prefixes = dump.PrefixSettings('request:', 'response:')
+ dump._dump_response_data(
+ response=self.response,
+ prefixes=prefixes,
+ bytearr=array,
+ )
+
+ assert b'response:HTTP/? 201 OK\r\n' in array
+ assert b'response:Content-Type: application/json\r\n' in array
+
+
+class TestResponsePublicFunctions(RequestResponseMixin):
+
+ """Excercise public functions using responses."""
+
+ def test_dump_response_fails_without_request(self):
+ """Show that a response without a request raises a ValueError."""
+ del self.response.request
+ assert hasattr(self.response, 'request') is False
+
+ with pytest.raises(ValueError):
+ dump.dump_response(self.response)
+
+ def test_dump_response_uses_provided_bytearray(self):
+ """Show that users providing bytearrays receive those back."""
+ self.configure_request(
+ url='http://example.com/',
+ method='GET',
+ )
+ self.configure_response(
+ url='https://example.com/redirected',
+ content=b'foobarbogus',
+ reason=b'OK',
+ )
+ self.configure_httpresponse(
+ headers={'Content-Type': 'application/json'},
+ reason=b'OK',
+ status=201,
+ )
+ arr = bytearray()
+
+ retarr = dump.dump_response(self.response, data_array=arr)
+ assert retarr is arr
+
+
+class TestDumpRealResponses(object):
+
+ """Exercise dump utilities against real data."""
+
+ def test_dump_response(self):
+ session = requests.Session()
+ recorder = get_betamax(session)
+ with recorder.use_cassette('simple_get_request'):
+ response = session.get('https://httpbin.org/get')
+
+ arr = dump.dump_response(response)
+ assert b'< GET /get HTTP/1.1\r\n' in arr
+ assert b'< Host: httpbin.org\r\n' in arr
+ # NOTE(sigmavirus24): The ? below is only because Betamax doesn't
+ # preserve which HTTP version the server reports as supporting.
+ # When not using Betamax, there should be a different version
+ # reported.
+ assert b'> HTTP/? 200 OK\r\n' in arr
+ assert b'> Content-Type: application/json\r\n' in arr
+
+ def test_dump_all(self):
+ session = requests.Session()
+ recorder = get_betamax(session)
+ with recorder.use_cassette('redirect_request_for_dump_all'):
+ response = session.get('https://httpbin.org/redirect/5')
+
+ arr = dump.dump_all(response)
+ assert b'< GET /redirect/5 HTTP/1.1\r\n' in arr
+ assert b'> Location: /relative-redirect/4\r\n' in arr
+ assert b'< GET /relative-redirect/4 HTTP/1.1\r\n' in arr
+ assert b'> Location: /relative-redirect/3\r\n' in arr
+ assert b'< GET /relative-redirect/3 HTTP/1.1\r\n' in arr
+ assert b'> Location: /relative-redirect/2\r\n' in arr
+ assert b'< GET /relative-redirect/2 HTTP/1.1\r\n' in arr
+ assert b'> Location: /relative-redirect/1\r\n' in arr
+ assert b'< GET /relative-redirect/1 HTTP/1.1\r\n' in arr
+ assert b'> Location: /get\r\n' in arr
+ assert b'< GET /get HTTP/1.1\r\n' in arr
diff --git a/tests/test_fingerprintadapter.py b/tests/test_fingerprintadapter.py
new file mode 100644
index 0000000..1951160
--- /dev/null
+++ b/tests/test_fingerprintadapter.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+import requests
+import unittest
+
+from requests_toolbelt.adapters.fingerprint import FingerprintAdapter
+from . import get_betamax
+
+
+class TestFingerprintAdapter(unittest.TestCase):
+ HTTP2BIN_FINGERPRINT = 'abf8683eeba8521ad2e8dc48e92a1cbea3ff8608f1417948fdad75d7b50eb264'
+
+ def setUp(self):
+ self.session = requests.Session()
+ self.session.mount('https://http2bin.org', FingerprintAdapter(self.HTTP2BIN_FINGERPRINT))
+ self.recorder = get_betamax(self.session)
+
+ def test_fingerprint(self):
+ with self.recorder.use_cassette('http2bin_fingerprint'):
+ r = self.session.get('https://http2bin.org/get')
+ assert r.status_code == 200
diff --git a/tests/test_forgetfulcookiejar.py b/tests/test_forgetfulcookiejar.py
new file mode 100644
index 0000000..e9d07ed
--- /dev/null
+++ b/tests/test_forgetfulcookiejar.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+import requests
+import unittest
+
+from requests_toolbelt.cookies.forgetful import ForgetfulCookieJar
+from . import get_betamax
+
+
+class TestForgetfulCookieJar(unittest.TestCase):
+
+ def setUp(self):
+ self.session = requests.Session()
+ self.session.cookies = ForgetfulCookieJar()
+ self.recorder = get_betamax(self.session)
+
+ def test_cookies_are_ignored(self):
+ with self.recorder.use_cassette('http2bin_cookies'):
+ url = 'https://httpbin.org/cookies/set'
+ cookies = {
+ 'cookie0': 'value0',
+ }
+ r = self.session.request(
+ 'GET', url,
+ params=cookies
+ )
+ assert 'cookie0' not in self.session.cookies
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
new file mode 100644
index 0000000..dd8bf03
--- /dev/null
+++ b/tests/test_formdata.py
@@ -0,0 +1,76 @@
+"""Test module for requests_toolbelt.utils.formdata."""
+try:
+ from urllib.parse import parse_qs
+except ImportError:
+ from urlparse import parse_qs
+
+from requests_toolbelt.utils.formdata import urlencode
+
+import pytest
+
+dict_query = {
+ 'first_nested': {
+ 'second_nested': {
+ 'third_nested': {
+ 'fourth0': 'fourth_value0',
+ 'fourth1': 'fourth_value1',
+ },
+ 'third0': 'third_value0',
+ },
+ 'second0': 'second_value0',
+ },
+ 'outter': 'outter_value',
+}
+
+list_query = [
+ ('first_nested', [
+ ('second_nested', [
+ ('third_nested', [
+ ('fourth0', 'fourth_value0'),
+ ('fourth1', 'fourth_value1'),
+ ]),
+ ('third0', 'third_value0'),
+ ]),
+ ('second0', 'second_value0'),
+ ]),
+ ('outter', 'outter_value'),
+]
+
+mixed_dict_query = {
+ 'first_nested': {
+ 'second_nested': [
+ ('third_nested', {
+ 'fourth0': 'fourth_value0',
+ 'fourth1': 'fourth_value1',
+ }),
+ ('third0', 'third_value0'),
+ ],
+ 'second0': 'second_value0',
+ },
+ 'outter': 'outter_value',
+}
+
+expected_parsed_query = {
+ 'first_nested[second0]': ['second_value0'],
+ 'first_nested[second_nested][third0]': ['third_value0'],
+ 'first_nested[second_nested][third_nested][fourth0]': ['fourth_value0'],
+ 'first_nested[second_nested][third_nested][fourth1]': ['fourth_value1'],
+ 'outter': ['outter_value'],
+}
+
+
+@pytest.mark.parametrize("query", [dict_query, list_query, mixed_dict_query])
+def test_urlencode_flattens_nested_structures(query):
+ """Show that when parsed, the structure is conveniently flat."""
+ parsed = parse_qs(urlencode(query))
+
+ assert parsed == expected_parsed_query
+
+
+def test_urlencode_catches_invalid_input():
+ """Show that queries are loosely validated."""
+ with pytest.raises(ValueError):
+ urlencode(['fo'])
+
+ with pytest.raises(ValueError):
+ urlencode([('foo', 'bar', 'bogus')])
diff --git a/tests/test_host_header_ssl_adapter.py b/tests/test_host_header_ssl_adapter.py
new file mode 100644
index 0000000..d86378e
--- /dev/null
+++ b/tests/test_host_header_ssl_adapter.py
@@ -0,0 +1,48 @@
+import pytest
+import requests
+
+from requests_toolbelt.adapters import host_header_ssl as hhssl
+
+
+@pytest.fixture
+def session():
+ """Create a session with our adapter mounted."""
+ session = requests.Session()
+ session.mount('https://', hhssl.HostHeaderSSLAdapter())
+
+
+@pytest.mark.skip
+class TestHostHeaderSSLAdapter(object):
+ """Tests for our HostHeaderSNIAdapter."""
+
+ def test_ssladapter(self, session):
+ # normal mode
+ r = session.get('https://example.org')
+ assert r.status_code == 200
+
+ # accessing IP address directly
+ r = session.get('https://93.184.216.34',
+ headers={"Host": "example.org"})
+ assert r.status_code == 200
+
+ # vHost
+ r = session.get('https://93.184.216.34',
+ headers={'Host': 'example.com'})
+ assert r.status_code == 200
+
+ def test_stream(self):
+ self.session.get('https://54.175.219.8/stream/20',
+ headers={'Host': 'httpbin.org'},
+ stream=True)
+
+ def test_case_insensitive_header(self):
+ r = self.session.get('https://93.184.216.34',
+ headers={'hOSt': 'example.org'})
+ assert r.status_code == 200
+
+ def test_plain_requests(self):
+ # test whether the reason for this adapter remains
+ # (may be implemented into requests in the future)
+ with pytest.raises(requests.exceptions.SSLError):
+ requests.get(url='https://93.184.216.34',
+ headers={'Host': 'example.org'})
diff --git a/tests/test_multipart_decoder.py b/tests/test_multipart_decoder.py
new file mode 100644
index 0000000..19b1ae8
--- /dev/null
+++ b/tests/test_multipart_decoder.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+import io
+import sys
+import unittest
+import mock
+import pytest
+import requests
+from requests_toolbelt.multipart.decoder import BodyPart
+from requests_toolbelt.multipart.decoder import (
+ ImproperBodyPartContentException
+)
+from requests_toolbelt.multipart.decoder import MultipartDecoder
+from requests_toolbelt.multipart.decoder import (
+ NonMultipartContentTypeException
+)
+from requests_toolbelt.multipart.encoder import encode_with
+from requests_toolbelt.multipart.encoder import MultipartEncoder
+
+
+class TestBodyPart(unittest.TestCase):
+ @staticmethod
+ def u(content):
+ major = sys.version_info[0]
+ if major == 3:
+ return content
+ else:
+ return unicode(content.replace(r'\\', r'\\\\'), 'unicode_escape')
+
+ @staticmethod
+ def bodypart_bytes_from_headers_and_values(headers, value, encoding):
+ return b'\r\n\r\n'.join(
+ [
+ b'\r\n'.join(
+ [
+ b': '.join([encode_with(i, encoding) for i in h])
+ for h in headers
+ ]
+ ),
+ encode_with(value, encoding)
+ ]
+ )
+
+ def setUp(self):
+ self.header_1 = (TestBodyPart.u('Snowman'), TestBodyPart.u('☃'))
+ self.value_1 = TestBodyPart.u('©')
+ self.part_1 = BodyPart(
+ TestBodyPart.bodypart_bytes_from_headers_and_values(
+ (self.header_1,), self.value_1, 'utf-8'
+ ),
+ 'utf-8'
+ )
+ self.part_2 = BodyPart(
+ TestBodyPart.bodypart_bytes_from_headers_and_values(
+ [], self.value_1, 'utf-16'
+ ),
+ 'utf-16'
+ )
+
+ def test_equality_content_should_be_equal(self):
+ part_3 = BodyPart(
+ TestBodyPart.bodypart_bytes_from_headers_and_values(
+ [], self.value_1, 'utf-8'
+ ),
+ 'utf-8'
+ )
+ assert self.part_1.content == part_3.content
+
+ def test_equality_content_equals_bytes(self):
+ assert self.part_1.content == encode_with(self.value_1, 'utf-8')
+
+ def test_equality_content_should_not_be_equal(self):
+ assert self.part_1.content != self.part_2.content
+
+ def test_equality_content_does_not_equal_bytes(self):
+ assert self.part_1.content != encode_with(self.value_1, 'latin-1')
+
+ def test_changing_encoding_changes_text(self):
+ part_2_orig_text = self.part_2.text
+ self.part_2.encoding = 'latin-1'
+ assert self.part_2.text != part_2_orig_text
+
+ def test_text_should_be_equal(self):
+ assert self.part_1.text == self.part_2.text
+
+ def test_no_headers(self):
+ sample_1 = b'\r\n\r\nNo headers\r\nTwo lines'
+ part_3 = BodyPart(sample_1, 'utf-8')
+ assert len(part_3.headers) == 0
+ assert part_3.content == b'No headers\r\nTwo lines'
+
+ def test_no_crlf_crlf_in_content(self):
+ content = b'no CRLF CRLF here!\r\n'
+ with pytest.raises(ImproperBodyPartContentException):
+ BodyPart(content, 'utf-8')
+
+
+class TestMultipartDecoder(unittest.TestCase):
+ def setUp(self):
+ self.sample_1 = (
+ ('field 1', 'value 1'),
+ ('field 2', 'value 2'),
+ ('field 3', 'value 3'),
+ ('field 4', 'value 4'),
+ )
+ self.boundary = 'test boundary'
+ self.encoded_1 = MultipartEncoder(self.sample_1, self.boundary)
+ self.decoded_1 = MultipartDecoder(
+ self.encoded_1.to_string(),
+ self.encoded_1.content_type
+ )
+
+ def test_non_multipart_response_fails(self):
+ jpeg_response = mock.NonCallableMagicMock(spec=requests.Response)
+ jpeg_response.headers = {'content-type': 'image/jpeg'}
+ with pytest.raises(NonMultipartContentTypeException):
+ MultipartDecoder.from_response(jpeg_response)
+
+ def test_length_of_parts(self):
+ assert len(self.sample_1) == len(self.decoded_1.parts)
+
+ def test_content_of_parts(self):
+ def parts_equal(part, sample):
+ return part.content == encode_with(sample[1], 'utf-8')
+
+ parts_iter = zip(self.decoded_1.parts, self.sample_1)
+ assert all(parts_equal(part, sample) for part, sample in parts_iter)
+
+ def test_header_of_parts(self):
+ def parts_header_equal(part, sample):
+ return part.headers[b'Content-Disposition'] == encode_with(
+ 'form-data; name="{0}"'.format(sample[0]), 'utf-8'
+ )
+
+ parts_iter = zip(self.decoded_1.parts, self.sample_1)
+ assert all(
+ parts_header_equal(part, sample)
+ for part, sample in parts_iter
+ )
+
+ def test_from_response(self):
+ response = mock.NonCallableMagicMock(spec=requests.Response)
+ response.headers = {
+ 'content-type': 'multipart/related; boundary="samp1"'
+ }
+ cnt = io.BytesIO()
+ cnt.write(b'\r\n--samp1\r\n')
+ cnt.write(b'Header-1: Header-Value-1\r\n')
+ cnt.write(b'Header-2: Header-Value-2\r\n')
+ cnt.write(b'\r\n')
+ cnt.write(b'Body 1, Line 1\r\n')
+ cnt.write(b'Body 1, Line 2\r\n')
+ cnt.write(b'--samp1\r\n')
+ cnt.write(b'\r\n')
+ cnt.write(b'Body 2, Line 1\r\n')
+ cnt.write(b'--samp1--\r\n')
+ response.content = cnt.getvalue()
+ decoder_2 = MultipartDecoder.from_response(response)
+ assert decoder_2.content_type == response.headers['content-type']
+ assert (
+ decoder_2.parts[0].content == b'Body 1, Line 1\r\nBody 1, Line 2'
+ )
+ assert decoder_2.parts[0].headers[b'Header-1'] == b'Header-Value-1'
+ assert len(decoder_2.parts[1].headers) == 0
+ assert decoder_2.parts[1].content == b'Body 2, Line 1'
diff --git a/tests/test_multipart_encoder.py b/tests/test_multipart_encoder.py
new file mode 100644
index 0000000..f97ca91
--- /dev/null
+++ b/tests/test_multipart_encoder.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+import unittest
+import io
+from requests_toolbelt.multipart.encoder import CustomBytesIO, MultipartEncoder
+from requests_toolbelt._compat import filepost
+
+
+class LargeFileMock(object):
+ def __init__(self):
+ # Let's keep track of how many bytes we've given
+ self.bytes_read = 0
+ # Our limit (1GB)
+ self.bytes_max = 1024 * 1024 * 1024
+ # Fake name
+ self.name = 'fake_name.py'
+ # Create a fileno attribute
+ self.fileno = None
+
+ def __len__(self):
+ return self.bytes_max
+
+ def read(self, size=None):
+ if self.bytes_read >= self.bytes_max:
+ return b''
+
+ if size is None:
+ length = self.bytes_max - self.bytes_read
+ else:
+ length = size
+
+ length = int(length)
+ length = min([length, self.bytes_max - self.bytes_read])
+
+ self.bytes_read += length
+
+ return b'a' * length
+
+ def tell(self):
+ return self.bytes_read
+
+
+class TestCustomBytesIO(unittest.TestCase):
+ def setUp(self):
+ self.instance = CustomBytesIO()
+
+ def test_writable(self):
+ assert hasattr(self.instance, 'write')
+ assert self.instance.write(b'example') == 7
+
+ def test_readable(self):
+ assert hasattr(self.instance, 'read')
+ assert self.instance.read() == b''
+ assert self.instance.read(10) == b''
+
+ def test_can_read_after_writing_to(self):
+ self.instance.write(b'example text')
+ self.instance.read() == b'example text'
+
+ def test_can_read_some_after_writing_to(self):
+ self.instance.write(b'example text')
+ self.instance.read(6) == b'exampl'
+
+ def test_can_get_length(self):
+ self.instance.write(b'example')
+ self.instance.seek(0, 0)
+ assert self.instance.len == 7
+
+ def test_truncates_intelligently(self):
+ self.instance.write(b'abcdefghijklmnopqrstuvwxyzabcd') # 30 bytes
+ assert self.instance.tell() == 30
+ self.instance.seek(-10, 2)
+ self.instance.smart_truncate()
+ assert self.instance.len == 10
+ assert self.instance.read() == b'uvwxyzabcd'
+ assert self.instance.tell() == 10
+
+ def test_accepts_encoded_strings_with_unicode(self):
+ """Accepts a string with encoded unicode characters."""
+ s = b'this is a unicode string: \xc3\xa9 \xc3\xa1 \xc7\xab \xc3\xb3'
+ self.instance = CustomBytesIO(s)
+ assert self.instance.read() == s
+
+
+class TestMultipartEncoder(unittest.TestCase):
+ def setUp(self):
+ self.parts = [('field', 'value'), ('other_field', 'other_value')]
+ self.boundary = 'this-is-a-boundary'
+ self.instance = MultipartEncoder(self.parts, boundary=self.boundary)
+
+ def test_to_string(self):
+ assert self.instance.to_string() == (
+ '--this-is-a-boundary\r\n'
+ 'Content-Disposition: form-data; name="field"\r\n\r\n'
+ 'value\r\n'
+ '--this-is-a-boundary\r\n'
+ 'Content-Disposition: form-data; name="other_field"\r\n\r\n'
+ 'other_value\r\n'
+ '--this-is-a-boundary--\r\n'
+ ).encode()
+
+ def test_content_type(self):
+ expected = 'multipart/form-data; boundary=this-is-a-boundary'
+ assert self.instance.content_type == expected
+
+ def test_encodes_data_the_same(self):
+ encoded = filepost.encode_multipart_formdata(self.parts,
+ self.boundary)[0]
+ assert encoded == self.instance.read()
+
+ def test_streams_its_data(self):
+ large_file = LargeFileMock()
+ parts = {'some field': 'value',
+ 'some file': large_file,
+ }
+ encoder = MultipartEncoder(parts)
+ total_size = encoder.len
+ read_size = 1024 * 1024 * 128
+ already_read = 0
+ while True:
+ read = encoder.read(read_size)
+ already_read += len(read)
+ if not read:
+ break
+
+ assert encoder._buffer.tell() <= read_size
+ assert already_read == total_size
+
+ def test_length_is_correct(self):
+ encoded = filepost.encode_multipart_formdata(self.parts,
+ self.boundary)[0]
+ assert len(encoded) == self.instance.len
+
+ def test_encodes_with_readable_data(self):
+ s = io.BytesIO(b'value')
+ m = MultipartEncoder([('field', s)], boundary=self.boundary)
+ assert m.read() == (
+ '--this-is-a-boundary\r\n'
+ 'Content-Disposition: form-data; name="field"\r\n\r\n'
+ 'value\r\n'
+ '--this-is-a-boundary--\r\n'
+ ).encode()
+
+ def test_reads_open_file_objects(self):
+ with open('setup.py', 'rb') as fd:
+ m = MultipartEncoder([('field', 'foo'), ('file', fd)])
+ assert m.read() is not None
+
+ def test_reads_open_file_objects_with_a_specified_filename(self):
+ with open('setup.py', 'rb') as fd:
+ m = MultipartEncoder(
+ [('field', 'foo'), ('file', ('filename', fd, 'text/plain'))]
+ )
+ assert m.read() is not None
+
+ def test_reads_open_file_objects_using_to_string(self):
+ with open('setup.py', 'rb') as fd:
+ m = MultipartEncoder([('field', 'foo'), ('file', fd)])
+ assert m.to_string() is not None
+
+ def test_handles_encoded_unicode_strings(self):
+ m = MultipartEncoder([
+ ('field',
+ b'this is a unicode string: \xc3\xa9 \xc3\xa1 \xc7\xab \xc3\xb3')
+ ])
+ assert m.read() is not None
+
+ def test_handles_uncode_strings(self):
+ s = b'this is a unicode string: \xc3\xa9 \xc3\xa1 \xc7\xab \xc3\xb3'
+ m = MultipartEncoder([
+ ('field', s.decode('utf-8'))
+ ])
+ assert m.read() is not None
+
+ def test_regresion_1(self):
+ """Ensure issue #31 doesn't ever happen again."""
+ fields = {
+ "test": "t" * 100
+ }
+
+ for x in range(30):
+ fields['f%d' % x] = (
+ 'test', open('tests/test_multipart_encoder.py', 'rb')
+ )
+
+ m = MultipartEncoder(fields=fields)
+ total_size = m.len
+
+ blocksize = 8192
+ read_so_far = 0
+
+ while True:
+ data = m.read(blocksize)
+ if not data:
+ break
+ read_so_far += len(data)
+
+ assert read_so_far == total_size
+
+ def test_regression_2(self):
+ """Ensure issue #31 doesn't ever happen again."""
+ fields = {
+ "test": "t" * 8100
+ }
+
+ m = MultipartEncoder(fields=fields)
+ total_size = m.len
+
+ blocksize = 8192
+ read_so_far = 0
+
+ while True:
+ data = m.read(blocksize)
+ if not data:
+ break
+ read_so_far += len(data)
+
+ assert read_so_far == total_size
+
+ def test_handles_empty_unicode_values(self):
+ """Verify that the Encoder can handle empty unicode strings.
+
+ See https://github.com/sigmavirus24/requests-toolbelt/issues/46 for
+ more context.
+ """
+ fields = [(b'test'.decode('utf-8'), b''.decode('utf-8'))]
+ m = MultipartEncoder(fields=fields)
+ assert len(m.read()) > 0
+
+ def test_accepts_custom_content_type(self):
+ """Verify that the Encoder handles custom content-types.
+
+ See https://github.com/sigmavirus24/requests-toolbelt/issues/52
+ """
+ fields = [
+ (b'test'.decode('utf-8'), (b'filename'.decode('utf-8'),
+ b'filecontent',
+ b'application/json'.decode('utf-8')))
+ ]
+ m = MultipartEncoder(fields=fields)
+ output = m.read().decode('utf-8')
+ assert output.index('Content-Type: application/json\r\n') > 0
+
+ def test_accepts_custom_headers(self):
+ """Verify that the Encoder handles custom headers.
+
+ See https://github.com/sigmavirus24/requests-toolbelt/issues/52
+ """
+ fields = [
+ (b'test'.decode('utf-8'), (b'filename'.decode('utf-8'),
+ b'filecontent',
+ b'application/json'.decode('utf-8'),
+ {'X-My-Header': 'my-value'}))
+ ]
+ m = MultipartEncoder(fields=fields)
+ output = m.read().decode('utf-8')
+ assert output.index('X-My-Header: my-value\r\n') > 0
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_multipart_monitor.py b/tests/test_multipart_monitor.py
new file mode 100644
index 0000000..a37f95f
--- /dev/null
+++ b/tests/test_multipart_monitor.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+import math
+import unittest
+from requests_toolbelt.multipart.encoder import (
+ IDENTITY, MultipartEncoder, MultipartEncoderMonitor
+ )
+
+
+class TestMultipartEncoderMonitor(unittest.TestCase):
+ def setUp(self):
+ self.fields = {'a': 'b'}
+ self.boundary = 'thisisaboundary'
+ self.encoder = MultipartEncoder(self.fields, self.boundary)
+ self.monitor = MultipartEncoderMonitor(self.encoder)
+
+ def test_content_type(self):
+ assert self.monitor.content_type == self.encoder.content_type
+
+ def test_length(self):
+ assert self.encoder.len == self.monitor.len
+
+ def test_read(self):
+ new_encoder = MultipartEncoder(self.fields, self.boundary)
+ assert new_encoder.read() == self.monitor.read()
+
+ def test_callback_called_when_reading_everything(self):
+ callback = Callback(self.monitor)
+ self.monitor.callback = callback
+ self.monitor.read()
+ assert callback.called == 1
+
+ def test_callback(self):
+ callback = Callback(self.monitor)
+ self.monitor.callback = callback
+ chunk_size = int(math.ceil(self.encoder.len / 4.0))
+ while self.monitor.read(chunk_size):
+ pass
+ assert callback.called == 5
+
+ def test_bytes_read(self):
+ bytes_to_read = self.encoder.len
+ self.monitor.read()
+ assert self.monitor.bytes_read == bytes_to_read
+
+ def test_default_callable_is_the_identity(self):
+ assert self.monitor.callback == IDENTITY
+ assert IDENTITY(1) == 1
+
+ def test_from_fields(self):
+ monitor = MultipartEncoderMonitor.from_fields(
+ self.fields, self.boundary
+ )
+ assert isinstance(monitor, MultipartEncoderMonitor)
+ assert isinstance(monitor.encoder, MultipartEncoder)
+ assert monitor.encoder.boundary_value == self.boundary
+
+
+class Callback(object):
+ def __init__(self, monitor):
+ self.called = 0
+ self.monitor = monitor
+
+ def __call__(self, monitor):
+ self.called += 1
+ assert monitor == self.monitor
diff --git a/tests/test_proxy_digest_auth.py b/tests/test_proxy_digest_auth.py
new file mode 100644
index 0000000..003816f
--- /dev/null
+++ b/tests/test_proxy_digest_auth.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""Test proxy digest authentication."""
+
+import unittest
+import mock
+
+import requests
+from requests_toolbelt.auth import http_proxy_digest
+
+
+class TestProxyDigestAuth(unittest.TestCase):
+ """Tests for the ProxyDigestAuth class."""
+
+ def setUp(self):
+ """Set up variables for each test."""
+ self.username = "username"
+ self.password = "password"
+ self.auth = http_proxy_digest.HTTPProxyDigestAuth(
+ self.username, self.password
+ )
+ self.prepared_request = requests.Request(
+ 'GET',
+ 'http://host.org/index.html'
+ ).prepare()
+
+ def test_with_existing_nonce(self):
+ """Test if it will generate Proxy-Auth header when nonce present.
+
+ Digest authentication's correctness will not be tested here.
+ """
+ self.auth.last_nonce = "bH3FVAAAAAAg74rL3X8AAI3CyBAAAAAA"
+ self.auth.chal = {
+ 'nonce': self.auth.last_nonce,
+ 'realm': 'testrealm@host.org',
+ 'qop': 'auth'
+ }
+
+ # prepared_request headers should be clear before calling auth
+ assert self.prepared_request.headers.get('Proxy-Authorization') is None
+ self.auth(self.prepared_request)
+ assert self.prepared_request.headers['Proxy-Authorization'] is not None
+
+ def test_no_challenge(self):
+ """Test that a response containing no auth challenge is left alone."""
+ connection = MockConnection()
+ first_response = connection.make_response(self.prepared_request)
+ first_response.status_code = 404
+
+ assert self.auth.last_nonce == ''
+ final_response = self.auth.handle_407(first_response)
+ headers = final_response.request.headers
+ assert self.auth.last_nonce == ''
+ assert first_response is final_response
+ assert headers.get('Proxy-Authorization') is None
+
+ def test_digest_challenge(self):
+ """Test a response with a digest auth challenge causes a new request.
+
+ This ensures that the auth class generates a new request with a
+ Proxy-Authorization header.
+
+ Digest authentication's correctness will not be tested here.
+ """
+ connection = MockConnection()
+ first_response = connection.make_response(self.prepared_request)
+ first_response.status_code = 407
+ first_response.headers['Proxy-Authenticate'] = (
+ 'Digest'
+ ' realm="Fake Realm", nonce="oS6WVgAAAABw698CAAAAAHAk/HUAAAAA",'
+ ' qop="auth", stale=false'
+ )
+
+ assert self.auth.last_nonce == ''
+ final_response = self.auth.handle_407(first_response)
+ headers = final_response.request.headers
+ assert self.auth.last_nonce != ''
+ assert first_response is not final_response
+ assert headers.get('Proxy-Authorization') is not None
+
+ def test_ntlm_challenge(self):
+ """Test a response without a Digest auth challenge is left alone."""
+ connection = MockConnection()
+ first_response = connection.make_response(self.prepared_request)
+ first_response.status_code = 407
+ first_response.headers['Proxy-Authenticate'] = 'NTLM'
+
+ assert self.auth.last_nonce == ''
+ final_response = self.auth.handle_407(first_response)
+ headers = final_response.request.headers
+ assert self.auth.last_nonce == ''
+ assert first_response is final_response
+ assert headers.get('Proxy-Authorization') is None
+
+
+class MockConnection(object):
+ """Fake connection object."""
+
+ def send(self, request, **kwargs):
+ """Mock out the send method."""
+ return self.make_response(request)
+
+ def make_response(self, request):
+ """Make a response for us based on the request."""
+ response = requests.Response()
+ response.status_code = 200
+ response.request = request
+ response.raw = mock.MagicMock()
+ response.connection = self
+ return response
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
new file mode 100644
index 0000000..297d9ea
--- /dev/null
+++ b/tests/test_sessions.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+import unittest
+import pytest
+
+from requests_toolbelt import sessions
+from . import get_betamax
+
+
+class TestBasedSession(unittest.TestCase):
+ def test_with_base(self):
+ session = sessions.BaseUrlSession('https://httpbin.org/')
+ recorder = get_betamax(session)
+ with recorder.use_cassette('simple_get_request'):
+ response = session.get('/get')
+ response.raise_for_status()
+
+ def test_without_base(self):
+ session = sessions.BaseUrlSession()
+ with pytest.raises(ValueError):
+ session.get('/')
+
+ def test_override_base(self):
+ session = sessions.BaseUrlSession('https://www.google.com')
+ recorder = get_betamax(session)
+ with recorder.use_cassette('simple_get_request'):
+ response = session.get('https://httpbin.org/get')
+ response.raise_for_status()
+ assert response.json()['headers']['Host'] == 'httpbin.org'
diff --git a/tests/test_socket_options_adapter.py b/tests/test_socket_options_adapter.py
new file mode 100644
index 0000000..7e264f8
--- /dev/null
+++ b/tests/test_socket_options_adapter.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+"""Tests for the SocketOptionsAdapter and TCPKeepAliveAdapter."""
+import contextlib
+import socket
+
+import mock
+import requests
+from requests_toolbelt._compat import poolmanager
+
+from requests_toolbelt.adapters import socket_options
+
+
+@contextlib.contextmanager
+def remove_keepidle():
+ """A context manager to remove TCP_KEEPIDLE from socket."""
+ TCP_KEEPIDLE = getattr(socket, 'TCP_KEEPIDLE', None)
+ if TCP_KEEPIDLE is not None:
+ del socket.TCP_KEEPIDLE
+
+ yield
+
+ if TCP_KEEPIDLE is not None:
+ socket.TCP_KEEPIDLE = TCP_KEEPIDLE
+
+
+@contextlib.contextmanager
+def set_keepidle(value):
+ """A context manager to set TCP_KEEPALIVE on socket always."""
+ TCP_KEEPIDLE = getattr(socket, 'TCP_KEEPIDLE', None)
+ socket.TCP_KEEPIDLE = value
+
+ yield
+
+ if TCP_KEEPIDLE is not None:
+ socket.TCP_KEEPIDLE = TCP_KEEPIDLE
+ else:
+ del socket.TCP_KEEPIDLE
+
+
+@mock.patch.object(requests, '__build__', 0x020500)
+@mock.patch.object(poolmanager, 'PoolManager')
+def test_options_passing_on_newer_requests(PoolManager):
+ """Show that options are passed for a new enough version of requests."""
+ fake_opts = [('test', 'options', 'fake')]
+ adapter = socket_options.SocketOptionsAdapter(
+ socket_options=fake_opts,
+ pool_connections=10,
+ pool_maxsize=5,
+ pool_block=True,
+ )
+ PoolManager.assert_called_once_with(
+ num_pools=10, maxsize=5, block=True,
+ socket_options=fake_opts
+ )
+ assert adapter.socket_options == fake_opts
+
+
+@mock.patch.object(requests, '__build__', 0x020300)
+@mock.patch.object(poolmanager, 'PoolManager')
+def test_options_not_passed_on_older_requests(PoolManager):
+ """Show that options are not passed for older versions of requests."""
+ fake_opts = [('test', 'options', 'fake')]
+ socket_options.SocketOptionsAdapter(
+ socket_options=fake_opts,
+ pool_connections=10,
+ pool_maxsize=5,
+ pool_block=True,
+ )
+ assert PoolManager.called is False
+
+
+@mock.patch.object(requests, '__build__', 0x020500)
+@mock.patch.object(poolmanager, 'PoolManager')
+def test_keep_alive_on_newer_requests_no_idle(PoolManager):
+ """Show that options are generated correctly from kwargs."""
+ socket_opts = [
+ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
+ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 10),
+ ]
+ with remove_keepidle():
+ adapter = socket_options.TCPKeepAliveAdapter(
+ idle=30, interval=10, count=10,
+ pool_connections=10,
+ pool_maxsize=5,
+ pool_block=True,
+ )
+ PoolManager.assert_called_once_with(
+ num_pools=10, maxsize=5, block=True,
+ socket_options=socket_opts
+ )
+ assert adapter.socket_options == socket_opts
+
+
+@mock.patch.object(requests, '__build__', 0x020500)
+@mock.patch.object(poolmanager, 'PoolManager')
+def test_keep_alive_on_newer_requests_with_idle(PoolManager):
+ """Show that options are generated correctly from kwargs with KEEPIDLE."""
+ with set_keepidle(3000):
+ socket_opts = [
+ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
+ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 10),
+ (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30),
+ ]
+ adapter = socket_options.TCPKeepAliveAdapter(
+ idle=30, interval=10, count=10,
+ pool_connections=10,
+ pool_maxsize=5,
+ pool_block=True,
+ )
+
+ PoolManager.assert_called_once_with(
+ num_pools=10, maxsize=5, block=True,
+ socket_options=socket_opts
+ )
+ assert adapter.socket_options == socket_opts
diff --git a/tests/test_source_adapter.py b/tests/test_source_adapter.py
new file mode 100644
index 0000000..688385a
--- /dev/null
+++ b/tests/test_source_adapter.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from requests.adapters import DEFAULT_POOLSIZE, DEFAULT_POOLBLOCK
+from mock import patch
+from requests_toolbelt.adapters.source import SourceAddressAdapter
+
+import pytest
+
+
+@patch('requests_toolbelt.adapters.source.poolmanager')
+def test_source_address_adapter_string(poolmanager):
+ SourceAddressAdapter('10.10.10.10')
+
+ poolmanager.PoolManager.assert_called_once_with(
+ num_pools=DEFAULT_POOLSIZE,
+ maxsize=DEFAULT_POOLSIZE,
+ block=DEFAULT_POOLBLOCK,
+ source_address=('10.10.10.10', 0)
+ )
+
+
+@patch('requests_toolbelt.adapters.source.poolmanager')
+def test_source_address_adapter_tuple(poolmanager):
+ SourceAddressAdapter(('10.10.10.10', 80))
+
+ poolmanager.PoolManager.assert_called_once_with(
+ num_pools=DEFAULT_POOLSIZE,
+ maxsize=DEFAULT_POOLSIZE,
+ block=DEFAULT_POOLBLOCK,
+ source_address=('10.10.10.10', 80)
+ )
+
+
+@patch('requests_toolbelt.adapters.source.poolmanager')
+def test_source_address_adapter_type_error(poolmanager):
+ with pytest.raises(TypeError):
+ SourceAddressAdapter({'10.10.10.10': 80})
+
+ assert not poolmanager.PoolManager.called
diff --git a/tests/test_ssladapter.py b/tests/test_ssladapter.py
new file mode 100644
index 0000000..6b28ae2
--- /dev/null
+++ b/tests/test_ssladapter.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+import mock
+import pytest
+import requests
+import unittest
+
+from requests_toolbelt import SSLAdapter
+from . import get_betamax
+
+
+class TestSSLAdapter(unittest.TestCase):
+ def setUp(self):
+ self.session = requests.Session()
+ self.session.mount('https://', SSLAdapter('SSLv3'))
+ self.recorder = get_betamax(self.session)
+
+ def test_klevas(self):
+ with self.recorder.use_cassette('klevas_vu_lt_ssl3'):
+ r = self.session.get('https://klevas.vu.lt/')
+ assert r.status_code == 200
+
+ @pytest.mark.skipif(requests.__build__ < 0x020400,
+ reason="Requires Requests v2.4.0 or later")
+ @mock.patch('requests.packages.urllib3.poolmanager.ProxyManager')
+ def test_proxies(self, ProxyManager):
+ a = SSLAdapter('SSLv3')
+ a.proxy_manager_for('http://127.0.0.1:8888')
+
+ assert ProxyManager.call_count == 1
+ kwargs = ProxyManager.call_args_list[0][1]
+ assert kwargs['ssl_version'] == 'SSLv3'
diff --git a/tests/test_streaming_iterator.py b/tests/test_streaming_iterator.py
new file mode 100644
index 0000000..cdb9ca5
--- /dev/null
+++ b/tests/test_streaming_iterator.py
@@ -0,0 +1,68 @@
+import io
+
+from requests_toolbelt.streaming_iterator import StreamingIterator
+
+import pytest
+
+@pytest.fixture(params=[True, False])
+def get_iterable(request):
+ '''
+ When this fixture is used, the test is run twice -- once with the iterable
+ being a file-like object, once being an iterator.
+ '''
+ is_file = request.param
+ def inner(chunks):
+ if is_file:
+ return io.BytesIO(b''.join(chunks))
+ return iter(chunks)
+ return inner
+
+
+class TestStreamingIterator(object):
+ @pytest.fixture(autouse=True)
+ def setup(self, get_iterable):
+ self.chunks = [b'here', b'are', b'some', b'chunks']
+ self.size = 17
+ self.uploader = StreamingIterator(self.size, get_iterable(self.chunks))
+
+ def test_read_returns_all_chunks_in_one(self):
+ assert self.uploader.read() == b''.join(self.chunks)
+
+ def test_read_returns_empty_string_after_exhausting_the_iterator(self):
+ for i in range(0, 4):
+ self.uploader.read(8192)
+
+ assert self.uploader.read() == b''
+ assert self.uploader.read(8192) == b''
+
+
+class TestStreamingIteratorWithLargeChunks(object):
+ @pytest.fixture(autouse=True)
+ def setup(self, get_iterable):
+ self.letters = [b'a', b'b', b'c', b'd', b'e']
+ self.chunks = (letter * 2000 for letter in self.letters)
+ self.size = 5 * 2000
+ self.uploader = StreamingIterator(self.size, get_iterable(self.chunks))
+
+ def test_returns_the_amount_requested(self):
+ chunk_size = 1000
+ bytes_read = 0
+ while True:
+ b = self.uploader.read(chunk_size)
+ if not b:
+ break
+ assert len(b) == chunk_size
+ bytes_read += len(b)
+
+ assert bytes_read == self.size
+
+ def test_returns_all_of_the_bytes(self):
+ chunk_size = 8192
+ bytes_read = 0
+ while True:
+ b = self.uploader.read(chunk_size)
+ if not b:
+ break
+ bytes_read += len(b)
+
+ assert bytes_read == self.size
diff --git a/tests/test_user_agent.py b/tests/test_user_agent.py
new file mode 100644
index 0000000..7800339
--- /dev/null
+++ b/tests/test_user_agent.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+import unittest
+import sys
+
+from mock import patch
+import pytest
+
+from requests_toolbelt.utils import user_agent as ua
+
+
+class Object(object):
+ """
+ A simple mock object that can have attributes added to it.
+ """
+ pass
+
+
+class TestUserAgentBuilder(unittest.TestCase):
+ def test_only_user_agent_name(self):
+ assert 'fake/1.0.0' == ua.UserAgentBuilder('fake', '1.0.0').build()
+
+ def test_includes_extras(self):
+ expected = 'fake/1.0.0 another-fake/2.0.1 yet-another-fake/17.1.0'
+ actual = ua.UserAgentBuilder('fake', '1.0.0').include_extras([
+ ('another-fake', '2.0.1'),
+ ('yet-another-fake', '17.1.0'),
+ ]).build()
+ assert expected == actual
+
+ @patch('platform.python_implementation', return_value='CPython')
+ @patch('platform.python_version', return_value='2.7.13')
+ def test_include_implementation(self, *_):
+ expected = 'fake/1.0.0 CPython/2.7.13'
+ actual = ua.UserAgentBuilder('fake', '1.0.0').include_implementation(
+ ).build()
+ assert expected == actual
+
+ @patch('platform.system', return_value='Linux')
+ @patch('platform.release', return_value='4.9.5')
+ def test_include_system(self, *_):
+ expected = 'fake/1.0.0 Linux/4.9.5'
+ actual = ua.UserAgentBuilder('fake', '1.0.0').include_system(
+ ).build()
+ assert expected == actual
+
+
+class TestUserAgent(unittest.TestCase):
+ def test_user_agent_provides_package_name(self):
+ assert "my-package" in ua.user_agent("my-package", "0.0.1")
+
+ def test_user_agent_provides_package_version(self):
+ assert "0.0.1" in ua.user_agent("my-package", "0.0.1")
+
+ def test_user_agent_builds_extras_appropriately(self):
+ assert "extra/1.0.0" in ua.user_agent(
+ "my-package", "0.0.1", extras=[("extra", "1.0.0")]
+ )
+
+ def test_user_agent_checks_extras_for_tuples_of_incorrect_length(self):
+ with pytest.raises(ValueError):
+ ua.user_agent("my-package", "0.0.1", extras=[
+ ("extra", "1.0.0", "oops")
+ ])
+
+ with pytest.raises(ValueError):
+ ua.user_agent("my-package", "0.0.1", extras=[
+ ("extra",)
+ ])
+
+
+class TestImplementationString(unittest.TestCase):
+ @patch('platform.python_implementation')
+ @patch('platform.python_version')
+ def test_cpython_implementation(self, mock_version, mock_implementation):
+ mock_implementation.return_value = 'CPython'
+ mock_version.return_value = '2.7.5'
+ assert 'CPython/2.7.5' == ua._implementation_string()
+
+ @patch('platform.python_implementation')
+ def test_pypy_implementation_final(self, mock_implementation):
+ mock_implementation.return_value = 'PyPy'
+ sys.pypy_version_info = Object()
+ sys.pypy_version_info.major = 2
+ sys.pypy_version_info.minor = 0
+ sys.pypy_version_info.micro = 1
+ sys.pypy_version_info.releaselevel = 'final'
+
+ assert 'PyPy/2.0.1' == ua._implementation_string()
+
+ @patch('platform.python_implementation')
+ def test_pypy_implementation_non_final(self, mock_implementation):
+ mock_implementation.return_value = 'PyPy'
+ sys.pypy_version_info = Object()
+ sys.pypy_version_info.major = 2
+ sys.pypy_version_info.minor = 0
+ sys.pypy_version_info.micro = 1
+ sys.pypy_version_info.releaselevel = 'beta2'
+
+ assert 'PyPy/2.0.1beta2' == ua._implementation_string()
+
+ @patch('platform.python_implementation')
+ def test_unknown_implementation(self, mock_implementation):
+ mock_implementation.return_value = "Lukasa'sSuperPython"
+
+ assert "Lukasa'sSuperPython/Unknown" == ua._implementation_string()
diff --git a/tests/threaded/__init__.py b/tests/threaded/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/threaded/__init__.py
diff --git a/tests/threaded/test_api.py b/tests/threaded/test_api.py
new file mode 100644
index 0000000..678178b
--- /dev/null
+++ b/tests/threaded/test_api.py
@@ -0,0 +1,59 @@
+"""Module containing tests for requests_toolbelt.threaded API."""
+
+import mock
+import pytest
+
+from requests_toolbelt._compat import queue
+from requests_toolbelt import threaded
+
+
+def test_creates_a_pool_for_the_user():
+ """Assert a Pool object is used correctly and as we expect.
+
+ This just ensures that we're not jumping through any extra hoops with our
+ internal usage of a Pool object.
+ """
+ mocked_pool = mock.Mock(spec=['join_all', 'responses', 'exceptions'])
+ with mock.patch('requests_toolbelt.threaded.pool.Pool') as Pool:
+ Pool.return_value = mocked_pool
+ threaded.map([{}, {}])
+
+ assert Pool.called is True
+ _, kwargs = Pool.call_args
+ assert 'job_queue' in kwargs
+ assert isinstance(kwargs['job_queue'], queue.Queue)
+ mocked_pool.join_all.assert_called_once_with()
+ mocked_pool.responses.assert_called_once_with()
+ mocked_pool.exceptions.assert_called_once_with()
+
+
+def test_raises_a_value_error_for_non_dictionaries():
+ """Exercise our lazy valdation."""
+ with pytest.raises(ValueError):
+ threaded.map([[], []])
+
+
+def test_raises_a_value_error_for_falsey_requests():
+ """Assert that the requests param is truthy."""
+ with pytest.raises(ValueError):
+ threaded.map([])
+
+ with pytest.raises(ValueError):
+ threaded.map(None)
+
+
+def test_passes_on_kwargs():
+ """Verify that we pass on kwargs to the Pool constructor."""
+ mocked_pool = mock.Mock(spec=['join_all', 'responses', 'exceptions'])
+ with mock.patch('requests_toolbelt.threaded.pool.Pool') as Pool:
+ Pool.return_value = mocked_pool
+ threaded.map([{}, {}], num_processes=1000,
+ initializer=test_passes_on_kwargs)
+
+ _, kwargs = Pool.call_args
+ assert 'job_queue' in kwargs
+ assert 'num_processes' in kwargs
+ assert 'initializer' in kwargs
+
+ assert kwargs['num_processes'] == 1000
+ assert kwargs['initializer'] == test_passes_on_kwargs
diff --git a/tests/threaded/test_pool.py b/tests/threaded/test_pool.py
new file mode 100644
index 0000000..b0653bb
--- /dev/null
+++ b/tests/threaded/test_pool.py
@@ -0,0 +1,226 @@
+"""Module containing the tests for requests_toolbelt.threaded.pool."""
+try:
+ import queue # Python 3
+except ImportError:
+ import Queue as queue
+import unittest
+
+import mock
+import pytest
+
+from requests_toolbelt.threaded import pool
+from requests_toolbelt.threaded import thread
+
+
+class TestPool(unittest.TestCase):
+
+ """Collection of tests for requests_toolbelt.threaded.pool.Pool."""
+
+ def test_requires_positive_number_of_processes(self):
+ """Show that the number of processes has to be > 0."""
+ with pytest.raises(ValueError):
+ pool.Pool(None, num_processes=0)
+
+ with pytest.raises(ValueError):
+ pool.Pool(None, num_processes=-1)
+
+ def test_number_of_processes_can_be_arbitrary(self):
+ """Show that the number of processes can be set."""
+ p = pool.Pool(None, num_processes=100)
+ assert p._processes == 100
+ assert len(p._pool) == 100
+
+ p = pool.Pool(None, num_processes=1)
+ assert p._processes == 1
+ assert len(p._pool) == 1
+
+ def test_initializer_is_called(self):
+ """Ensure that the initializer function is called."""
+ initializer = mock.MagicMock()
+ pool.Pool(None, num_processes=1, initializer=initializer)
+ assert initializer.called is True
+ initializer.assert_called_once_with(mock.ANY)
+
+ def test_auth_generator_is_called(self):
+ """Ensure that the auth_generator function is called."""
+ auth_generator = mock.MagicMock()
+ pool.Pool(None, num_processes=1, auth_generator=auth_generator)
+ assert auth_generator.called is True
+ auth_generator.assert_called_once_with(mock.ANY)
+
+ def test_session_is_called(self):
+ """Ensure that the session function is called."""
+ session = mock.MagicMock()
+ pool.Pool(None, num_processes=1, session=session)
+ assert session.called is True
+ session.assert_called_once_with()
+
+ def test_from_exceptions_populates_a_queue(self):
+ """Ensure a Queue is properly populated from exceptions."""
+ urls = ["https://httpbin.org/get?n={0}".format(n) for n in range(5)]
+ Exc = pool.ThreadException
+ excs = (Exc({'method': 'GET', 'url': url}, None) for url in urls)
+
+ job_queue = mock.MagicMock()
+ with mock.patch.object(queue, 'Queue', return_value=job_queue):
+ with mock.patch.object(thread, 'SessionThread'):
+ pool.Pool.from_exceptions(excs)
+
+ assert job_queue.put.call_count == 5
+ assert job_queue.put.mock_calls == [
+ mock.call({'method': 'GET', 'url': url})
+ for url in urls
+ ]
+
+ def test_from_urls_constructs_get_requests(self):
+ """Ensure a Queue is properly populated from an iterable of urls."""
+ urls = ["https://httpbin.org/get?n={0}".format(n) for n in range(5)]
+
+ job_queue = mock.MagicMock()
+ with mock.patch.object(queue, 'Queue', return_value=job_queue):
+ with mock.patch.object(thread, 'SessionThread'):
+ pool.Pool.from_urls(urls)
+
+ assert job_queue.put.call_count == 5
+ assert job_queue.put.mock_calls == [
+ mock.call({'method': 'GET', 'url': url})
+ for url in urls
+ ]
+
+ def test_from_urls_constructs_get_requests_with_kwargs(self):
+ """Ensure a Queue is properly populated from an iterable of urls."""
+ def merge(*args):
+ final = {}
+ for d in args:
+ final.update(d)
+ return final
+
+ urls = ["https://httpbin.org/get?n={0}".format(n) for n in range(5)]
+
+ kwargs = {'stream': True, 'headers': {'Accept': 'application/json'}}
+ job_queue = mock.MagicMock()
+ with mock.patch.object(queue, 'Queue', return_value=job_queue):
+ with mock.patch.object(thread, 'SessionThread'):
+ pool.Pool.from_urls(urls, kwargs)
+
+ assert job_queue.put.call_count == 5
+ assert job_queue.put.mock_calls == [
+ mock.call(merge({'method': 'GET', 'url': url}, kwargs))
+ for url in urls
+ ]
+
+ def test_join_all(self):
+ """Ensure that all threads are joined properly."""
+ session_threads = []
+
+ def _side_effect(*args, **kwargs):
+ thread = mock.MagicMock()
+ session_threads.append(thread)
+ return thread
+
+ with mock.patch.object(thread, 'SessionThread',
+ side_effect=_side_effect):
+ pool.Pool(None).join_all()
+
+ for st in session_threads:
+ st.join.assert_called_once_with()
+
+ def test_get_response_returns_thread_response(self):
+ """Ensure that a ThreadResponse is made when there's data."""
+ queues = []
+
+ def _side_effect():
+ q = mock.MagicMock()
+ q.get_nowait.return_value = ({}, None)
+ queues.append(q)
+ return q
+
+ with mock.patch.object(queue, 'Queue', side_effect=_side_effect):
+ with mock.patch.object(thread, 'SessionThread'):
+ p = pool.Pool(None)
+
+ assert len(queues) == 2
+
+ assert isinstance(p.get_response(), pool.ThreadResponse)
+ assert len([q for q in queues if q.get_nowait.called]) == 1
+
+ def test_get_exception_returns_thread_exception(self):
+ """Ensure that a ThreadException is made when there's data."""
+ queues = []
+
+ def _side_effect():
+ q = mock.MagicMock()
+ q.get_nowait.return_value = ({}, None)
+ queues.append(q)
+ return q
+
+ with mock.patch.object(queue, 'Queue', side_effect=_side_effect):
+ with mock.patch.object(thread, 'SessionThread'):
+ p = pool.Pool(None)
+
+ assert len(queues) == 2
+
+ assert isinstance(p.get_exception(), pool.ThreadException)
+ assert len([q for q in queues if q.get_nowait.called]) == 1
+
+ def test_get_response_returns_none_when_queue_is_empty(self):
+ """Ensure that None is returned when the response Queue is empty."""
+ queues = []
+
+ def _side_effect():
+ q = mock.MagicMock()
+ q.get_nowait.side_effect = queue.Empty()
+ queues.append(q)
+ return q
+
+ with mock.patch.object(queue, 'Queue', side_effect=_side_effect):
+ with mock.patch.object(thread, 'SessionThread'):
+ p = pool.Pool(None)
+
+ assert len(queues) == 2
+
+ assert p.get_response() is None
+ assert len([q for q in queues if q.get_nowait.called]) == 1
+
+ def test_get_exception_returns_none_when_queue_is_empty(self):
+ """Ensure that None is returned when the exception Queue is empty."""
+ queues = []
+
+ def _side_effect():
+ q = mock.MagicMock()
+ q.get_nowait.side_effect = queue.Empty()
+ queues.append(q)
+ return q
+
+ with mock.patch.object(queue, 'Queue', side_effect=_side_effect):
+ with mock.patch.object(thread, 'SessionThread'):
+ p = pool.Pool(None)
+
+ assert len(queues) == 2
+
+ assert p.get_exception() is None
+ assert len([q for q in queues if q.get_nowait.called]) == 1
+
+ def test_lists_are_correctly_returned(self):
+ """Ensure that exceptions and responses return correct lists."""
+ def _make_queue():
+ q = queue.Queue()
+ q.put(({}, None))
+ return q
+
+ with mock.patch.object(thread, 'SessionThread'):
+ p = pool.Pool(None)
+
+ # Set up real queues.
+ p._response_queue = _make_queue()
+ p._exc_queue = _make_queue()
+
+ excs = list(p.exceptions())
+ assert len(excs) == 1
+ for exc in excs:
+ assert isinstance(exc, pool.ThreadException)
+
+ resps = list(p.responses())
+ assert len(resps) == 1
+ for resp in resps:
+ assert isinstance(resp, pool.ThreadResponse)
diff --git a/tests/threaded/test_thread.py b/tests/threaded/test_thread.py
new file mode 100644
index 0000000..bb92f7f
--- /dev/null
+++ b/tests/threaded/test_thread.py
@@ -0,0 +1,131 @@
+"""Module containing the tests for requests_toolbelt.threaded.thread."""
+try:
+ import queue # Python 3
+except ImportError:
+ import Queue as queue
+import threading
+import unittest
+import uuid
+
+import mock
+import requests.exceptions
+
+from requests_toolbelt.threaded import thread
+
+
+def _make_mocks():
+ return (mock.MagicMock() for _ in range(4))
+
+
+def _initialize_a_session_thread(session=None, job_queue=None,
+ response_queue=None, exception_queue=None):
+ with mock.patch.object(threading, 'Thread') as Thread:
+ thread_instance = mock.MagicMock()
+ Thread.return_value = thread_instance
+ st = thread.SessionThread(
+ initialized_session=session,
+ job_queue=job_queue,
+ response_queue=response_queue,
+ exception_queue=exception_queue,
+ )
+
+ return (st, thread_instance, Thread)
+
+
+class TestSessionThread(unittest.TestCase):
+
+ """Tests for requests_toolbelt.threaded.thread.SessionThread."""
+
+ def test_thread_initialization(self):
+ """Test the way a SessionThread is initialized.
+
+ We want to ensure that we creat a thread with a name generated by the
+ uuid module, and that we pass the right method to use as a target.
+ """
+ with mock.patch.object(uuid, 'uuid4', return_value='test'):
+ (st, thread_instance, Thread) = _initialize_a_session_thread()
+
+ Thread.assert_called_once_with(target=st._make_request, name='test')
+ assert thread_instance.daemon is True
+ assert thread_instance._state is 0
+ thread_instance.start.assert_called_once_with()
+
+ def test_is_alive_proxies_to_worker(self):
+ """Test that we proxy the is_alive method to the Thread."""
+ with mock.patch.object(threading, 'Thread') as Thread:
+ thread_instance = mock.MagicMock()
+ Thread.return_value = thread_instance
+ st = thread.SessionThread(None, None, None, None)
+
+ st.is_alive()
+ thread_instance.is_alive.assert_called_once_with()
+
+ def test_join_proxies_to_worker(self):
+ """Test that we proxy the join method to the Thread."""
+ st, thread_instance, _ = _initialize_a_session_thread()
+
+ st.join()
+ thread_instance.join.assert_called_once_with()
+
+ def test_handle_valid_request(self):
+ """Test that a response is added to the right queue."""
+ session, job_queue, response_queue, exception_queue = _make_mocks()
+ response = mock.MagicMock()
+ session.request.return_value = response
+
+ st, _, _ = _initialize_a_session_thread(
+ session, job_queue, response_queue, exception_queue)
+
+ st._handle_request({'method': 'GET', 'url': 'http://example.com'})
+ session.request.assert_called_once_with(
+ method='GET',
+ url='http://example.com'
+ )
+
+ response_queue.put.assert_called_once_with(
+ ({'method': 'GET', 'url': 'http://example.com'}, response)
+ )
+ assert exception_queue.put.called is False
+ assert job_queue.get.called is False
+ assert job_queue.get_nowait.called is False
+ assert job_queue.get_nowait.called is False
+ assert job_queue.task_done.called is True
+
+ def test_handle_invalid_request(self):
+ """Test that exceptions from requests are added to the right queue."""
+ session, job_queue, response_queue, exception_queue = _make_mocks()
+ exception = requests.exceptions.InvalidURL()
+
+ def _side_effect(*args, **kwargs):
+ raise exception
+
+ # Make the request raise an exception
+ session.request.side_effect = _side_effect
+
+ st, _, _ = _initialize_a_session_thread(
+ session, job_queue, response_queue, exception_queue)
+
+ st._handle_request({'method': 'GET', 'url': 'http://example.com'})
+ session.request.assert_called_once_with(
+ method='GET',
+ url='http://example.com'
+ )
+
+ exception_queue.put.assert_called_once_with(
+ ({'method': 'GET', 'url': 'http://example.com'}, exception)
+ )
+ assert response_queue.put.called is False
+ assert job_queue.get.called is False
+ assert job_queue.get_nowait.called is False
+ assert job_queue.get_nowait.called is False
+ assert job_queue.task_done.called is True
+
+ def test_make_request(self):
+ """Test that _make_request exits when the queue is Empty."""
+ job_queue = next(_make_mocks())
+ job_queue.get_nowait.side_effect = queue.Empty()
+
+ st, _, _ = _initialize_a_session_thread(job_queue=job_queue)
+ st._make_request()
+
+ job_queue.get_nowait.assert_called_once_with()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b399ebd
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,55 @@
+[tox]
+envlist = py27,py33,py34,py35,pypy,{py27,py34}-flake8,docstrings
+
+[testenv]
+pip_pre = False
+deps =
+ requests{env:REQUESTS_VERSION:>=2.0.1,<3.0.0}
+ pytest
+ mock
+ betamax>0.5.0
+commands = py.test {posargs}
+
+[testenv:py27-flake8]
+basepython = python2.7
+deps =
+ flake8
+commands = flake8 {posargs} requests_toolbelt
+
+[testenv:py34-flake8]
+basepython = python3.4
+deps =
+ flake8
+commands = flake8 {posargs} requests_toolbelt
+
+[testenv:docstrings]
+deps =
+ flake8
+ flake8-docstrings
+commands = flake8 {posargs} requests_toolbelt
+
+[testenv:docs]
+deps =
+ sphinx>=1.3.0
+ sphinx_rtd_theme
+ .
+commands =
+ sphinx-build -E -c docs -b html docs/ docs/_build/html
+
+[testenv:readme]
+deps =
+ readme_renderer
+commands =
+ python setup.py check -m -r -s
+
+[testenv:release]
+deps =
+ twine >= 1.4.0
+ wheel
+commands =
+ python setup.py sdist bdist_wheel
+ twine upload --skip-existing dist/*
+
+[pytest]
+addopts = -q
+norecursedirs = *.egg .git .* _*