pax_global_header00006660000000000000000000000064126422405330014513gustar00rootroot0000000000000052 comment=17740df0d5f5a8d4a0b3d3037f3ae48e746cf12d betamax-0.5.1/000077500000000000000000000000001264224053300131375ustar00rootroot00000000000000betamax-0.5.1/AUTHORS.rst000066400000000000000000000004321264224053300150150ustar00rootroot00000000000000Development Lead ---------------- - Ian Cordasco Requests ```````` - Kenneth Reitz Design Advice ------------- - Cory Benfield Contributors ------------ - Marc Abramowitz (@msabramo) - Bryce Boe (@bboe) betamax-0.5.1/HISTORY.rst000066400000000000000000000100541264224053300150320ustar00rootroot00000000000000History ======= 0.5.1 - 2015-10-24 ------------------ - Fix bugs with requests 2.8.x integration - Fix bugs with older versions of requests that were missing an HTTPHeaderDict implementation 0.5.0 - 2015-07-15 ------------------ - Add unittest integration in ``betamax.fixtures.unittest`` - Add pytest integration in ``betamax.fixtures.pytest`` - Add a decorator as a short cut for ``use_cassette`` - Fix bug where body bytes were not always encoded on Python 3.2+ Fixed by @bboe 0.4.2 - 2015-04-18 ------------------ - Fix issue #58 reported by @bboe Multiple cookies were not being properly stored or replayed after being recorded. - @leighlondon converted ``__all__`` to a tuple 0.4.1 - 2014-09-24 ------------------ - Fix issue #39 reported by @buttscicles This bug did not properly parse the Set-Cookie header with multiple cookies when replaying a recorded response. 0.4.0 - 2014-07-29 ------------------ - Allow the user to pass placeholders to ``Betamax#use_cassette``. - Include Betamax's version number in cassettes 0.3.2 - 2014-06-05 ------------------ - Fix request and response bodies courtesy of @dgouldin 0.3.1 - 2014-05-28 ------------------ - Fix GitHub Issue #35 - Placeholders were not being properly applied to request bodies. This release fixes that so placeholders are now behave as expected with recorded request bodies. 0.3.0 - 2014-05-23 ------------------ - Add ``Betamax#start`` and ``Betamax#stop`` to allow users to start recording and stop without using a context-manager. - Add ``digest-auth`` matcher to help users match the right request when using requests' ``HTTPDigestAuth``. - Reorganize and refactor the cassettes, matchers, and serializers modules. - Refactor some portions of code a bit. - ``Cassette.cassette_name`` no longer is the relative path to the file in which the cassette is saved. To access that information use ``Cassette.cassette_path``. The ``cassette_name`` attribute is now the name that you pass to ``Betamax#use_cassette``. 0.2.0 - 2014-04-12 ------------------ - Fix bug where new interactions recorded under ``new_episodes`` or ``all`` were not actually saved to disk. - Match URIs in a far more intelligent way. - Use the Session's original adapters when making new requests In the event the Session has a custom adapter mounted, e.g., the SSLAdapter in requests-toolbelt, then we should probably use that. - Add ``on_init`` hook to ``BaseMatcher`` so matcher authors can customize initialization - Add support for custom Serialization formats. See the docs for more info. - Add support for preserving exact body bytes. - Deprecate ``serialize`` keyword to ``Betamax#use_cassette`` in preference for ``serialize_with`` (to be more similar to VCR). 0.1.6 - 2013-12-07 ------------------ - Fix how global settings and per-invocation options are persisted and honored. (#10) - Support ``match_requests_on`` as a parameter sent to ``Betamax#use_cassette``. (No issue) 0.1.5 - 2013-09-27 ------------------ - Make sure what we pass to ``base64.b64decode`` is a bytes object 0.1.4 - 2013-09-27 ------------------ - Do not try to sanitize something that may not exist. 0.1.3 - 2013-09-27 ------------------ - Fix issue when response has a Content-Encoding of gzip and we need to preserve the original bytes of the message. 0.1.2 - 2013-09-21 ------------------ - Fix issues with how requests parses cookies out of responses - Fix unicode issues with ``Response#text`` (trying to use ``Response#json`` raises exception because it cannot use string decoding on a unicode string) 0.1.1 - 2013-09-19 ------------------ - Fix issue where there is a unicode character not in ``range(128)`` 0.1.0 - 2013-09-17 ------------------ - Initial Release - Support for VCR generated cassettes (JSON only) - Support for ``re_record_interval`` - Support for the ``once``, ``all``, ``new_episodes``, ``all`` cassette modes - Support for filtering sensitive data - Support for the following methods of request matching: - Method - URI - Host - Path - Query String - Body - Headers betamax-0.5.1/LICENSE000066400000000000000000000011041264224053300141400ustar00rootroot00000000000000Copyright 2013 Ian Cordasco 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. betamax-0.5.1/MANIFEST.in000066400000000000000000000002321264224053300146720ustar00rootroot00000000000000include README.rst include LICENSE include HISTORY.rst include AUTHORS.rst recursive-include docs/ recursive-include tests/ prune *.pyc prune docs/_build betamax-0.5.1/PKG-INFO000066400000000000000000000246451264224053300142470ustar00rootroot00000000000000Metadata-Version: 1.1 Name: betamax Version: 0.5.1 Summary: A VCR imitation for python-requests Home-page: https://github.com/sigmavirus24/betamax Author: Ian Cordasco Author-email: graffatcolmingov@gmail.com License: Copyright 2013 Ian Cordasco 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. Description: betamax ======= Betamax is a VCR_ imitation for requests. This will make mocking out requests much easier. It is tested on `Travis CI`_. Put in a more humorous way: "Betamax records your HTTP interactions so the NSA does not have to." Example Use ----------- .. code-block:: python from betamax import Betamax from requests import Session from unittest import TestCase with Betamax.configure() as config: config.cassette_library_dir = 'tests/fixtures/cassettes' class TestGitHubAPI(TestCase): def setUp(self): self.session = Session() self.headers.update(...) # Set the cassette in a line other than the context declaration def test_user(self): with Betamax(self.session) as vcr: vcr.use_cassette('user') resp = self.session.get('https://api.github.com/user', auth=('user', 'pass')) assert resp.json()['login'] is not None # Set the cassette in line with the context declaration def test_repo(self): with Betamax(self.session).use_cassette('repo'): resp = self.session.get( 'https://api.github.com/repos/sigmavirus24/github3.py' ) assert resp.json()['owner'] != {} What does it even do? --------------------- If you are unfamiliar with VCR_, you might need a better explanation of what Betamax does. Betamax intercepts every request you make and attempts to find a matching request that has already been intercepted and recorded. Two things can then happen: 1. If there is a matching request, it will return the response that is associated with it. 2. If there is **not** a matching request and it is allowed to record new responses, it will make the request, record the response and return the response. Recorded requests and corresponding responses - also known as interactions - are stored in files called cassettes. (An example cassette can be seen in the `examples section of the documentation`_.) The directory you store your cassettes in is called your library, or your `cassette library`_. VCR Cassette Compatibility -------------------------- Betamax can use any VCR-recorded cassette as of this point in time. The only caveat is that python-requests returns a URL on each response. VCR does not store that in a cassette now but we will. Any VCR-recorded cassette used to playback a response will unfortunately not have a URL attribute on responses that are returned. This is a minor annoyance but not something that can be fixed. Contributing ------------ You can check out the project board on waffle.io_ to see what the status of each issue is. .. _VCR: https://github.com/vcr/vcr .. _Travis CI: https://travis-ci.org/sigmavirus24/betamax .. _waffle.io: https://waffle.io/sigmavirus24/betamax .. _examples section of the documentation: http://betamax.readthedocs.org/en/latest/api.html#examples .. _cassette library: http://betamax.readthedocs.org/en/latest/cassettes.html History ======= 0.5.1 - 2015-10-24 ------------------ - Fix bugs with requests 2.8.x integration - Fix bugs with older versions of requests that were missing an HTTPHeaderDict implementation 0.5.0 - 2015-07-15 ------------------ - Add unittest integration in ``betamax.fixtures.unittest`` - Add pytest integration in ``betamax.fixtures.pytest`` - Add a decorator as a short cut for ``use_cassette`` - Fix bug where body bytes were not always encoded on Python 3.2+ Fixed by @bboe 0.4.2 - 2015-04-18 ------------------ - Fix issue #58 reported by @bboe Multiple cookies were not being properly stored or replayed after being recorded. - @leighlondon converted ``__all__`` to a tuple 0.4.1 - 2014-09-24 ------------------ - Fix issue #39 reported by @buttscicles This bug did not properly parse the Set-Cookie header with multiple cookies when replaying a recorded response. 0.4.0 - 2014-07-29 ------------------ - Allow the user to pass placeholders to ``Betamax#use_cassette``. - Include Betamax's version number in cassettes 0.3.2 - 2014-06-05 ------------------ - Fix request and response bodies courtesy of @dgouldin 0.3.1 - 2014-05-28 ------------------ - Fix GitHub Issue #35 - Placeholders were not being properly applied to request bodies. This release fixes that so placeholders are now behave as expected with recorded request bodies. 0.3.0 - 2014-05-23 ------------------ - Add ``Betamax#start`` and ``Betamax#stop`` to allow users to start recording and stop without using a context-manager. - Add ``digest-auth`` matcher to help users match the right request when using requests' ``HTTPDigestAuth``. - Reorganize and refactor the cassettes, matchers, and serializers modules. - Refactor some portions of code a bit. - ``Cassette.cassette_name`` no longer is the relative path to the file in which the cassette is saved. To access that information use ``Cassette.cassette_path``. The ``cassette_name`` attribute is now the name that you pass to ``Betamax#use_cassette``. 0.2.0 - 2014-04-12 ------------------ - Fix bug where new interactions recorded under ``new_episodes`` or ``all`` were not actually saved to disk. - Match URIs in a far more intelligent way. - Use the Session's original adapters when making new requests In the event the Session has a custom adapter mounted, e.g., the SSLAdapter in requests-toolbelt, then we should probably use that. - Add ``on_init`` hook to ``BaseMatcher`` so matcher authors can customize initialization - Add support for custom Serialization formats. See the docs for more info. - Add support for preserving exact body bytes. - Deprecate ``serialize`` keyword to ``Betamax#use_cassette`` in preference for ``serialize_with`` (to be more similar to VCR). 0.1.6 - 2013-12-07 ------------------ - Fix how global settings and per-invocation options are persisted and honored. (#10) - Support ``match_requests_on`` as a parameter sent to ``Betamax#use_cassette``. (No issue) 0.1.5 - 2013-09-27 ------------------ - Make sure what we pass to ``base64.b64decode`` is a bytes object 0.1.4 - 2013-09-27 ------------------ - Do not try to sanitize something that may not exist. 0.1.3 - 2013-09-27 ------------------ - Fix issue when response has a Content-Encoding of gzip and we need to preserve the original bytes of the message. 0.1.2 - 2013-09-21 ------------------ - Fix issues with how requests parses cookies out of responses - Fix unicode issues with ``Response#text`` (trying to use ``Response#json`` raises exception because it cannot use string decoding on a unicode string) 0.1.1 - 2013-09-19 ------------------ - Fix issue where there is a unicode character not in ``range(128)`` 0.1.0 - 2013-09-17 ------------------ - Initial Release - Support for VCR generated cassettes (JSON only) - Support for ``re_record_interval`` - Support for the ``once``, ``all``, ``new_episodes``, ``all`` cassette modes - Support for filtering sensitive data - Support for the following methods of request matching: - Method - URI - Host - Path - Query String - Body - Headers Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython betamax-0.5.1/README.rst000066400000000000000000000060321264224053300146270ustar00rootroot00000000000000betamax ======= Betamax is a VCR_ imitation for requests. This will make mocking out requests much easier. It is tested on `Travis CI`_. Put in a more humorous way: "Betamax records your HTTP interactions so the NSA does not have to." Example Use ----------- .. code-block:: python from betamax import Betamax from requests import Session from unittest import TestCase with Betamax.configure() as config: config.cassette_library_dir = 'tests/fixtures/cassettes' class TestGitHubAPI(TestCase): def setUp(self): self.session = Session() self.headers.update(...) # Set the cassette in a line other than the context declaration def test_user(self): with Betamax(self.session) as vcr: vcr.use_cassette('user') resp = self.session.get('https://api.github.com/user', auth=('user', 'pass')) assert resp.json()['login'] is not None # Set the cassette in line with the context declaration def test_repo(self): with Betamax(self.session).use_cassette('repo'): resp = self.session.get( 'https://api.github.com/repos/sigmavirus24/github3.py' ) assert resp.json()['owner'] != {} What does it even do? --------------------- If you are unfamiliar with VCR_, you might need a better explanation of what Betamax does. Betamax intercepts every request you make and attempts to find a matching request that has already been intercepted and recorded. Two things can then happen: 1. If there is a matching request, it will return the response that is associated with it. 2. If there is **not** a matching request and it is allowed to record new responses, it will make the request, record the response and return the response. Recorded requests and corresponding responses - also known as interactions - are stored in files called cassettes. (An example cassette can be seen in the `examples section of the documentation`_.) The directory you store your cassettes in is called your library, or your `cassette library`_. VCR Cassette Compatibility -------------------------- Betamax can use any VCR-recorded cassette as of this point in time. The only caveat is that python-requests returns a URL on each response. VCR does not store that in a cassette now but we will. Any VCR-recorded cassette used to playback a response will unfortunately not have a URL attribute on responses that are returned. This is a minor annoyance but not something that can be fixed. Contributing ------------ You can check out the project board on waffle.io_ to see what the status of each issue is. .. _VCR: https://github.com/vcr/vcr .. _Travis CI: https://travis-ci.org/sigmavirus24/betamax .. _waffle.io: https://waffle.io/sigmavirus24/betamax .. _examples section of the documentation: http://betamax.readthedocs.org/en/latest/api.html#examples .. _cassette library: http://betamax.readthedocs.org/en/latest/cassettes.html betamax-0.5.1/betamax.egg-info/000077500000000000000000000000001264224053300162525ustar00rootroot00000000000000betamax-0.5.1/betamax.egg-info/PKG-INFO000066400000000000000000000246451264224053300173620ustar00rootroot00000000000000Metadata-Version: 1.1 Name: betamax Version: 0.5.1 Summary: A VCR imitation for python-requests Home-page: https://github.com/sigmavirus24/betamax Author: Ian Cordasco Author-email: graffatcolmingov@gmail.com License: Copyright 2013 Ian Cordasco 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. Description: betamax ======= Betamax is a VCR_ imitation for requests. This will make mocking out requests much easier. It is tested on `Travis CI`_. Put in a more humorous way: "Betamax records your HTTP interactions so the NSA does not have to." Example Use ----------- .. code-block:: python from betamax import Betamax from requests import Session from unittest import TestCase with Betamax.configure() as config: config.cassette_library_dir = 'tests/fixtures/cassettes' class TestGitHubAPI(TestCase): def setUp(self): self.session = Session() self.headers.update(...) # Set the cassette in a line other than the context declaration def test_user(self): with Betamax(self.session) as vcr: vcr.use_cassette('user') resp = self.session.get('https://api.github.com/user', auth=('user', 'pass')) assert resp.json()['login'] is not None # Set the cassette in line with the context declaration def test_repo(self): with Betamax(self.session).use_cassette('repo'): resp = self.session.get( 'https://api.github.com/repos/sigmavirus24/github3.py' ) assert resp.json()['owner'] != {} What does it even do? --------------------- If you are unfamiliar with VCR_, you might need a better explanation of what Betamax does. Betamax intercepts every request you make and attempts to find a matching request that has already been intercepted and recorded. Two things can then happen: 1. If there is a matching request, it will return the response that is associated with it. 2. If there is **not** a matching request and it is allowed to record new responses, it will make the request, record the response and return the response. Recorded requests and corresponding responses - also known as interactions - are stored in files called cassettes. (An example cassette can be seen in the `examples section of the documentation`_.) The directory you store your cassettes in is called your library, or your `cassette library`_. VCR Cassette Compatibility -------------------------- Betamax can use any VCR-recorded cassette as of this point in time. The only caveat is that python-requests returns a URL on each response. VCR does not store that in a cassette now but we will. Any VCR-recorded cassette used to playback a response will unfortunately not have a URL attribute on responses that are returned. This is a minor annoyance but not something that can be fixed. Contributing ------------ You can check out the project board on waffle.io_ to see what the status of each issue is. .. _VCR: https://github.com/vcr/vcr .. _Travis CI: https://travis-ci.org/sigmavirus24/betamax .. _waffle.io: https://waffle.io/sigmavirus24/betamax .. _examples section of the documentation: http://betamax.readthedocs.org/en/latest/api.html#examples .. _cassette library: http://betamax.readthedocs.org/en/latest/cassettes.html History ======= 0.5.1 - 2015-10-24 ------------------ - Fix bugs with requests 2.8.x integration - Fix bugs with older versions of requests that were missing an HTTPHeaderDict implementation 0.5.0 - 2015-07-15 ------------------ - Add unittest integration in ``betamax.fixtures.unittest`` - Add pytest integration in ``betamax.fixtures.pytest`` - Add a decorator as a short cut for ``use_cassette`` - Fix bug where body bytes were not always encoded on Python 3.2+ Fixed by @bboe 0.4.2 - 2015-04-18 ------------------ - Fix issue #58 reported by @bboe Multiple cookies were not being properly stored or replayed after being recorded. - @leighlondon converted ``__all__`` to a tuple 0.4.1 - 2014-09-24 ------------------ - Fix issue #39 reported by @buttscicles This bug did not properly parse the Set-Cookie header with multiple cookies when replaying a recorded response. 0.4.0 - 2014-07-29 ------------------ - Allow the user to pass placeholders to ``Betamax#use_cassette``. - Include Betamax's version number in cassettes 0.3.2 - 2014-06-05 ------------------ - Fix request and response bodies courtesy of @dgouldin 0.3.1 - 2014-05-28 ------------------ - Fix GitHub Issue #35 - Placeholders were not being properly applied to request bodies. This release fixes that so placeholders are now behave as expected with recorded request bodies. 0.3.0 - 2014-05-23 ------------------ - Add ``Betamax#start`` and ``Betamax#stop`` to allow users to start recording and stop without using a context-manager. - Add ``digest-auth`` matcher to help users match the right request when using requests' ``HTTPDigestAuth``. - Reorganize and refactor the cassettes, matchers, and serializers modules. - Refactor some portions of code a bit. - ``Cassette.cassette_name`` no longer is the relative path to the file in which the cassette is saved. To access that information use ``Cassette.cassette_path``. The ``cassette_name`` attribute is now the name that you pass to ``Betamax#use_cassette``. 0.2.0 - 2014-04-12 ------------------ - Fix bug where new interactions recorded under ``new_episodes`` or ``all`` were not actually saved to disk. - Match URIs in a far more intelligent way. - Use the Session's original adapters when making new requests In the event the Session has a custom adapter mounted, e.g., the SSLAdapter in requests-toolbelt, then we should probably use that. - Add ``on_init`` hook to ``BaseMatcher`` so matcher authors can customize initialization - Add support for custom Serialization formats. See the docs for more info. - Add support for preserving exact body bytes. - Deprecate ``serialize`` keyword to ``Betamax#use_cassette`` in preference for ``serialize_with`` (to be more similar to VCR). 0.1.6 - 2013-12-07 ------------------ - Fix how global settings and per-invocation options are persisted and honored. (#10) - Support ``match_requests_on`` as a parameter sent to ``Betamax#use_cassette``. (No issue) 0.1.5 - 2013-09-27 ------------------ - Make sure what we pass to ``base64.b64decode`` is a bytes object 0.1.4 - 2013-09-27 ------------------ - Do not try to sanitize something that may not exist. 0.1.3 - 2013-09-27 ------------------ - Fix issue when response has a Content-Encoding of gzip and we need to preserve the original bytes of the message. 0.1.2 - 2013-09-21 ------------------ - Fix issues with how requests parses cookies out of responses - Fix unicode issues with ``Response#text`` (trying to use ``Response#json`` raises exception because it cannot use string decoding on a unicode string) 0.1.1 - 2013-09-19 ------------------ - Fix issue where there is a unicode character not in ``range(128)`` 0.1.0 - 2013-09-17 ------------------ - Initial Release - Support for VCR generated cassettes (JSON only) - Support for ``re_record_interval`` - Support for the ``once``, ``all``, ``new_episodes``, ``all`` cassette modes - Support for filtering sensitive data - Support for the following methods of request matching: - Method - URI - Host - Path - Query String - Body - Headers Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython betamax-0.5.1/betamax.egg-info/SOURCES.txt000066400000000000000000000025721264224053300201440ustar00rootroot00000000000000AUTHORS.rst HISTORY.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py betamax/__init__.py betamax/adapter.py betamax/configure.py betamax/decorator.py betamax/exceptions.py betamax/options.py betamax/recorder.py betamax.egg-info/PKG-INFO betamax.egg-info/SOURCES.txt betamax.egg-info/dependency_links.txt betamax.egg-info/entry_points.txt betamax.egg-info/requires.txt betamax.egg-info/top_level.txt betamax/cassette/__init__.py betamax/cassette/cassette.py betamax/cassette/headers.py betamax/cassette/interaction.py betamax/cassette/mock_response.py betamax/cassette/util.py betamax/fixtures/__init__.py betamax/fixtures/pytest.py betamax/fixtures/unittest.py betamax/matchers/__init__.py betamax/matchers/base.py betamax/matchers/body.py betamax/matchers/digest_auth.py betamax/matchers/headers.py betamax/matchers/host.py betamax/matchers/method.py betamax/matchers/path.py betamax/matchers/query.py betamax/matchers/uri.py betamax/serializers/__init__.py betamax/serializers/base.py betamax/serializers/json_serializer.py betamax/serializers/proxy.py tests/integration/__init__.py tests/integration/helper.py tests/integration/test_backwards_compat.py tests/integration/test_fixtures.py tests/integration/test_multiple_cookies.py tests/integration/test_placeholders.py tests/integration/test_preserve_exact_body_bytes.py tests/integration/test_record_modes.py tests/integration/test_unicode.pybetamax-0.5.1/betamax.egg-info/dependency_links.txt000066400000000000000000000000011264224053300223200ustar00rootroot00000000000000 betamax-0.5.1/betamax.egg-info/entry_points.txt000066400000000000000000000000651264224053300215510ustar00rootroot00000000000000[pytest11] pytest-betamax = betamax.fixtures.pytest betamax-0.5.1/betamax.egg-info/requires.txt000066400000000000000000000000201264224053300206420ustar00rootroot00000000000000requests >= 2.0 betamax-0.5.1/betamax.egg-info/top_level.txt000066400000000000000000000000161264224053300210010ustar00rootroot00000000000000betamax tests betamax-0.5.1/betamax/000077500000000000000000000000001264224053300145605ustar00rootroot00000000000000betamax-0.5.1/betamax/__init__.py000066400000000000000000000012171264224053300166720ustar00rootroot00000000000000""" betamax ======= See http://betamax.rtfd.org/ for documentation. :copyright: (c) 2013 by Ian Cordasco :license: Apache 2.0, see LICENSE for more details """ from .decorator import use_cassette from .exceptions import BetamaxError from .matchers import BaseMatcher from .recorder import Betamax from .serializers import BaseSerializer __all__ = ('BetamaxError', 'Betamax', 'BaseMatcher', 'BaseSerializer', 'use_cassette') __author__ = 'Ian Cordasco' __copyright__ = 'Copyright 2013-2014 Ian Cordasco' __license__ = 'Apache 2.0' __title__ = 'betamax' __version__ = '0.5.1' __version_info__ = tuple(int(i) for i in __version__.split('.')) betamax-0.5.1/betamax/adapter.py000066400000000000000000000104551264224053300165570ustar00rootroot00000000000000import os from .cassette import Cassette from .exceptions import BetamaxError from datetime import datetime, timedelta from requests.adapters import BaseAdapter, HTTPAdapter class BetamaxAdapter(BaseAdapter): """This object is an implementation detail of the library. It is not meant to be a public API and is not exported as such. """ def __init__(self, **kwargs): super(BetamaxAdapter, self).__init__() self.cassette = None self.cassette_name = None self.old_adapters = kwargs.pop('old_adapters', {}) self.http_adapter = HTTPAdapter(**kwargs) self.serialize = None self.options = {} def cassette_exists(self): if self.cassette_name and os.path.exists(self.cassette_name): return True return False def close(self): self.http_adapter.close() def eject_cassette(self): if self.cassette: self.cassette.eject() self.cassette = None # Allow self.cassette to be garbage-collected def load_cassette(self, cassette_name, serialize, options): self.cassette_name = cassette_name self.serialize = serialize self.options.update(options.items()) placeholders = self.options.get('placeholders', []) default_options = Cassette.default_cassette_options match_requests_on = self.options.get( 'match_requests_on', default_options['match_requests_on'] ) preserve_exact_body_bytes = self.options.get( 'preserve_exact_body_bytes', ) self.cassette = Cassette( cassette_name, serialize, placeholders=placeholders, record_mode=self.options.get('record'), preserve_exact_body_bytes=preserve_exact_body_bytes, cassette_library_dir=self.options.get('cassette_library_dir') ) if 'record' in self.options: self.cassette.record_mode = self.options['record'] self.cassette.match_options = match_requests_on re_record_interval = timedelta.max if self.options.get('re_record_interval'): re_record_interval = timedelta(self.options['re_record_interval']) now = datetime.utcnow() if re_record_interval < (now - self.cassette.earliest_recorded_date): self.cassette.clear() def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): interaction = None if not self.cassette: raise BetamaxError('No cassette was specified or found.') if self.cassette.interactions: interaction = self.cassette.find_match(request) if not interaction and self.cassette.is_recording(): interaction = self.send_and_record( request, stream, timeout, verify, cert, proxies ) if not interaction: raise BetamaxError(unhandled_request_message(request, self.cassette)) resp = interaction.as_response() resp.connection = self return resp def send_and_record(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): adapter = self.find_adapter(request.url) response = adapter.send( request, stream=True, timeout=timeout, verify=verify, cert=cert, proxies=proxies ) self.cassette.save_interaction(response, request) return self.cassette.interactions[-1] def find_adapter(self, url): for (prefix, adapter) in self.old_adapters.items(): if url.lower().startswith(prefix): return adapter # Unlike in requests, we cannot possibly get this far. UNHANDLED_REQUEST_EXCEPTION = """A request was made that could not be handled. A request was made to {url} that could not be found in {cassette_file_path}. The settings on the cassette are: - record_mode: {cassette_record_mode} - match_options {cassette_match_options}. """ def unhandled_request_message(request, cassette): return UNHANDLED_REQUEST_EXCEPTION.format( url=request.url, cassette_file_path=cassette.cassette_name, cassette_record_mode=cassette.record_mode, cassette_match_options=cassette.match_options ) betamax-0.5.1/betamax/cassette/000077500000000000000000000000001264224053300163735ustar00rootroot00000000000000betamax-0.5.1/betamax/cassette/__init__.py000066400000000000000000000002531264224053300205040ustar00rootroot00000000000000from .cassette import Cassette from .interaction import Interaction from .mock_response import MockHTTPResponse __all__ = ('Cassette', 'Interaction', 'MockHTTPResponse') betamax-0.5.1/betamax/cassette/cassette.py000066400000000000000000000137171264224053300205710ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .interaction import Interaction from .util import (_option_from, serialize_prepared_request, serialize_response, timestamp) from betamax.matchers import matcher_registry from betamax.serializers import serializer_registry, SerializerProxy from datetime import datetime from functools import partial import os.path class Cassette(object): default_cassette_options = { 'record_mode': 'once', 'match_requests_on': ['method', 'uri'], 're_record_interval': None, 'placeholders': [], 'preserve_exact_body_bytes': False } def __init__(self, cassette_name, serialization_format, **kwargs): #: Short name of the cassette self.cassette_name = cassette_name self.serialized = None defaults = Cassette.default_cassette_options # Determine the record mode self.record_mode = _option_from('record_mode', kwargs, defaults) # Retrieve the serializer for this cassette self.serializer = SerializerProxy.find( serialization_format, kwargs.get('cassette_library_dir'), cassette_name ) self.cassette_path = self.serializer.cassette_path # Determine which placeholders to use self.placeholders = kwargs.get('placeholders') if not self.placeholders: self.placeholders = defaults['placeholders'] # Determine whether to preserve exact body bytes self.preserve_exact_body_bytes = _option_from( 'preserve_exact_body_bytes', kwargs, defaults ) # Initialize the interactions self.interactions = [] # Initialize the match options self.match_options = set() self.load_interactions() self.serializer.allow_serialization = self.is_recording() @staticmethod def can_be_loaded(cassette_library_dir, cassette_name, serialize_with, record_mode): # If we want to record a cassette we don't care if the file exists # yet recording = False if record_mode in ['once', 'all', 'new_episodes']: recording = True serializer = serializer_registry.get(serialize_with) if not serializer: raise ValueError( 'Serializer {0} is not registered with Betamax'.format( serialize_with )) cassette_path = serializer.generate_cassette_name( cassette_library_dir, cassette_name ) # Otherwise if we're only replaying responses, we should probably # have the cassette the user expects us to load and raise. return os.path.exists(cassette_path) or recording def clear(self): # Clear out the interactions self.interactions = [] # Serialize to the cassette file self._save_cassette() @property def earliest_recorded_date(self): """The earliest date of all of the interactions this cassette.""" if self.interactions: i = sorted(self.interactions, key=lambda i: i.recorded_at)[0] return i.recorded_at return datetime.now() def eject(self): self._save_cassette() def find_match(self, request): """Find a matching interaction based on the matchers and request. This uses all of the matchers selected via configuration or ``use_cassette`` and passes in the request currently in progress. :param request: ``requests.PreparedRequest`` :returns: :class:`Interaction ` """ opts = self.match_options # Curry those matchers matchers = [partial(matcher_registry[o].match, request) for o in opts] for i in self.interactions: if i.match(matchers): # If the interaction matches everything if self.record_mode == 'all': # If we're recording everything and there's a matching # interaction we want to overwrite it, so we remove it. self.interactions.remove(i) break return i # No matches. So sad. return None def is_empty(self): """Determine if the cassette was empty when loaded.""" return not self.serialized def is_recording(self): """Return whether the cassette is recording.""" values = { 'none': False, 'once': self.is_empty(), } return values.get(self.record_mode, True) def load_interactions(self): if self.serialized is None: self.serialized = self.serializer.deserialize() interactions = self.serialized.get('http_interactions', []) self.interactions = [Interaction(i) for i in interactions] for i in self.interactions: i.replace_all(self.placeholders, ('placeholder', 'replace')) i.deserialize() # this needs to happen *after* replace_all def sanitize_interactions(self): for i in self.interactions: i.replace_all(self.placeholders) def save_interaction(self, response, request): interaction = self.serialize_interaction(response, request) self.interactions.append(Interaction(interaction, response)) def serialize_interaction(self, response, request): return { 'request': serialize_prepared_request( request, self.preserve_exact_body_bytes ), 'response': serialize_response( response, self.preserve_exact_body_bytes ), 'recorded_at': timestamp(), } # Private methods def _save_cassette(self): from .. import __version__ self.sanitize_interactions() cassette_data = { 'http_interactions': [i.json for i in self.interactions], 'recorded_with': 'betamax/{0}'.format(__version__) } self.serializer.serialize(cassette_data) betamax-0.5.1/betamax/cassette/headers.py000066400000000000000000000175771264224053300204010ustar00rootroot00000000000000"""Backport of urllib3's HTTPHeaderDict for older versions of requests. This code was originally licensed under the MIT License and is copyrighted by Andrey Petrov and contributors to urllib3. This version was imported from: https://github.com/shazow/urllib3/blob/3bd63406bef7c16d007c17563b6af14582567d4b/urllib3/_collections.py """ import sys from collections import Mapping, MutableMapping __all__ = ('HTTPHeaderDict',) PY3 = sys.version_info >= (3, 0) 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 the 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 ({0} 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) betamax-0.5.1/betamax/cassette/interaction.py000066400000000000000000000065241264224053300212730ustar00rootroot00000000000000from .util import (deserialize_response, deserialize_prepared_request, from_list) from requests.cookies import extract_cookies_to_jar from datetime import datetime class Interaction(object): """The Interaction object represents the entirety of a single interaction. The interaction includes the date it was recorded, its JSON representation, and the ``requests.Response`` object complete with its ``request`` attribute. This object also handles the filtering of sensitive data. No methods or attributes on this object are considered public or part of the public API. As such they are entirely considered implementation details and subject to change. Using or relying on them is not wise or advised. """ def __init__(self, interaction, response=None): self.recorded_at = None self.json = interaction self.orig_response = response self.deserialize() def as_response(self): """Return the Interaction as a Response object.""" return self.recorded_response def deserialize(self): """Turn a serialized interaction into a Response.""" r = deserialize_response(self.json['response']) r.request = deserialize_prepared_request(self.json['request']) extract_cookies_to_jar(r.cookies, r.request, r.raw) self.recorded_at = datetime.strptime( self.json['recorded_at'], '%Y-%m-%dT%H:%M:%S' ) self.recorded_response = r def match(self, matchers): """Return whether this interaction is a match.""" request = self.json['request'] return all(m(request) for m in matchers) def replace(self, text_to_replace, placeholder): """Replace sensitive data in this interaction.""" self.replace_in_headers(text_to_replace, placeholder) self.replace_in_body(text_to_replace, placeholder) self.replace_in_uri(text_to_replace, placeholder) def replace_all(self, replacements, key_order=('replace', 'placeholder')): """Easy way to accept all placeholders registered.""" (replace_key, placeholder_key) = key_order for r in replacements: self.replace(r[replace_key], r[placeholder_key]) def replace_in_headers(self, text_to_replace, placeholder): for obj in ('request', 'response'): headers = self.json[obj]['headers'] for k, v in list(headers.items()): v = from_list(v) headers[k] = v.replace(text_to_replace, placeholder) def replace_in_body(self, text_to_replace, placeholder): for obj in ('request', 'response'): body = self.json[obj]['body'] old_style = hasattr(body, 'replace') if not old_style: body = body.get('string', '') if text_to_replace in body: body = body.replace(text_to_replace, placeholder) if old_style: self.json[obj]['body'] = body else: self.json[obj]['body']['string'] = body def replace_in_uri(self, text_to_replace, placeholder): for (obj, key) in (('request', 'uri'), ('response', 'url')): uri = self.json[obj][key] if text_to_replace in uri: self.json[obj][key] = uri.replace( text_to_replace, placeholder ) betamax-0.5.1/betamax/cassette/mock_response.py000066400000000000000000000015041264224053300216140ustar00rootroot00000000000000from email import parser, message import sys class MockHTTPResponse(object): def __init__(self, headers): from .util import coerce_content h = ["%s: %s" % (k, v) for k in headers for v in headers.getlist(k)] h = map(coerce_content, h) h = '\r\n'.join(h) if sys.version_info < (2, 7): h = h.encode() p = parser.Parser(EmailMessage) # Thanks to Python 3, we have to use the slightly more awful API below # mimetools was deprecated so we have to use email.message.Message # which takes no arguments in its initializer. self.msg = p.parsestr(h) self.msg.set_payload(h) def isclosed(self): return False class EmailMessage(message.Message): def getheaders(self, value, *args): return self.get_all(value, []) betamax-0.5.1/betamax/cassette/util.py000066400000000000000000000127361264224053300177330ustar00rootroot00000000000000from .mock_response import MockHTTPResponse from datetime import datetime from requests.models import PreparedRequest, Response from requests.packages.urllib3 import HTTPResponse from requests.structures import CaseInsensitiveDict from requests.status_codes import _codes from requests.cookies import RequestsCookieJar try: from requests.packages.urllib3._collections import HTTPHeaderDict except ImportError: from .headers import HTTPHeaderDict import base64 import io import sys def coerce_content(content, encoding=None): if hasattr(content, 'decode'): content = content.decode(encoding or 'utf-8', 'replace') return content def body_io(string, encoding=None): if hasattr(string, 'encode'): string = string.encode(encoding or 'utf-8') return io.BytesIO(string) def from_list(value): if isinstance(value, list): return value[0] return value def add_body(r, preserve_exact_body_bytes, body_dict): """Simple function which takes a response or request and coerces the body. This function adds either ``'string'`` or ``'base64_string'`` to ``body_dict``. If ``preserve_exact_body_bytes`` is ``True`` then it encodes the body as a base64 string and saves it like that. Otherwise, it saves the plain string. :param r: This is either a PreparedRequest instance or a Response instance. :param preserve_exact_body_bytes bool: Either True or False. :param body_dict dict: A dictionary already containing the encoding to be used. """ body = getattr(r, 'raw', getattr(r, 'body', None)) if hasattr(body, 'read'): body = body.read() if not body: body = '' if (preserve_exact_body_bytes or 'gzip' in r.headers.get('Content-Encoding', '')): if sys.version_info >= (3, 0) and hasattr(body, 'encode'): body = body.encode(body_dict['encoding'] or 'utf-8') body_dict['base64_string'] = base64.b64encode(body).decode() else: body_dict['string'] = coerce_content(body, body_dict['encoding']) def serialize_prepared_request(request, preserve_exact_body_bytes): headers = request.headers body = {'encoding': 'utf-8'} add_body(request, preserve_exact_body_bytes, body) return { 'body': body, 'headers': dict( (coerce_content(k, 'utf-8'), [v]) for (k, v) in headers.items() ), 'method': request.method, 'uri': request.url, } def deserialize_prepared_request(serialized): p = PreparedRequest() p._cookies = RequestsCookieJar() body = serialized['body'] if isinstance(body, dict): original_body = body.get('string') p.body = original_body or base64.b64decode( body.get('base64_string', '').encode()) else: p.body = body h = [(k, from_list(v)) for k, v in serialized['headers'].items()] p.headers = CaseInsensitiveDict(h) p.method = serialized['method'] p.url = serialized['uri'] return p def serialize_response(response, preserve_exact_body_bytes): body = {'encoding': response.encoding} add_body(response, preserve_exact_body_bytes, body) header_map = HTTPHeaderDict(response.raw.headers) headers = {} for header_name in header_map.keys(): headers[header_name] = header_map.getlist(header_name) return { 'body': body, 'headers': headers, 'status': {'code': response.status_code, 'message': response.reason}, 'url': response.url, } def deserialize_response(serialized): r = Response() r.encoding = serialized['body']['encoding'] header_dict = HTTPHeaderDict() for header_name, header_list in serialized['headers'].items(): if isinstance(header_list, list): for header_value in header_list: header_dict.add(header_name, header_value) else: header_dict.add(header_name, header_list) r.headers = CaseInsensitiveDict(header_dict) r.url = serialized.get('url', '') if 'status' in serialized: r.status_code = serialized['status']['code'] r.reason = serialized['status']['message'] else: r.status_code = serialized['status_code'] r.reason = _codes[r.status_code][0].upper() add_urllib3_response(serialized, r, header_dict) return r def add_urllib3_response(serialized, response, headers): if 'base64_string' in serialized['body']: body = io.BytesIO( base64.b64decode(serialized['body']['base64_string'].encode()) ) else: body = body_io(**serialized['body']) h = HTTPResponse( body, status=response.status_code, headers=headers, preload_content=False, original_response=MockHTTPResponse(headers) ) # NOTE(sigmavirus24): # urllib3 updated it's chunked encoding handling which breaks on recorded # responses. Since a recorded response cannot be streamed appropriately # for this handling to work, we can preserve the integrity of the data in # the response by forcing the chunked attribute to always be False. # This isn't pretty, but it is much better than munging a response. h.chunked = False response.raw = h def timestamp(): stamp = datetime.utcnow().isoformat() try: i = stamp.rindex('.') except ValueError: return stamp else: return stamp[:i] def _option_from(option, kwargs, defaults): value = kwargs.get(option) if value is None: value = defaults.get(option) return value betamax-0.5.1/betamax/configure.py000066400000000000000000000046051264224053300171200ustar00rootroot00000000000000from .cassette import Cassette class Configuration(object): """This object acts as a proxy to configure different parts of Betamax. You should only ever encounter this object when configuring the library as a whole. For example: .. code:: with Betamax.configure() as config: config.cassette_library_dir = 'tests/cassettes/' config.default_cassette_options['record_mode'] = 'once' config.default_cassette_options['match_requests_on'] = ['uri'] config.define_cassette_placeholder('', 'http://httpbin.org') config.preserve_exact_body_bytes = True """ CASSETTE_LIBRARY_DIR = 'vcr/cassettes' def __enter__(self): return self def __exit__(self, *args): pass def __setattr__(self, prop, value): if prop == 'preserve_exact_body_bytes': self.default_cassette_options[prop] = True else: super(Configuration, self).__setattr__(prop, value) @property def cassette_library_dir(self): """Retrieve and set the directory to store the cassettes in.""" return Configuration.CASSETTE_LIBRARY_DIR @cassette_library_dir.setter def cassette_library_dir(self, value): Configuration.CASSETTE_LIBRARY_DIR = value @property def default_cassette_options(self): """Retrieve and set the default cassette options. The options include: - ``match_requests_on`` - ``placeholders`` - ``re_record_interval`` - ``record_mode`` - ``preserve_exact_body_bytes`` Other options will be ignored. """ return Cassette.default_cassette_options @default_cassette_options.setter def default_cassette_options(self, value): Cassette.default_cassette_options = value def define_cassette_placeholder(self, placeholder, replace): """Define a placeholder value for some text. This also will replace the placeholder text with the text you wish it to use when replaying interactions from cassettes. :param str placeholder: (required), text to be used as a placeholder :param str replace: (required), text to be replaced or replacing the placeholder """ self.default_cassette_options['placeholders'].append({ 'placeholder': placeholder, 'replace': replace }) betamax-0.5.1/betamax/decorator.py000066400000000000000000000035721264224053300171230ustar00rootroot00000000000000import functools import unittest import requests from . import recorder def use_cassette(cassette_name, cassette_library_dir=None, default_cassette_options={}, **use_cassette_kwargs): """Provide a Betamax-wrapped Session for convenience. .. versionadded:: 0.5.0 This decorator can be used to get a plain Session that has been wrapped in Betamax. For example, .. code-block:: python from betamax.decorator import use_cassette @use_cassette('example-decorator', cassette_library_dir='.') def test_get(session): # do things with session :param str cassette_name: Name of the cassette file in which interactions will be stored. :param str cassette_library_dir: Directory in which cassette files will be stored. :param dict default_cassette_options: Dictionary of default cassette options to set for the cassette used when recording these interactions. :param \*\*use_cassette_kwargs: Keyword arguments passed to :meth:`~betamax.Betamax.use_cassette` """ def actual_decorator(func): @functools.wraps(func) def test_wrapper(*args, **kwargs): session = requests.Session() recr = recorder.Betamax( session=session, cassette_library_dir=cassette_library_dir, default_cassette_options=default_cassette_options ) if args: fst, args = args[0], args[1:] if isinstance(fst, unittest.TestCase): args = (fst, session) + args else: args = (session, fst) + args else: args = (session,) with recr.use_cassette(cassette_name, **use_cassette_kwargs): func(*args, **kwargs) return test_wrapper return actual_decorator betamax-0.5.1/betamax/exceptions.py000066400000000000000000000003001264224053300173040ustar00rootroot00000000000000class BetamaxError(Exception): def __init__(self, message): super(BetamaxError, self).__init__(message) def __repr__(self): return 'BetamaxError("%s")' % self.message betamax-0.5.1/betamax/fixtures/000077500000000000000000000000001264224053300164315ustar00rootroot00000000000000betamax-0.5.1/betamax/fixtures/__init__.py000066400000000000000000000000001264224053300205300ustar00rootroot00000000000000betamax-0.5.1/betamax/fixtures/pytest.py000066400000000000000000000026651264224053300203440ustar00rootroot00000000000000# -*- coding: utf-8 -*- """A set of fixtures to integrate Betamax with py.test. .. autofunction:: betamax_session """ from __future__ import absolute_import import pytest import requests from .. import recorder as betamax @pytest.fixture def betamax_session(request): """Generate a session that has Betamax already installed. This will create a new :class:`requests.Session` instance that is already using Betamax with a generated cassette name. The cassette name is generated by first using the module name from where the test is collected, then the class name (if it exists), and then the test function name. For example, if your test is in ``test_stuff.py`` and is the method ``TestStuffClass.test_stuff`` then your cassette name will be ``test_stuff_TestStuffClass_test_stuff``. :param request: A request object from pytest giving us context information for the fixture. :returns: An instantiated requests Session wrapped by Betamax. """ cassette_name = '' if request.module is not None: cassette_name += request.module.__name__ + '.' if request.cls is not None: cassette_name += request.cls.__name__ + '.' cassette_name += request.function.__name__ session = requests.Session() recorder = betamax.Betamax(session) recorder.use_cassette(cassette_name) recorder.start() request.addfinalizer(recorder.stop) return session betamax-0.5.1/betamax/fixtures/unittest.py000066400000000000000000000065171264224053300206730ustar00rootroot00000000000000"""Minimal :class:`unittest.TestCase` subclass adding Betamax integration. .. autoclass:: betamax.fixtures.unittest.BetamaxTestCase :members: When using Betamax with unittest, you can use the traditional style of Betamax covered in the documentation thoroughly, or you can use your fixture methods, :meth:`unittest.TestCase.setUp` and :meth:`unittest.TestCase.tearDown` to wrap entire tests in Betamax. Here's how you might use it: .. code-block:: python from betamax.fixtures import unittest from myapi import SessionManager class TestMyApi(unittest.BetamaxTestCase): def setUp(self): # Call BetamaxTestCase's setUp first to get a session super(TestMyApi, self).setUp() self.manager = SessionManager(self.session) def test_all_users(self): \"\"\"Retrieve all users from the API.\"\"\" for user in self.manager: # Make assertions or something Alternatively, if you are subclassing a :class:`requests.Session` to provide extra functionality, you can do something like this: .. code-block:: python from betamax.fixtures import unittest from myapi import Session, SessionManager class TestMyApi(unittest.BetamaxTestCase): SESSION_CLASS = Session # See above ... """ # NOTE(sigmavirus24): absolute_import is required to make import unittest work from __future__ import absolute_import import unittest import requests from .. import recorder __all__ = ('BetamaxTestCase',) class BetamaxTestCase(unittest.TestCase): """Betamax integration for unittest. .. versionadded:: 0.5.0 """ #: Class that is a subclass of :class:`requests.Session` SESSION_CLASS = requests.Session def generate_cassette_name(self): """Generates a cassette name for the current test. The default format is "%(classname)s.%(testMethodName)s" To change the default cassette format, override this method in a subclass. :returns: Cassette name for the current test. :rtype: str """ cls = getattr(self, '__class__') test = self._testMethodName return '{0}.{1}'.format(cls.__name__, test) def setUp(self): """Betamax-ified setUp fixture. This will call the superclass' setUp method *first* and then it will create a new :class:`requests.Session` and wrap that in a Betamax object to record it. At the end of ``setUp``, it will start recording. """ # Bail out early if the SESSION_CLASS isn't a subclass of # requests.Session self.assertTrue(issubclass(self.SESSION_CLASS, requests.Session)) # Make sure if the user is multiply inheriting that all setUps are # called. (If that confuses you, see: https://youtu.be/EiOglTERPEo) super(BetamaxTestCase, self).setUp() cassette_name = self.generate_cassette_name() self.session = self.SESSION_CLASS() self.recorder = recorder.Betamax(session=self.session) self.recorder.use_cassette(cassette_name) self.recorder.start() def tearDown(self): """Betamax-ified tearDown fixture. This will call the superclass' tearDown method *first* and then it will stop recording interactions. """ super(BetamaxTestCase, self).tearDown() self.recorder.stop() betamax-0.5.1/betamax/matchers/000077500000000000000000000000001264224053300163665ustar00rootroot00000000000000betamax-0.5.1/betamax/matchers/__init__.py000066400000000000000000000013161264224053300205000ustar00rootroot00000000000000matcher_registry = {} from .base import BaseMatcher from .body import BodyMatcher from .digest_auth import DigestAuthMatcher from .headers import HeadersMatcher from .host import HostMatcher from .method import MethodMatcher from .path import PathMatcher from .query import QueryMatcher from .uri import URIMatcher __all__ = ('BaseMatcher', 'BodyMatcher', 'DigestAuthMatcher', 'HeadersMatcher', 'HostMatcher', 'MethodMatcher', 'PathMatcher', 'QueryMatcher', 'URIMatcher') _matchers = [BodyMatcher, DigestAuthMatcher, HeadersMatcher, HostMatcher, MethodMatcher, PathMatcher, QueryMatcher, URIMatcher] matcher_registry.update(dict((m.name, m()) for m in _matchers)) del _matchers betamax-0.5.1/betamax/matchers/base.py000066400000000000000000000034401264224053300176530ustar00rootroot00000000000000# -*- coding: utf-8 -*- class BaseMatcher(object): """ Base class that ensures sub-classes that implement custom matchers can be registered and have the only method that is required. Usage: .. code-block:: python from betamax import Betamax, BaseMatcher class MyMatcher(BaseMatcher): name = 'my' def match(self, request, recorded_request): # My fancy matching algorithm Betamax.register_request_matcher(MyMatcher) The last line is absolutely necessary. The `match` method will be given a `requests.PreparedRequest` object and a dictionary. The dictionary always has the following keys: - url - method - body - headers """ name = None def __init__(self): if not self.name: raise ValueError('Matchers require names') self.on_init() def on_init(self): """Method to implement if you wish something to happen in ``__init__``. The return value is not checked and this is called at the end of ``__init__``. It is meant to provide the matcher author a way to perform things during initialization of the instance that would otherwise require them to override ``BaseMatcher.__init__``. """ return None def match(self, request, recorded_request): """A method that must be implemented by the user. :param PreparedRequest request: A requests PreparedRequest object :param dict recorded_request: A dictionary containing the serialized request in the cassette :returns bool: True if they match else False """ raise NotImplementedError('The match method must be implemented on' ' %s' % self.__class__.__name__) betamax-0.5.1/betamax/matchers/body.py000066400000000000000000000012121264224053300176710ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from ..cassette.util import deserialize_prepared_request class BodyMatcher(BaseMatcher): # Matches based on the body of the request name = 'body' def match(self, request, recorded_request): recorded_request = deserialize_prepared_request(recorded_request) if request.body: if isinstance(recorded_request.body, type(request.body)): request_body = request.body else: request_body = request.body.encode('utf-8') else: request_body = b'' return recorded_request.body == request_body betamax-0.5.1/betamax/matchers/digest_auth.py000066400000000000000000000031151264224053300212400ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from betamax.cassette.util import from_list class DigestAuthMatcher(BaseMatcher): """This matcher is provided to help those who need to use Digest Auth. .. note:: The code requests 2.0.1 uses to generate this header is different from the code that every requests version after it uses. Specifically, in 2.0.1 one of the parameters is ``qop=auth`` and every other version is ``qop="auth"``. Given that there's also an unsupported type of ``qop`` in requests, I've chosen not to ignore ore sanitize this. All cassettes recorded on 2.0.1 will need to be re-recorded for any requests version after it. This matcher also ignores the ``cnonce`` and ``response`` parameters. These parameters require the system time to be monkey-patched and that is out of the scope of betamax """ name = 'digest-auth' def match(self, request, recorded_request): request_digest = self.digest_parts(request.headers) recorded_digest = self.digest_parts(recorded_request['headers']) return request_digest == recorded_digest def digest_parts(self, headers): auth = headers.get('Authorization') or headers.get('authorization') if not auth: return None auth = from_list(auth).strip('Digest ') # cnonce and response will be based on the system time, which I will # not monkey-patch. excludes = ('cnonce', 'response') return [p for p in auth.split(', ') if not p.startswith(excludes)] betamax-0.5.1/betamax/matchers/headers.py000066400000000000000000000007511264224053300203560ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher class HeadersMatcher(BaseMatcher): # Matches based on the headers of the request name = 'headers' def match(self, request, recorded_request): return dict(request.headers) == self.flatten_headers(recorded_request) def flatten_headers(self, request): from betamax.cassette.util import from_list headers = request['headers'].items() return dict((k, from_list(v)) for (k, v) in headers) betamax-0.5.1/betamax/matchers/host.py000066400000000000000000000006211264224053300177140ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from requests.compat import urlparse class HostMatcher(BaseMatcher): # Matches based on the host of the request name = 'host' def match(self, request, recorded_request): request_host = urlparse(request.url).netloc recorded_host = urlparse(recorded_request['uri']).netloc return request_host == recorded_host betamax-0.5.1/betamax/matchers/method.py000066400000000000000000000004141264224053300202170ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher class MethodMatcher(BaseMatcher): # Matches based on the method of the request name = 'method' def match(self, request, recorded_request): return request.method == recorded_request['method'] betamax-0.5.1/betamax/matchers/path.py000066400000000000000000000006151264224053300176760ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from requests.compat import urlparse class PathMatcher(BaseMatcher): # Matches based on the path of the request name = 'path' def match(self, request, recorded_request): request_path = urlparse(request.url).path recorded_path = urlparse(recorded_request['uri']).path return request_path == recorded_path betamax-0.5.1/betamax/matchers/query.py000066400000000000000000000012751264224053300201120ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from requests.compat import urlparse try: from urlparse import parse_qs except ImportError: from urllib.parse import parse_qs class QueryMatcher(BaseMatcher): # Matches based on the query of the request name = 'query' def to_dict(self, query): """Turn the query string into a dictionary.""" return parse_qs(query or '') # Protect against None def match(self, request, recorded_request): request_query = self.to_dict(urlparse(request.url).query) recorded_query = self.to_dict( urlparse(recorded_request['uri']).query ) return request_query == recorded_query betamax-0.5.1/betamax/matchers/uri.py000066400000000000000000000020271264224053300175400ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from .query import QueryMatcher from requests.compat import urlparse class URIMatcher(BaseMatcher): # Matches based on the uri of the request name = 'uri' def on_init(self): # Get something we can use to match query strings with self.query_matcher = QueryMatcher().match def match(self, request, recorded_request): queries_match = self.query_matcher(request, recorded_request) request_url, recorded_url = request.url, recorded_request['uri'] return self.all_equal(request_url, recorded_url) and queries_match def parse(self, uri): parsed = urlparse(uri) return { 'scheme': parsed.scheme, 'netloc': parsed.netloc, 'path': parsed.path, 'fragment': parsed.fragment } def all_equal(self, new_uri, recorded_uri): new_parsed = self.parse(new_uri) recorded_parsed = self.parse(recorded_uri) return (new_parsed == recorded_parsed) betamax-0.5.1/betamax/options.py000066400000000000000000000046021264224053300166270ustar00rootroot00000000000000from .cassette import Cassette def validate_record(record): return record in ['all', 'new_episodes', 'none', 'once'] def validate_matchers(matchers): from betamax.matchers import matcher_registry available_matchers = list(matcher_registry.keys()) return all(m in available_matchers for m in matchers) def validate_serializer(serializer): from betamax.serializers import serializer_registry return serializer in list(serializer_registry.keys()) def validate_placeholders(placeholders): keys = ['placeholder', 'replace'] return all( sorted(list(p.keys())) == keys for p in placeholders ) def translate_cassette_options(): for (k, v) in Cassette.default_cassette_options.items(): yield (k, v) if k != 'record_mode' else ('record', v) class Options(object): valid_options = { 'match_requests_on': validate_matchers, 're_record_interval': lambda x: x is None or x > 0, 'record': validate_record, 'serialize': validate_serializer, # TODO: Remove this 'serialize_with': validate_serializer, 'preserve_exact_body_bytes': lambda x: x in [True, False], 'placeholders': validate_placeholders, } defaults = { 'match_requests_on': ['method', 'uri'], 're_record_interval': None, 'record': 'once', 'serialize': None, # TODO: Remove this 'serialize_with': 'json', 'preserve_exact_body_bytes': False, 'placeholders': [], } def __init__(self, data=None): self.data = data or {} self.validate() self.defaults = Options.defaults.copy() self.defaults.update(translate_cassette_options()) def __repr__(self): return 'Options(%s)' % self.data def __getitem__(self, key): return self.data.get(key, self.defaults.get(key)) def __setitem__(self, key, value): self.data[key] = value return value def __delitem__(self, key): del self.data[key] def __contains__(self, key): return key in self.data def items(self): return self.data.items() def validate(self): for key, value in list(self.data.items()): if key not in Options.valid_options: del self[key] else: is_valid = Options.valid_options[key] if not is_valid(value): del self[key] betamax-0.5.1/betamax/recorder.py000066400000000000000000000133521264224053300167430ustar00rootroot00000000000000# -*- coding: utf-8 -*- from . import matchers, serializers from .adapter import BetamaxAdapter from .cassette import Cassette from .configure import Configuration from .options import Options class Betamax(object): """This object contains the main API of the request-vcr library. This object is entirely a context manager so all you have to do is: .. code:: s = requests.Session() with Betamax(s) as vcr: vcr.use_cassette('example') r = s.get('https://httpbin.org/get') Or more concisely, you can do: .. code:: s = requests.Session() with Betamax(s).use_cassette('example') as vcr: r = s.get('https://httpbin.org/get') This object allows for the user to specify the cassette library directory and default cassette options. .. code:: s = requests.Session() with Betamax(s, cassette_library_dir='tests/cassettes') as vcr: vcr.use_cassette('example') r = s.get('https://httpbin.org/get') with Betamax(s, default_cassette_options={ 're_record_interval': 1000 }) as vcr: vcr.use_cassette('example') r = s.get('https://httpbin.org/get') """ def __init__(self, session, cassette_library_dir=None, default_cassette_options={}): #: Store the requests.Session object being wrapped. self.session = session #: Store the session's original adapters. self.http_adapters = session.adapters.copy() #: Create a new adapter to replace the existing ones self.betamax_adapter = BetamaxAdapter(old_adapters=self.http_adapters) # We need a configuration instance to make life easier self.config = Configuration() # Merge the new cassette options with the default ones self.config.default_cassette_options.update( default_cassette_options or {} ) # If it was passed in, use that instead. if cassette_library_dir: self.config.cassette_library_dir = cassette_library_dir def __enter__(self): self.start() return self def __exit__(self, *ex_args): self.stop() # ex_args comes through as the exception type, exception value and # exception traceback. If any of them are not None, we should probably # try to raise the exception and not muffle anything. if any(ex_args): # If you return False, Python will re-raise the exception for you return False @staticmethod def configure(): """Help to configure the library as a whole. .. code:: with Betamax.configure() as config: config.cassette_library_dir = 'tests/cassettes/' config.default_cassette_options['match_options'] = [ 'method', 'uri', 'headers' ] """ return Configuration() @property def current_cassette(self): """Return the cassette that is currently in use. :returns: :class:`Cassette ` """ return self.betamax_adapter.cassette @staticmethod def register_request_matcher(matcher_class): """Register a new request matcher. :param matcher_class: (required), this must sub-class :class:`BaseMatcher ` """ matchers.matcher_registry[matcher_class.name] = matcher_class() @staticmethod def register_serializer(serializer_class): """Register a new serializer. :param matcher_class: (required), this must sub-class :class:`BaseSerializer ` """ name = serializer_class.name serializers.serializer_registry[name] = serializer_class() # ❙▸ def start(self): """Start recording or replaying interactions.""" for k in self.http_adapters: self.session.mount(k, self.betamax_adapter) # ■ def stop(self): """Stop recording or replaying interactions.""" # No need to keep the cassette in memory any longer. self.betamax_adapter.eject_cassette() # On exit, we no longer wish to use our adapter and we want the # session to behave normally! Woooo! self.betamax_adapter.close() for (k, v) in self.http_adapters.items(): self.session.mount(k, v) def use_cassette(self, cassette_name, **kwargs): """Tell Betamax which cassette you wish to use for the context. :param str cassette_name: relative name, without the serialization format, of the cassette you wish Betamax would use :param str serialize_with: the format you want Betamax to serialize the cassette with :param str serialize: DEPRECATED the format you want Betamax to serialize the request and response data to and from """ kwargs = Options(kwargs) serialize = kwargs['serialize'] or kwargs['serialize_with'] kwargs['cassette_library_dir'] = self.config.cassette_library_dir can_load = Cassette.can_be_loaded( self.config.cassette_library_dir, cassette_name, serialize, kwargs['record'] ) if can_load: self.betamax_adapter.load_cassette(cassette_name, serialize, kwargs) else: # If we're not recording or replaying an existing cassette, we # should tell the user/developer that there is no cassette, only # Zuul raise ValueError('Cassette must have a valid name and may not be' ' None.') return self betamax-0.5.1/betamax/serializers/000077500000000000000000000000001264224053300171145ustar00rootroot00000000000000betamax-0.5.1/betamax/serializers/__init__.py000066400000000000000000000005361264224053300212310ustar00rootroot00000000000000# -*- coding: utf-8 -*- serializer_registry = {} from .base import BaseSerializer from .json_serializer import JSONSerializer from .proxy import SerializerProxy _serializers = [JSONSerializer] serializer_registry.update(dict((s.name, s()) for s in _serializers)) del _serializers __all__ = ('BaseSerializer', 'JSONSerializer', 'SerializerProxy') betamax-0.5.1/betamax/serializers/base.py000066400000000000000000000054621264224053300204070ustar00rootroot00000000000000# -*- coding: utf-8 -*- NOT_IMPLEMENTED_ERROR_MSG = ('This method must be implemented by classes' ' inheriting from BaseSerializer.') class BaseSerializer(object): """ Base Serializer class that provides an interface for other serializers. Usage: .. code-block:: python from betamax import Betamax, BaseSerializer class MySerializer(BaseSerializer): name = 'my' @staticmethod def generate_cassette_name(cassette_library_dir, cassette_name): # Generate a string that will give the relative path of a # cassette def serialize(self, cassette_data): # Take a dictionary and convert it to whatever def deserialize(self): # Uses a cassette file to return a dictionary with the # cassette information Betamax.register_serializer(MySerializer) The last line is absolutely necessary. """ name = None @staticmethod def generate_cassette_name(cassette_library_dir, cassette_name): raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MSG) def __init__(self): if not self.name: raise ValueError("Serializer's name attribute must be a string" " value, not None.") self.on_init() def on_init(self): """Method to implement if you wish something to happen in ``__init__``. The return value is not checked and this is called at the end of ``__init__``. It is meant to provide the matcher author a way to perform things during initialization of the instance that would otherwise require them to override ``BaseSerializer.__init__``. """ return None def serialize(self, cassette_data): """A method that must be implemented by the Serializer author. :param dict cassette_data: A dictionary with two keys: ``http_interactions``, ``recorded_with``. :returns: Serialized data as a string. """ raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MSG) def deserialize(self, cassette_data): """A method that must be implemented by the Serializer author. The return value is extremely important. If it is not empty, the dictionary returned must have the following structure:: { 'http_interactions': [{ # Interaction }, { # Interaction }], 'recorded_with': 'name of recorder' } :params str cassette_data: The data serialized as a string which needs to be deserialized. :returns: dictionary """ raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MSG) betamax-0.5.1/betamax/serializers/json_serializer.py000066400000000000000000000012321264224053300226660ustar00rootroot00000000000000from .base import BaseSerializer import json import os class JSONSerializer(BaseSerializer): # Serializes and deserializes a cassette to JSON name = 'json' @staticmethod def generate_cassette_name(cassette_library_dir, cassette_name): return os.path.join(cassette_library_dir, '{0}.{1}'.format(cassette_name, 'json')) def serialize(self, cassette_data): return json.dumps(cassette_data) def deserialize(self, cassette_data): try: deserialized_data = json.loads(cassette_data) except ValueError: deserialized_data = {} return deserialized_data betamax-0.5.1/betamax/serializers/proxy.py000066400000000000000000000044331264224053300206530ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .base import BaseSerializer import os class SerializerProxy(BaseSerializer): """ This is an internal implementation detail of the betamax library. No users implementing a serializer should be using this. Developers working on betamax need only understand that this handles the logic surrounding whether a cassette should be updated, overwritten, or created. It provides one consistent way for betamax to be confident in how it serializes the data it receives. It allows authors of Serializer classes to not have to duplicate how files are handled. It delegates the responsibility of actually serializing the data to those classes and handles the rest. """ def __init__(self, serializer, cassette_path, allow_serialization=False): self.proxied_serializer = serializer self.allow_serialization = allow_serialization self.cassette_path = cassette_path def _ensure_path_exists(self): if not os.path.exists(self.cassette_path): open(self.cassette_path, 'w+').close() @classmethod def find(cls, serialize_with, cassette_library_dir, cassette_name): from . import serializer_registry serializer = serializer_registry.get(serialize_with) if serializer is None: raise ValueError( 'No serializer registered for {0}'.format(serialize_with) ) cassette_path = cls.generate_cassette_name( serializer, cassette_library_dir, cassette_name ) return cls(serializer, cassette_path) @staticmethod def generate_cassette_name(serializer, cassette_library_dir, cassette_name): return serializer.generate_cassette_name( cassette_library_dir, cassette_name ) def serialize(self, cassette_data): if not self.allow_serialization: return self._ensure_path_exists() with open(self.cassette_path, 'w') as fd: fd.write(self.proxied_serializer.serialize(cassette_data)) def deserialize(self): self._ensure_path_exists() data = {} with open(self.cassette_path) as fd: data = self.proxied_serializer.deserialize(fd.read()) return data betamax-0.5.1/setup.cfg000066400000000000000000000001221264224053300147530ustar00rootroot00000000000000[wheel] universal = 1 [egg_info] tag_date = 0 tag_build = tag_svn_revision = 0 betamax-0.5.1/setup.py000066400000000000000000000034721264224053300146570ustar00rootroot00000000000000import os import re import sys from setuptools import setup, find_packages packages = find_packages(exclude=['tests']) requires = ['requests >= 2.0'] __version__ = '' with open('betamax/__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 if not __version__: raise RuntimeError('Cannot find version information') if sys.argv[-1] in ['submit', 'publish']: os.system("python setup.py sdist bdist_wheel upload") sys.exit() def data_for(filename): with open(filename) as fd: content = fd.read() return content setup( name="betamax", version=__version__, description="A VCR imitation for python-requests", long_description="\n\n".join([data_for("README.rst"), data_for("HISTORY.rst")]), license=data_for('LICENSE'), author="Ian Cordasco", author_email="graffatcolmingov@gmail.com", url="https://github.com/sigmavirus24/betamax", packages=packages, package_data={'': ['LICENSE', 'AUTHORS.rst']}, include_package_data=True, install_requires=requires, entry_points={ 'pytest11': ['pytest-betamax = betamax.fixtures.pytest'] }, classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved', 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: Implementation :: CPython', ] ) betamax-0.5.1/tests/000077500000000000000000000000001264224053300143015ustar00rootroot00000000000000betamax-0.5.1/tests/integration/000077500000000000000000000000001264224053300166245ustar00rootroot00000000000000betamax-0.5.1/tests/integration/__init__.py000066400000000000000000000000001264224053300207230ustar00rootroot00000000000000betamax-0.5.1/tests/integration/helper.py000066400000000000000000000005621264224053300204600ustar00rootroot00000000000000import os import unittest from requests import Session class IntegrationHelper(unittest.TestCase): cassette_created = True def setUp(self): self.cassette_path = None self.session = Session() def tearDown(self): if self.cassette_created: assert self.cassette_path is not None os.unlink(self.cassette_path) betamax-0.5.1/tests/integration/test_backwards_compat.py000066400000000000000000000035031264224053300235420ustar00rootroot00000000000000import betamax import copy from .helper import IntegrationHelper class TestBackwardsCompatibleSerialization(IntegrationHelper): def setUp(self): super(TestBackwardsCompatibleSerialization, self).setUp() self.cassette_created = False opts = betamax.cassette.Cassette.default_cassette_options self.original_defaults = copy.deepcopy(opts) with betamax.Betamax.configure() as config: config.define_cassette_placeholder('', 'nothing to replace') def tearDown(self): super(TestBackwardsCompatibleSerialization, self).setUp() Cassette = betamax.cassette.Cassette Cassette.default_cassette_options = self.original_defaults def test_can_deserialize_an_old_cassette(self): with betamax.Betamax(self.session).use_cassette('GitHub_emojis') as b: assert b.current_cassette is not None cassette = b.current_cassette assert len(cassette.interactions) > -1 def test_matches_old_request_data(self): with betamax.Betamax(self.session).use_cassette('GitHub_emojis'): r = self.session.get('https://api.github.com/emojis') assert r is not None def tests_populates_correct_fields_with_missing_data(self): with betamax.Betamax(self.session).use_cassette('GitHub_emojis'): r = self.session.get('https://api.github.com/emojis') assert r.reason == 'OK' assert r.status_code == 200 def tests_deserializes_old_cassette_headers(self): with betamax.Betamax(self.session).use_cassette('GitHub_emojis') as b: self.session.get('https://api.github.com/emojis') interaction = b.current_cassette.interactions[0].json header = interaction['request']['headers']['Accept'] assert not isinstance(header, list) betamax-0.5.1/tests/integration/test_fixtures.py000066400000000000000000000016521264224053300221120ustar00rootroot00000000000000import os.path import pytest @pytest.mark.usefixtures('betamax_session') class TestPyTestFixtures: @pytest.fixture(autouse=True) def setup(self, request): """After test hook to assert everything.""" def finalizer(): test_dir = os.path.abspath('.') cassette_name = ('tests.integration.test_fixtures.' # Module name 'TestPyTestFixtures.' # Class name 'test_pytest_fixture' # Test function name '.json') file_name = os.path.join(test_dir, 'tests', 'cassettes', cassette_name) assert os.path.exists(file_name) is True request.addfinalizer(finalizer) def test_pytest_fixture(self, betamax_session): """Exercise the fixture itself.""" resp = betamax_session.get('https://httpbin.org/get') assert resp.ok betamax-0.5.1/tests/integration/test_multiple_cookies.py000066400000000000000000000026551264224053300236140ustar00rootroot00000000000000import betamax from .helper import IntegrationHelper class TestMultipleCookies(IntegrationHelper): """Previously our handling of multiple instances of cookies was wrong. This set of tests is here to ensure that we properly serialize/deserialize the case where the client receives and betamax serializes multiple Set-Cookie headers. See the following for more information: - https://github.com/sigmavirus24/betamax/pull/60 - https://github.com/sigmavirus24/betamax/pull/59 - https://github.com/sigmavirus24/betamax/issues/58 """ def setUp(self): super(TestMultipleCookies, self).setUp() self.cassette_created = False def test_multiple_cookies(self): """Make a request to httpbin.org and verify we serialize it correctly. We should be able to see that the cookiejar on the session has the cookies properly parsed and distinguished. """ recorder = betamax.Betamax(self.session) cassette_name = 'test-multiple-cookies-regression' url = 'https://httpbin.org/cookies/set' cookies = { 'cookie0': 'value0', 'cookie1': 'value1', 'cookie2': 'value2', 'cookie3': 'value3', } with recorder.use_cassette(cassette_name): self.session.get(url, params=cookies) for name, value in cookies.items(): assert self.session.cookies[name] == value betamax-0.5.1/tests/integration/test_placeholders.py000066400000000000000000000030251264224053300227020ustar00rootroot00000000000000from betamax import Betamax from betamax.cassette import Cassette from copy import deepcopy from tests.integration.helper import IntegrationHelper original_cassette_options = deepcopy(Cassette.default_cassette_options) b64_foobar = 'Zm9vOmJhcg==' # base64.b64encode('foo:bar') class TestPlaceholders(IntegrationHelper): def setUp(self): super(TestPlaceholders, self).setUp() config = Betamax.configure() config.define_cassette_placeholder('', b64_foobar) def tearDown(self): super(TestPlaceholders, self).tearDown() Cassette.default_cassette_options = original_cassette_options def test_placeholders_work(self): placeholders = Cassette.default_cassette_options['placeholders'] placeholder = { 'placeholder': '', 'replace': b64_foobar } assert placeholders != [] assert placeholder in placeholders s = self.session cassette = None with Betamax(s).use_cassette('test_placeholders') as recorder: r = s.get('http://httpbin.org/get', auth=('foo', 'bar')) cassette = recorder.current_cassette assert r.status_code == 200 auth = r.json()['headers']['Authorization'] assert b64_foobar in auth #cassette.sanitize_interactions() self.cassette_path = cassette.cassette_path i = cassette.interactions[0] auth = i.json['request']['headers']['Authorization'] assert '' in auth betamax-0.5.1/tests/integration/test_preserve_exact_body_bytes.py000066400000000000000000000034471264224053300255070ustar00rootroot00000000000000from .helper import IntegrationHelper from betamax import Betamax from betamax.cassette import Cassette import copy class TestPreserveExactBodyBytes(IntegrationHelper): def test_preserve_exact_body_bytes_does_not_munge_response_content(self): # Do not delete this cassette after the test self.cassette_created = False with Betamax(self.session) as b: b.use_cassette('preserve_exact_bytes', preserve_exact_body_bytes=True, match_requests_on=['uri', 'method', 'body']) r = self.session.post('https://httpbin.org/post', data={'a': 1}) assert 'headers' in r.json() interaction = b.current_cassette.interactions[0].json assert 'base64_string' in interaction['request']['body'] assert 'base64_string' in interaction['response']['body'] class TestPreserveExactBodyBytesForAllCassettes(IntegrationHelper): def setUp(self): super(TestPreserveExactBodyBytesForAllCassettes, self).setUp() self.orig = copy.deepcopy(Cassette.default_cassette_options) self.cassette_created = False def tearDown(self): super(TestPreserveExactBodyBytesForAllCassettes, self).tearDown() Cassette.default_cassette_options = self.orig def test_preserve_exact_body_bytes(self): with Betamax.configure() as config: config.preserve_exact_body_bytes = True with Betamax(self.session) as b: b.use_cassette('global_preserve_exact_body_bytes') r = self.session.get('https://httpbin.org/get') assert 'headers' in r.json() interaction = b.current_cassette.interactions[0].json assert 'base64_string' in interaction['response']['body'] betamax-0.5.1/tests/integration/test_record_modes.py000066400000000000000000000120171264224053300227030ustar00rootroot00000000000000from betamax import Betamax, BetamaxError from tests.integration.helper import IntegrationHelper class TestRecordOnce(IntegrationHelper): def test_records_new_interaction(self): s = self.session with Betamax(s).use_cassette('test_record_once') as betamax: self.cassette_path = betamax.current_cassette.cassette_path assert betamax.current_cassette.is_empty() is True r = s.get('http://httpbin.org/get') assert r.status_code == 200 assert betamax.current_cassette.is_empty() is True assert betamax.current_cassette.interactions != [] def test_replays_response_from_cassette(self): s = self.session with Betamax(s).use_cassette('test_replays_response') as betamax: self.cassette_path = betamax.current_cassette.cassette_path assert betamax.current_cassette.is_empty() is True r0 = s.get('http://httpbin.org/get') assert r0.status_code == 200 assert betamax.current_cassette.interactions != [] assert len(betamax.current_cassette.interactions) == 1 r1 = s.get('http://httpbin.org/get') assert len(betamax.current_cassette.interactions) == 1 assert r1.status_code == 200 assert r0.headers == r1.headers assert r0.content == r1.content class TestRecordNone(IntegrationHelper): def test_raises_exception_when_no_interactions_present(self): s = self.session with Betamax(s) as betamax: betamax.use_cassette('test', record='none') self.cassette_created = False assert betamax.current_cassette is not None self.assertRaises(BetamaxError, s.get, 'http://httpbin.org/get') def test_record_none_does_not_create_cassettes(self): s = self.session with Betamax(s) as betamax: self.assertRaises(ValueError, betamax.use_cassette, 'test_record_none', record='none') self.cassette_created = False class TestRecordNewEpisodes(IntegrationHelper): def setUp(self): super(TestRecordNewEpisodes, self).setUp() with Betamax(self.session).use_cassette('test_record_new'): self.session.get('http://httpbin.org/get') self.session.get('http://httpbin.org/redirect/2') def test_records_new_events_with_existing_cassette(self): s = self.session opts = {'record': 'new_episodes'} with Betamax(s).use_cassette('test_record_new', **opts) as betamax: cassette = betamax.current_cassette self.cassette_path = cassette.cassette_path assert cassette.interactions != [] assert len(cassette.interactions) == 3 assert cassette.is_empty() is False s.get('https://httpbin.org/get') assert len(cassette.interactions) == 4 with Betamax(s).use_cassette('test_record_new') as betamax: cassette = betamax.current_cassette assert len(cassette.interactions) == 4 r = s.get('https://httpbin.org/get') assert r.status_code == 200 class TestRecordNewEpisodesCreatesCassettes(IntegrationHelper): def test_creates_new_cassettes(self): recorder = Betamax(self.session) opts = {'record': 'new_episodes'} cassette_name = 'test_record_new_makes_new_cassettes' with recorder.use_cassette(cassette_name, **opts) as betamax: self.cassette_path = betamax.current_cassette.cassette_path self.session.get('https://httpbin.org/get') class TestRecordAll(IntegrationHelper): def setUp(self): super(TestRecordAll, self).setUp() with Betamax(self.session).use_cassette('test_record_all'): self.session.get('http://httpbin.org/get') self.session.get('http://httpbin.org/redirect/2') def test_records_new_interactions(self): s = self.session opts = {'record': 'all'} with Betamax(s).use_cassette('test_record_all', **opts) as betamax: cassette = betamax.current_cassette self.cassette_path = cassette.cassette_path assert cassette.interactions != [] assert len(cassette.interactions) == 3 assert cassette.is_empty() is False s.post('http://httpbin.org/post', data={'foo': 'bar'}) assert len(cassette.interactions) == 4 with Betamax(s).use_cassette('test_record_all') as betamax: assert len(betamax.current_cassette.interactions) == 4 def test_replaces_old_interactions(self): s = self.session opts = {'record': 'all'} with Betamax(s).use_cassette('test_record_all', **opts) as betamax: cassette = betamax.current_cassette self.cassette_path = cassette.cassette_path assert cassette.interactions != [] assert len(cassette.interactions) == 3 assert cassette.is_empty() is False s.get('http://httpbin.org/get') assert len(cassette.interactions) == 3 betamax-0.5.1/tests/integration/test_unicode.py000066400000000000000000000007311264224053300216640ustar00rootroot00000000000000from betamax import Betamax from tests.integration.helper import IntegrationHelper class TestUnicode(IntegrationHelper): def test_unicode_is_saved_properly(self): s = self.session # https://github.com/kanzure/python-requestions/issues/4 url = 'http://www.amazon.com/review/RAYTXRF3122TO' with Betamax(s).use_cassette('test_unicode') as beta: self.cassette_path = beta.current_cassette.cassette_path s.get(url)