betamax-0.8.1/0000775000175000017500000000000013252047671014552 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/0000775000175000017500000000000013252047671016173 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/adapter.py0000664000175000017500000001447613040145704020170 0ustar icordascicordasc00000000000000""" betamax.adapter. ============== adapter for betamax """ import os from . import cassette from .exceptions import BetamaxError from datetime import datetime, timedelta from requests.adapters import BaseAdapter, HTTPAdapter _SENTINEL = object() 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): """Check if cassette exists on file system. :returns: bool -- True if exists, False otherwise """ if self.cassette_name and os.path.exists(self.cassette_name): return True return False def close(self): """Propagate close to underlying adapter.""" self.http_adapter.close() def eject_cassette(self): """Eject currently loaded cassette.""" if self.cassette: self.cassette.eject() self.cassette = None # Allow self.cassette to be garbage-collected def load_cassette(self, cassette_name, serialize, options): """Load cassette. Loads a previously serialized http response as a cassette :param str cassette_name: (required), name of cassette :param str serialize: (required), type of serialization i.e 'json' :options dict options: (required), options for cassette """ self.cassette_name = cassette_name self.serialize = serialize self.options.update(options.items()) placeholders = self.options.get('placeholders', {}) cassette_options = {} default_options = cassette.Cassette.default_cassette_options match_requests_on = self.options.get( 'match_requests_on', default_options['match_requests_on'] ) cassette_options['preserve_exact_body_bytes'] = self.options.get( 'preserve_exact_body_bytes', ) cassette_options['allow_playback_repeats'] = self.options.get( 'allow_playback_repeats' ) cassette_options['record_mode'] = self.options.get('record') for option, value in list(cassette_options.items()): if value is None: cassette_options.pop(option) self.cassette = cassette.Cassette( cassette_name, serialize, placeholders=placeholders, cassette_library_dir=self.options.get('cassette_library_dir'), **cassette_options ) if 'record' in self.options: self.cassette.record_mode = self.options['record'] # NOTE(sigmavirus24): Cassette.match_options is a set, might as well # use that instead of overriding it. self.cassette.match_options.update(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): """Send request. :param request request: request :returns: A Response object """ interaction = None current_cassette = self.cassette if not current_cassette: raise BetamaxError('No cassette was specified or found.') if current_cassette.interactions: interaction = current_cassette.find_match(request) if not interaction and current_cassette.is_recording(): interaction = self.send_and_record( request, stream, timeout, verify, cert, proxies ) if not interaction: raise BetamaxError(unhandled_request_message(request, current_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): """Send request and record response. The response will be serialized and saved to a cassette which can be replayed in the future. :param request request: request :param bool stream: (optional) defer download until content is accessed :param float timeout: (optional) time to wait for a response :param bool verify: (optional) verify SSL certificate :param str cert: (optional) path to SSL client :param proxies dict: (optional) mapping protocol to URL of the proxy :return: Interaction :rtype: class:`betamax.cassette.Interaction` """ adapter = self.find_adapter(request.url) response = adapter.send( request, stream=True, timeout=timeout, verify=verify, cert=cert, proxies=proxies ) return self.cassette.save_interaction(response, request) def find_adapter(self, url): """Find adapter. Searches for an existing adapter where the url and prefix match. :param url str: (required) url of the adapter :returns: betamax adapter """ 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): """Generate exception for unhandled requests.""" 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.8.1/betamax/serializers/0000775000175000017500000000000013252047671020527 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/serializers/base.py0000664000175000017500000000550113252043570022006 0ustar icordascicordasc00000000000000# -*- 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, cassette_data): # 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.8.1/betamax/serializers/json_serializer.py0000664000175000017500000000123213040145704024270 0ustar icordascicordasc00000000000000from .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.8.1/betamax/serializers/__init__.py0000664000175000017500000000053513040145704022632 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- from .base import BaseSerializer from .json_serializer import JSONSerializer from .proxy import SerializerProxy serializer_registry = {} _serializers = [JSONSerializer] serializer_registry.update(dict((s.name, s()) for s in _serializers)) del _serializers __all__ = ('BaseSerializer', 'JSONSerializer', 'SerializerProxy') betamax-0.8.1/betamax/serializers/proxy.py0000664000175000017500000000520013040145704022246 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- from .base import BaseSerializer from betamax.exceptions import MissingDirectoryError 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): directory, _ = os.path.split(self.cassette_path) if not (directory == '' or os.path.isdir(directory)): raise MissingDirectoryError( 'Configured cassette directory \'{0}\' does not exist - try ' 'creating it'.format(directory) ) 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.8.1/betamax/util.py0000664000175000017500000001304513040145704017514 0ustar icordascicordasc00000000000000from .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, reason=response.reason, 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] _SENTINEL = object() def _option_from(option, kwargs, defaults): value = kwargs.get(option, _SENTINEL) if value is _SENTINEL: value = defaults.get(option) return value betamax-0.8.1/betamax/options.py0000664000175000017500000000547013040145704020235 0ustar icordascicordasc00000000000000from .cassette import Cassette from .exceptions import InvalidOption, validation_error_map 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): """Validate placeholders is a dict-like structure""" keys = ['placeholder', 'replace'] try: return all(sorted(list(p.keys())) == keys for p in placeholders) except TypeError: return False def translate_cassette_options(): for (k, v) in Cassette.default_cassette_options.items(): yield (k, v) if k != 'record_mode' else ('record', v) def isboolean(value): return value in [True, False] 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': isboolean, 'placeholders': validate_placeholders, 'allow_playback_repeats': isboolean, } 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': [], 'allow_playback_repeats': False, } 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: raise InvalidOption('{0} is not a valid option'.format(key)) else: is_valid = Options.valid_options[key] if not is_valid(value): raise validation_error_map[key]('{0!r} is not valid' .format(value)) betamax-0.8.1/betamax/fixtures/0000775000175000017500000000000013252047671020044 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/fixtures/unittest.py0000664000175000017500000000661513040145704022274 0ustar icordascicordasc00000000000000"""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 try: import unittest2 as unittest except ImportError: 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.8.1/betamax/fixtures/__init__.py0000664000175000017500000000000013040145704022132 0ustar icordascicordasc00000000000000betamax-0.8.1/betamax/fixtures/pytest.py0000664000175000017500000001132413252043570021741 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- """A set of fixtures to integrate Betamax with py.test. .. autofunction:: betamax_session """ from __future__ import absolute_import import re import warnings import pytest import requests from .. import recorder as betamax def _sanitize(name): """ Replace certain characters (which might be problematic when contained in strings which will be used as file names) by '-'s. """ return re.sub(r'[\s/<>:\\"|?*]', '-', name) def _casette_name(request, parametrized): """Determine a cassette name from request. :param request: A request object from pytest giving us context information for the fixture. :param parametrized: Whether the name should consider parametrized tests. :returns: A cassette name. """ cassette_name = '' if request.module is not None: cassette_name += request.module.__name__ + '.' if request.cls is not None: cassette_name += request.cls.__name__ + '.' if parametrized: cassette_name += _sanitize(request.node.name) else: cassette_name += request.function.__name__ if request.node.name != request.function.__name__: warnings.warn( "betamax_recorder and betamax_session currently don't include " "parameters in the cassette name. " "Use betamax_parametrized_recorder/_session to include " "parameters. " "This behavior will be the default in betamax 1.0", FutureWarning, stacklevel=3) return cassette_name def _betamax_recorder(request, parametrized=True): cassette_name = _casette_name(request, parametrized=parametrized) session = requests.Session() recorder = betamax.Betamax(session) recorder.use_cassette(cassette_name) recorder.start() request.addfinalizer(recorder.stop) return recorder @pytest.fixture def betamax_recorder(request): """Generate a recorder with a session that has Betamax already installed. This will create a new Betamax instance 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``. If the test is parametrized, the parameters will not be included in the name. In case you need that, use betamax_parametrized_recorder instead. This will change in 1.0.0, where parameters will be included by default. :param request: A request object from pytest giving us context information for the fixture. :returns: An instantiated recorder. """ return _betamax_recorder(request, parametrized=False) @pytest.fixture def betamax_session(betamax_recorder): """Generate a session that has Betamax already installed. See `betamax_recorder` fixture. :param betamax_recorder: A recorder fixture with a configured request session. :returns: An instantiated requests Session wrapped by Betamax. """ return betamax_recorder.session @pytest.fixture def betamax_parametrized_recorder(request): """Generate a recorder with a session that has Betamax already installed. This will create a new Betamax instance 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 with parameters if parametrized. For example, if your test is in ``test_stuff.py`` and the method is ``TestStuffClass.test_stuff`` with parameter ``True`` then your cassette name will be ``test_stuff_TestStuffClass_test_stuff[True]``. :param request: A request object from pytest giving us context information for the fixture. :returns: An instantiated recorder. """ warnings.warn( "betamax_parametrized_recorder and betamax_parametrized_session " "will be removed in betamax 1.0. Their behavior will be the " "default.", DeprecationWarning) return _betamax_recorder(request, parametrized=True) @pytest.fixture def betamax_parametrized_session(betamax_parametrized_recorder): """Generate a session that has Betamax already installed. See `betamax_parametrized_recorder` fixture. :param betamax_parametrized_recorder: A recorder fixture with a configured request session. :returns: An instantiated requests Session wrapped by Betamax. """ return betamax_parametrized_recorder.session betamax-0.8.1/betamax/headers.py0000664000175000017500000001757713040145704020170 0ustar icordascicordasc00000000000000"""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.8.1/betamax/mock_response.py0000664000175000017500000000151313040145704021403 0ustar icordascicordasc00000000000000from email import parser, message import sys class MockHTTPResponse(object): def __init__(self, headers): from betamax.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.8.1/betamax/cassette/0000775000175000017500000000000013252047671020006 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/cassette/interaction.py0000664000175000017500000000720413252043570022674 0ustar icordascicordasc00000000000000from requests.cookies import extract_cookies_to_jar from datetime import datetime from betamax import util 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.data = interaction self.orig_response = response self.recorded_response = self.deserialize() self.used = False self.ignored = False def ignore(self): """Ignore this interaction. This is only to be used from a before_record or a before_playback callback. """ self.ignored = True def as_response(self): """Return the Interaction as a Response object.""" self.recorded_response = self.deserialize() return self.recorded_response @property def recorded_at(self): return datetime.strptime(self.data['recorded_at'], '%Y-%m-%dT%H:%M:%S') def deserialize(self): """Turn a serialized interaction into a Response.""" r = util.deserialize_response(self.data['response']) r.request = util.deserialize_prepared_request(self.data['request']) extract_cookies_to_jar(r.cookies, r.request, r.raw) return r def match(self, matchers): """Return whether this interaction is a match.""" request = self.data['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, serializing): """Easy way to accept all placeholders registered.""" for placeholder in replacements: self.replace(*placeholder.unpack(serializing)) def replace_in_headers(self, text_to_replace, placeholder): for obj in ('request', 'response'): headers = self.data[obj]['headers'] for k, v in list(headers.items()): if isinstance(v, list): headers[k] = [hv.replace(text_to_replace, placeholder) for hv in v] else: headers[k] = v.replace(text_to_replace, placeholder) def replace_in_body(self, text_to_replace, placeholder): for obj in ('request', 'response'): body = self.data[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.data[obj]['body'] = body else: self.data[obj]['body']['string'] = body def replace_in_uri(self, text_to_replace, placeholder): for (obj, key) in (('request', 'uri'), ('response', 'url')): uri = self.data[obj][key] if text_to_replace in uri: self.data[obj][key] = uri.replace( text_to_replace, placeholder ) betamax-0.8.1/betamax/cassette/cassette.py0000664000175000017500000002022213040145704022160 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- import collections from datetime import datetime from functools import partial import os.path from .interaction import Interaction from .. import matchers from .. import serializers from betamax.util import (_option_from, serialize_prepared_request, serialize_response, timestamp) class Cassette(object): default_cassette_options = { 'record_mode': 'once', 'match_requests_on': ['method', 'uri'], 're_record_interval': None, 'placeholders': [], 'preserve_exact_body_bytes': False, 'allow_playback_repeats': False, } hooks = collections.defaultdict(list) 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 = serializers.SerializerProxy.find( serialization_format, kwargs.get('cassette_library_dir'), cassette_name ) self.cassette_path = self.serializer.cassette_path # Determine which placeholders to use default_placeholders = defaults['placeholders'][:] cassette_placeholders = kwargs.get('placeholders', []) self.placeholders = merge_placeholder_lists(default_placeholders, cassette_placeholders) # Determine whether to preserve exact body bytes self.preserve_exact_body_bytes = _option_from( 'preserve_exact_body_bytes', kwargs, defaults ) self.allow_playback_repeats = _option_from( 'allow_playback_repeats', 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 = serializers.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:`~betamax.cassette.Interaction` """ # if we are recording, do not filter by match if self.is_recording() and self.record_mode != 'all': return None opts = self.match_options # Curry those matchers curried_matchers = [ partial(matchers.matcher_registry[o].match, request) for o in opts ] for interaction in self.interactions: if not interaction.match(curried_matchers): continue if interaction.used or interaction.ignored: continue # 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(interaction) break # set interaction as used before returning if not self.allow_playback_repeats: interaction.used = True return interaction # 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: dispatch_hooks('before_playback', i, self) i.replace_all(self.placeholders, False) def sanitize_interactions(self): for i in self.interactions: i.replace_all(self.placeholders, True) def save_interaction(self, response, request): serialized_data = self.serialize_interaction(response, request) interaction = Interaction(serialized_data, response) dispatch_hooks('before_record', interaction, self) if not interaction.ignored: # If a hook caused this to be ignored self.interactions.append(interaction) return interaction 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.data for i in self.interactions], 'recorded_with': 'betamax/{0}'.format(__version__) } self.serializer.serialize(cassette_data) class Placeholder(collections.namedtuple('Placeholder', 'placeholder replace')): """Encapsulate some logic about Placeholders.""" @classmethod def from_dict(cls, dictionary): return cls(**dictionary) def unpack(self, serializing): if serializing: return self.replace, self.placeholder else: return self.placeholder, self.replace def merge_placeholder_lists(defaults, overrides): overrides = [Placeholder.from_dict(override) for override in overrides] overrides_dict = dict((p.placeholder, p) for p in overrides) placeholders = [overrides_dict.pop(p.placeholder, p) for p in map(Placeholder.from_dict, defaults)] return placeholders + [p for p in overrides if p.placeholder in overrides_dict] def dispatch_hooks(hook_name, *args): """Dispatch registered hooks.""" # Cassette.hooks is a dictionary that defaults to an empty list, # we neither need to check for the presence of hook_name in it, nor # need to worry about whether the return value will be iterable hooks = Cassette.hooks[hook_name] for hook in hooks: hook(*args) betamax-0.8.1/betamax/cassette/__init__.py0000664000175000017500000000021513040145704022104 0ustar icordascicordasc00000000000000from .cassette import Cassette, dispatch_hooks from .interaction import Interaction __all__ = ('Cassette', 'Interaction', 'dispatch_hooks') betamax-0.8.1/betamax/exceptions.py0000664000175000017500000000220213252043666020722 0ustar icordascicordasc00000000000000class BetamaxError(Exception): def __init__(self, message): super(BetamaxError, self).__init__(message) class MissingDirectoryError(BetamaxError): pass class ValidationError(BetamaxError): pass class InvalidOption(ValidationError): pass class BodyBytesValidationError(ValidationError): pass class MatchersValidationError(ValidationError): pass class RecordValidationError(ValidationError): pass class RecordIntervalValidationError(ValidationError): pass class PlaceholdersValidationError(ValidationError): pass class PlaybackRepeatsValidationError(ValidationError): pass class SerializerValidationError(ValidationError): pass validation_error_map = { 'allow_playback_repeats': PlaybackRepeatsValidationError, 'match_requests_on': MatchersValidationError, 'record': RecordValidationError, 'placeholders': PlaceholdersValidationError, 'preserve_exact_body_bytes': BodyBytesValidationError, 're_record_interval': RecordIntervalValidationError, 'serialize': SerializerValidationError, # TODO: Remove this 'serialize_with': SerializerValidationError } betamax-0.8.1/betamax/__init__.py0000664000175000017500000000127313252047546020310 0ustar icordascicordasc00000000000000""" betamax. ======= See https://betamax.readthedocs.io/ for documentation. :copyright: (c) 2013-2018 by Ian Stapleton 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 Stapleton Cordasco' __copyright__ = 'Copyright 2013-2018 Ian Stapleton Cordasco' __license__ = 'Apache 2.0' __title__ = 'betamax' __version__ = '0.8.1' __version_info__ = tuple(int(i) for i in __version__.split('.')) betamax-0.8.1/betamax/decorator.py0000664000175000017500000000357213040145704020525 0ustar icordascicordasc00000000000000import 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.8.1/betamax/configure.py0000664000175000017500000000770513040145704020526 0ustar icordascicordasc00000000000000from .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) def before_playback(self, tag=None, callback=None): """Register a function to call before playing back an interaction. Example usage: .. code-block:: python def before_playback(interaction, cassette): pass with Betamax.configure() as config: config.before_playback(callback=before_playback) :param str tag: Limits the interactions passed to the function based on the interaction's tag (currently unsupported). :param callable callback: The function which either accepts just an interaction or an interaction and a cassette and mutates the interaction before returning. """ Cassette.hooks['before_playback'].append(callback) def before_record(self, tag=None, callback=None): """Register a function to call before recording an interaction. Example usage: .. code-block:: python def before_record(interaction, cassette): pass with Betamax.configure() as config: config.before_record(callback=before_record) :param str tag: Limits the interactions passed to the function based on the interaction's tag (currently unsupported). :param callable callback: The function which either accepts just an interaction or an interaction and a cassette and mutates the interaction before returning. """ Cassette.hooks['before_record'].append(callback) @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.8.1/betamax/recorder.py0000664000175000017500000001335213040145704020345 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/0000775000175000017500000000000013252047671020001 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax/matchers/base.py0000664000175000017500000000344013040145704021255 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/body.py0000664000175000017500000000113113040145704021273 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from betamax import util class BodyMatcher(BaseMatcher): # Matches based on the body of the request name = 'body' def match(self, request, recorded_request): recorded_request = util.deserialize_prepared_request(recorded_request) request_body = b'' if request.body: request_body = util.coerce_content(request.body) recorded_body = b'' if recorded_request.body: recorded_body = util.coerce_content(recorded_request.body) return recorded_body == request_body betamax-0.8.1/betamax/matchers/uri.py0000664000175000017500000000202713040145704021142 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/headers.py0000664000175000017500000000074013040145704021756 0ustar icordascicordasc00000000000000# -*- 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.util import from_list headers = request['headers'].items() return dict((k, from_list(v)) for (k, v) in headers) betamax-0.8.1/betamax/matchers/query.py0000664000175000017500000000237313040145704021514 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- import sys from .base import BaseMatcher try: from urlparse import parse_qs, urlparse except ImportError: from urllib.parse import parse_qs, urlparse isPY2 = (2, 6) <= sys.version_info < (3, 0) 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 keep_blank_values=True, ) def match(self, request, recorded_request): request_query_dict = self.to_dict(urlparse(request.url).query) recorded_query = urlparse(recorded_request['uri']).query if recorded_query and isPY2: # NOTE(sigmavirus24): If we're on Python 2, the request.url will # be str/bytes and the recorded_request['uri'] will be unicode. # For the comparison to work for high unicode characters, we need # to encode the recorded query string before parsing it. See also # GitHub bug #43. recorded_query = recorded_query.encode('utf-8') recorded_query_dict = self.to_dict(recorded_query) return request_query_dict == recorded_query_dict betamax-0.8.1/betamax/matchers/host.py0000664000175000017500000000062113040145704021316 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/path.py0000664000175000017500000000061513040145704021300 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/method.py0000664000175000017500000000041413040145704021621 0ustar icordascicordasc00000000000000# -*- 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.8.1/betamax/matchers/digest_auth.py0000664000175000017500000000310413040145704022640 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- from .base import BaseMatcher from betamax.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.8.1/betamax/matchers/__init__.py0000664000175000017500000000134113040145704022100 0ustar icordascicordasc00000000000000from .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 matcher_registry = {} __all__ = ('BaseMatcher', 'BodyMatcher', 'DigestAuthMatcher', 'HeadersMatcher', 'HostMatcher', 'MethodMatcher', 'PathMatcher', 'QueryMatcher', 'URIMatcher', 'matcher_registry') _matchers = [BodyMatcher, DigestAuthMatcher, HeadersMatcher, HostMatcher, MethodMatcher, PathMatcher, QueryMatcher, URIMatcher] matcher_registry.update(dict((m.name, m()) for m in _matchers)) del _matchers betamax-0.8.1/HISTORY.rst0000664000175000017500000001423313252047531016443 0ustar icordascicordasc00000000000000History ======= 0.8.1 - 2018-03-13 ------------------ - Previous attempts to sanitize cassette names were incomplete. Sanitization has become more thorough which could have some affects on existing cassette files. **This may cause new cassettes to be generated.** - Fix bug where there may be an exception raised in a ``betamax.exceptions.BetamaxError`` repr. 0.8.0 - 2016-08-16 ------------------ - Add ``betamax_parametrized_recorder`` and ``betamax_parametrized_session`` to our list of pytest fixtures so that users will have parametrized cassette names when writing parametrized tests with our fixtures. (I wonder if I can mention parametrization a bunch more times so I can say parametrize a lot in this bullet note.) - Add ``ValidationError`` and a set of subclasses for each possible validation error. - Raise ``InvalidOption`` on unknown cassette options rather than silently ignoring extra options. - Raise a subclass of ``ValidationError`` when a particular cassette option is invalid, rather than silently ignoring the validation failure. 0.7.2 - 2016-08-04 ------------------ - Fix bug with query string matcher where query-strings without values (e.g., ``?foo&bar`` as opposed to ``?foo=1&bar=2``) were treated as if there were no query string. 0.7.1 - 2016-06-14 ------------------ - Fix issue #108 by effectively copying the items in the match_requests_on list into the match_options set on a Cassette instance 0.7.0 - 2016-04-29 ------------------ - Add ``before_record`` and ``before_playback`` hooks - Allow per-cassette placeholders to be merged and override global placeholders - Fix bug where the ``QueryMatcher`` failed matching on high Unicode points 0.6.0 - 2016-04-12 ------------------ - Add ``betamax_recorder`` pytest fixture - Change default behaviour to allow duplicate interactions to be recorded in single cassette - Add ``allow_playback_repeats`` to allow an interaction to be used more than once from a single cassette - Always return a new ``Response`` object from an Interaction to allow for a streaming response to be usable multiple times - Remove CI support for Pythons 2.6 and 3.2 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.8.1/LICENSE0000664000175000017500000000110413040145704015542 0ustar icordascicordasc00000000000000Copyright 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.8.1/README.rst0000664000175000017500000000603213040145704016231 0ustar icordascicordasc00000000000000betamax ======= 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.8.1/tests/0000775000175000017500000000000013252047671015714 5ustar icordascicordasc00000000000000betamax-0.8.1/tests/cassettes/0000775000175000017500000000000013252047671017712 5ustar icordascicordasc00000000000000././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000betamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[eee-fff].jsonbetamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesyste0000664000175000017500000000007313252043570036335 0ustar icordascicordasc00000000000000{"http_interactions": [], "recorded_with": "betamax/0.8.0"}betamax-0.8.1/tests/cassettes/handles_digest_auth.json0000664000175000017500000000400113040145704024565 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.1.0"]}, "method": "GET", "uri": "https://httpbin.org/digest-auth/auth/user/passwd"}, "response": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"content-length": ["0"], "set-cookie": ["fake=fake_value"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Fri, 02 May 2014 13:13:26 GMT"], "access-control-allow-origin": ["*"], "content-type": ["text/html; charset=utf-8"], "www-authenticate": ["Digest opaque=\"8202f396ae9e04ba77f99a024a8cf5eb\", qop=auth, nonce=\"ad49767e1b450af7af35d21da282cbee\", realm=\"me@kennethreitz.com\""]}, "status": {"message": "UNAUTHORIZED", "code": 401}, "url": "https://httpbin.org/digest-auth/auth/user/passwd"}, "recorded_at": "2014-05-02T13:13:26"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Cookie": ["fake=fake_value"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["Digest username=\"user\", realm=\"me@kennethreitz.com\", nonce=\"ad49767e1b450af7af35d21da282cbee\", uri=\"/digest-auth/auth/user/passwd\", response=\"af73a28fe4b2ee57c87b73f4c523e0d4\", opaque=\"8202f396ae9e04ba77f99a024a8cf5eb\", qop=\"auth\", nc=00000001, cnonce=\"75bdd2263bb91c0a\""], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.1.0"]}, "method": "GET", "uri": "https://httpbin.org/digest-auth/auth/user/passwd"}, "response": {"body": {"string": "{\n \"user\": \"user\",\n \"authenticated\": true\n}", "encoding": null}, "headers": {"content-length": ["45"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Fri, 02 May 2014 13:13:26 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/digest-auth/auth/user/passwd"}, "recorded_at": "2014-05-02T13:13:26"}], "recorded_with": "betamax/{version}"}././@LongLink0000000000000000000000000000015200000000000011213 Lustar 00000000000000betamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.TestPyTestFixtures.test_pytest_fixture.jsonbetamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.TestPyTestFixtures.test_pytest_fixture0000664000175000017500000000205413040145704036175 0ustar icordascicordasc00000000000000{"recorded_with": "betamax/0.4.2", "http_interactions": [{"recorded_at": "2015-05-25T00:46:42", "response": {"body": {"encoding": null, "string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"python-requests/2.6.0 CPython/3.4.2 Darwin/14.1.0\"\n }, \n \"origin\": \"72.160.201.47\", \n \"url\": \"https://httpbin.org/get\"\n}\n"}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get", "headers": {"connection": ["keep-alive"], "content-type": ["application/json"], "content-length": ["266"], "date": ["Mon, 25 May 2015 00:46:42 GMT"], "access-control-allow-origin": ["*"], "access-control-allow-credentials": ["true"], "server": ["nginx"]}}, "request": {"method": "GET", "body": {"encoding": "utf-8", "string": ""}, "uri": "https://httpbin.org/get", "headers": {"Connection": ["keep-alive"], "User-Agent": ["python-requests/2.6.0 CPython/3.4.2 Darwin/14.1.0"], "Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"]}}}]}././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000betamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[aaa-bbb].jsonbetamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesyste0000664000175000017500000000007313252043570036335 0ustar icordascicordasc00000000000000{"http_interactions": [], "recorded_with": "betamax/0.8.0"}betamax-0.8.1/tests/cassettes/preserve_exact_bytes.json0000664000175000017500000000264013040145704025023 0ustar icordascicordasc00000000000000{"recorded_with": "betamax/0.4.2", "http_interactions": [{"recorded_at": "2015-06-27T15:39:25", "request": {"method": "POST", "uri": "https://httpbin.org/post", "headers": {"Content-Length": ["3"], "User-Agent": ["python-requests/2.7.0 CPython/3.4.3 Darwin/14.3.0"], "Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Content-Type": ["application/x-www-form-urlencoded"], "Accept": ["*/*"]}, "body": {"base64_string": "YT0x", "encoding": "utf-8"}}, "response": {"url": "https://httpbin.org/post", "headers": {"access-control-allow-origin": ["*"], "content-type": ["application/json"], "server": ["nginx"], "date": ["Sat, 27 Jun 2015 15:39:25 GMT"], "content-length": ["430"], "access-control-allow-credentials": ["true"], "connection": ["keep-alive"]}, "status": {"code": 200, "message": "OK"}, "body": {"base64_string": "ewogICJhcmdzIjoge30sIAogICJkYXRhIjogIiIsIAogICJmaWxlcyI6IHt9LCAKICAiZm9ybSI6IHsKICAgICJhIjogIjEiCiAgfSwgCiAgImhlYWRlcnMiOiB7CiAgICAiQWNjZXB0IjogIiovKiIsIAogICAgIkFjY2VwdC1FbmNvZGluZyI6ICJnemlwLCBkZWZsYXRlIiwgCiAgICAiQ29udGVudC1MZW5ndGgiOiAiMyIsIAogICAgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQiLCAKICAgICJIb3N0IjogImh0dHBiaW4ub3JnIiwgCiAgICAiVXNlci1BZ2VudCI6ICJweXRob24tcmVxdWVzdHMvMi43LjAgQ1B5dGhvbi8zLjQuMyBEYXJ3aW4vMTQuMy4wIgogIH0sIAogICJqc29uIjogbnVsbCwgCiAgIm9yaWdpbiI6ICI2OC42LjkxLjIzOSIsIAogICJ1cmwiOiAiaHR0cHM6Ly9odHRwYmluLm9yZy9wb3N0Igp9Cg==", "encoding": null}}}]}betamax-0.8.1/tests/cassettes/global_preserve_exact_body_bytes.json0000664000175000017500000000226113040145704027357 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": {"base64_string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate, compress"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.0.0 CPython/2.7.5 Darwin/13.1.0"]}, "method": "GET", "uri": "https://httpbin.org/get"}, "response": {"body": {"base64_string": "ewogICJhcmdzIjoge30sCiAgInVybCI6ICJodHRwOi8vaHR0cGJpbi5vcmcvZ2V0IiwKICAiaGVhZGVycyI6IHsKICAgICJYLVJlcXVlc3QtSWQiOiAiZTJlNDAwYjgtNTVjNC00NTVkLWIwMmYtYjcwZTYzYmI4ZGYyIiwKICAgICJDb25uZWN0aW9uIjogImNsb3NlIiwKICAgICJBY2NlcHQtRW5jb2RpbmciOiAiZ3ppcCwgZGVmbGF0ZSwgY29tcHJlc3MiLAogICAgIkFjY2VwdCI6ICIqLyoiLAogICAgIlVzZXItQWdlbnQiOiAicHl0aG9uLXJlcXVlc3RzLzIuMC4wIENQeXRob24vMi43LjUgRGFyd2luLzEzLjEuMCIsCiAgICAiSG9zdCI6ICJodHRwYmluLm9yZyIKICB9LAogICJvcmlnaW4iOiAiNjYuMTcxLjE3My4yNTAiCn0=", "encoding": null}, "headers": {"content-length": ["356"], "server": ["gunicorn/0.17.4"], "connection": ["keep-alive"], "date": ["Fri, 11 Apr 2014 20:30:30 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get"}, "recorded_at": "2014-04-11T20:30:30"}], "recorded_with": "betamax/{version}"}betamax-0.8.1/tests/cassettes/replay_multiple_times.json0000664000175000017500000001037613040145704025213 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"]}, "method": "GET", "uri": "http://httpbin.org/stream/5"}, "response": {"body": {"string": "{\"url\": \"http://httpbin.org/stream/5\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 0, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/5\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 1, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/5\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 2, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/5\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 3, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/5\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 4, \"origin\": \"188.39.80.202\"}\n", "encoding": null}, "headers": {"Transfer-Encoding": ["chunked"], "Server": ["nginx"], "Connection": ["keep-alive"], "Access-Control-Allow-Credentials": ["true"], "Date": ["Fri, 25 Mar 2016 17:06:34 GMT"], "Access-Control-Allow-Origin": ["*"], "Content-Type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/stream/5"}, "recorded_at": "2016-03-25T17:06:34"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"]}, "method": "GET", "uri": "http://httpbin.org/stream/1"}, "response": {"body": {"string": "{\"url\": \"http://httpbin.org/stream/1\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 0, \"origin\": \"188.39.80.202\"}\n", "encoding": null}, "headers": {"Transfer-Encoding": ["chunked"], "Server": ["nginx"], "Connection": ["keep-alive"], "Access-Control-Allow-Credentials": ["true"], "Date": ["Fri, 25 Mar 2016 17:20:38 GMT"], "Access-Control-Allow-Origin": ["*"], "Content-Type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/stream/1"}, "recorded_at": "2016-03-25T17:20:38"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"]}, "method": "GET", "uri": "http://httpbin.org/stream/3"}, "response": {"body": {"string": "{\"url\": \"http://httpbin.org/stream/3\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 0, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/3\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 1, \"origin\": \"188.39.80.202\"}\n{\"url\": \"http://httpbin.org/stream/3\", \"headers\": {\"Host\": \"httpbin.org\", \"Accept-Encoding\": \"gzip, deflate\", \"Accept\": \"*/*\", \"User-Agent\": \"python-requests/2.9.1\"}, \"args\": {}, \"id\": 2, \"origin\": \"188.39.80.202\"}\n", "encoding": null}, "headers": {"Transfer-Encoding": ["chunked"], "Server": ["nginx"], "Connection": ["keep-alive"], "Access-Control-Allow-Credentials": ["true"], "Date": ["Fri, 25 Mar 2016 17:37:06 GMT"], "Access-Control-Allow-Origin": ["*"], "Content-Type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "http://httpbin.org/stream/3"}, "recorded_at": "2016-03-25T17:37:06"}], "recorded_with": "betamax/0.5.1"}betamax-0.8.1/tests/cassettes/test.json0000664000175000017500000000006513040145704021554 0ustar icordascicordasc00000000000000{"recorded_with": "betamax", "http_interactions": []}betamax-0.8.1/tests/cassettes/GitHub_create_issue.json0000664000175000017500000002571613040145704024524 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": "", "headers": {"Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "github3.py/0.7.0", "Accept-Charset": "utf-8", "Content-Type": "application/json", "Authorization": "token "}, "method": "GET", "uri": "https://api.github.com/repos/github3py/fork_this"}, "response": {"body": {"string": "{\"id\":5816984,\"name\":\"fork_this\",\"full_name\":\"github3py/fork_this\",\"owner\":{\"login\":\"github3py\",\"id\":1782156,\"avatar_url\":\"https://0.gravatar.com/avatar/396e3de53320abf9855d912cd3d9431f?d=https%3A%2F%2Fidenticons.github.com%2Ff5a7e12a02816f3c90b0da74492d7b73.png\",\"gravatar_id\":\"396e3de53320abf9855d912cd3d9431f\",\"url\":\"https://api.github.com/users/github3py\",\"html_url\":\"https://github.com/github3py\",\"followers_url\":\"https://api.github.com/users/github3py/followers\",\"following_url\":\"https://api.github.com/users/github3py/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/github3py/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/github3py/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/github3py/subscriptions\",\"organizations_url\":\"https://api.github.com/users/github3py/orgs\",\"repos_url\":\"https://api.github.com/users/github3py/repos\",\"events_url\":\"https://api.github.com/users/github3py/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/github3py/received_events\",\"type\":\"Organization\"},\"private\":false,\"html_url\":\"https://github.com/github3py/fork_this\",\"description\":\"A repository to test forking of\",\"fork\":false,\"url\":\"https://api.github.com/repos/github3py/fork_this\",\"forks_url\":\"https://api.github.com/repos/github3py/fork_this/forks\",\"keys_url\":\"https://api.github.com/repos/github3py/fork_this/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/github3py/fork_this/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/github3py/fork_this/teams\",\"hooks_url\":\"https://api.github.com/repos/github3py/fork_this/hooks\",\"issue_events_url\":\"https://api.github.com/repos/github3py/fork_this/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/github3py/fork_this/events\",\"assignees_url\":\"https://api.github.com/repos/github3py/fork_this/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/github3py/fork_this/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/github3py/fork_this/tags\",\"blobs_url\":\"https://api.github.com/repos/github3py/fork_this/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/github3py/fork_this/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/github3py/fork_this/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/github3py/fork_this/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/github3py/fork_this/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/github3py/fork_this/languages\",\"stargazers_url\":\"https://api.github.com/repos/github3py/fork_this/stargazers\",\"contributors_url\":\"https://api.github.com/repos/github3py/fork_this/contributors\",\"subscribers_url\":\"https://api.github.com/repos/github3py/fork_this/subscribers\",\"subscription_url\":\"https://api.github.com/repos/github3py/fork_this/subscription\",\"commits_url\":\"https://api.github.com/repos/github3py/fork_this/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/github3py/fork_this/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/github3py/fork_this/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/github3py/fork_this/issues/comments/{number}\",\"contents_url\":\"https://api.github.com/repos/github3py/fork_this/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/github3py/fork_this/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/github3py/fork_this/merges\",\"archive_url\":\"https://api.github.com/repos/github3py/fork_this/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/github3py/fork_this/downloads\",\"issues_url\":\"https://api.github.com/repos/github3py/fork_this/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/github3py/fork_this/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/github3py/fork_this/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/github3py/fork_this/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/github3py/fork_this/labels{/name}\",\"created_at\":\"2012-09-15T02:40:33Z\",\"updated_at\":\"2013-01-12T06:17:23Z\",\"pushed_at\":\"2012-09-15T02:40:33Z\",\"git_url\":\"git://github.com/github3py/fork_this.git\",\"ssh_url\":\"git@github.com:github3py/fork_this.git\",\"clone_url\":\"https://github.com/github3py/fork_this.git\",\"svn_url\":\"https://github.com/github3py/fork_this\",\"homepage\":null,\"size\":124,\"watchers_count\":0,\"language\":null,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"forks_count\":0,\"mirror_url\":null,\"open_issues_count\":0,\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"master\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true},\"network_count\":0,\"organization\":{\"login\":\"github3py\",\"id\":1782156,\"avatar_url\":\"https://0.gravatar.com/avatar/396e3de53320abf9855d912cd3d9431f?d=https%3A%2F%2Fidenticons.github.com%2Ff5a7e12a02816f3c90b0da74492d7b73.png\",\"gravatar_id\":\"396e3de53320abf9855d912cd3d9431f\",\"url\":\"https://api.github.com/users/github3py\",\"html_url\":\"https://github.com/github3py\",\"followers_url\":\"https://api.github.com/users/github3py/followers\",\"following_url\":\"https://api.github.com/users/github3py/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/github3py/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/github3py/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/github3py/subscriptions\",\"organizations_url\":\"https://api.github.com/users/github3py/orgs\",\"repos_url\":\"https://api.github.com/users/github3py/repos\",\"events_url\":\"https://api.github.com/users/github3py/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/github3py/received_events\",\"type\":\"Organization\"}}", "encoding": "utf-8"}, "headers": {"status": "200 OK", "x-ratelimit-remaining": "4998", "x-github-media-type": "github.v3; param=full; format=json", "x-content-type-options": "nosniff", "access-control-expose-headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes", "transfer-encoding": "chunked", "x-github-request-id": "4B79B77B:4BAE:94DA28:523E624C", "content-encoding": "gzip", "vary": "Accept, Authorization, Cookie, Accept-Encoding", "x-accepted-oauth-scopes": "repo, public_repo, repo:status, repo:deployment, delete_repo, site_admin", "server": "GitHub.com", "cache-control": "private, max-age=60, s-maxage=60", "last-modified": "Sat, 12 Jan 2013 06:17:23 GMT", "x-ratelimit-limit": "5000", "etag": "\"987cea7f315ccf1f18a912a19ac5a8eb\"", "access-control-allow-credentials": "true", "date": "Sun, 22 Sep 2013 03:21:48 GMT", "x-oauth-scopes": "user, repo, gist", "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", "x-ratelimit-reset": "1379822638"}, "url": "https://api.github.com/repos/github3py/fork_this", "status_code": 200}, "recorded_at": "2013-09-22T03:21:04"}, {"request": {"body": "{\"body\": \"Let's see how well this works with Betamax\", \"labels\": [], \"title\": \"Test issue creation\"}", "headers": {"Content-Length": "100", "Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "github3.py/0.7.0", "Accept-Charset": "utf-8", "Content-Type": "application/json", "Authorization": "token "}, "method": "POST", "uri": "https://api.github.com/repos/github3py/fork_this/issues"}, "response": {"body": {"string": "{\"url\":\"https://api.github.com/repos/github3py/fork_this/issues/1\",\"labels_url\":\"https://api.github.com/repos/github3py/fork_this/issues/1/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/github3py/fork_this/issues/1/comments\",\"events_url\":\"https://api.github.com/repos/github3py/fork_this/issues/1/events\",\"html_url\":\"https://github.com/github3py/fork_this/issues/1\",\"id\":19867277,\"number\":1,\"title\":\"Test issue creation\",\"user\":{\"login\":\"sigmavirus24\",\"id\":240830,\"avatar_url\":\"https://2.gravatar.com/avatar/c148356d89f925e692178bee1d93acf7?d=https%3A%2F%2Fidenticons.github.com%2F4a71764034cdae877484be72718ba526.png\",\"gravatar_id\":\"c148356d89f925e692178bee1d93acf7\",\"url\":\"https://api.github.com/users/sigmavirus24\",\"html_url\":\"https://github.com/sigmavirus24\",\"followers_url\":\"https://api.github.com/users/sigmavirus24/followers\",\"following_url\":\"https://api.github.com/users/sigmavirus24/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/sigmavirus24/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/sigmavirus24/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/sigmavirus24/subscriptions\",\"organizations_url\":\"https://api.github.com/users/sigmavirus24/orgs\",\"repos_url\":\"https://api.github.com/users/sigmavirus24/repos\",\"events_url\":\"https://api.github.com/users/sigmavirus24/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/sigmavirus24/received_events\",\"type\":\"User\"},\"labels\":[],\"state\":\"open\",\"assignee\":null,\"milestone\":null,\"comments\":0,\"created_at\":\"2013-09-22T03:21:48Z\",\"updated_at\":\"2013-09-22T03:21:48Z\",\"closed_at\":null,\"body_html\":\"

Let's see how well this works with Betamax

\",\"body_text\":\"Let's see how well this works with Betamax\",\"body\":\"Let's see how well this works with Betamax\",\"closed_by\":null}", "encoding": "utf-8"}, "headers": {"status": "201 Created", "x-accepted-oauth-scopes": "repo, public_repo", "x-ratelimit-remaining": "4997", "x-github-media-type": "github.v3; param=full; format=json", "x-content-type-options": "nosniff", "access-control-expose-headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes", "x-github-request-id": "4B79B77B:4BAE:94DA3B:523E624C", "cache-control": "private, max-age=60, s-maxage=60", "vary": "Accept, Authorization, Cookie", "content-length": "1814", "server": "GitHub.com", "x-ratelimit-limit": "5000", "location": "https://api.github.com/repos/github3py/fork_this/issues/1", "access-control-allow-credentials": "true", "date": "Sun, 22 Sep 2013 03:21:48 GMT", "x-oauth-scopes": "user, repo, gist", "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", "etag": "\"a98254bd24fe3e6cd3acd2e5e115a023\"", "x-ratelimit-reset": "1379822638"}, "url": "https://api.github.com/repos/github3py/fork_this/issues", "status_code": 201}, "recorded_at": "2013-09-22T03:21:05"}], "recorded_with": "betamax"}betamax-0.8.1/tests/cassettes/test_replays_response_on_right_order.json0000664000175000017500000000451613040145704030322 0ustar icordascicordasc00000000000000{ "http_interactions": [ { "recorded_at": "2013-12-22T16:30:30", "response": { "headers": { "server": "gunicorn/0.17.4", "access-control-allow-origin": "*", "date": "Sun, 22 Dec 2013 16:31:13 GMT", "connection": "keep-alive", "content-type": "application/json", "content-length": "295" }, "status_code": 200, "body": { "string": "{\n \"headers\": {\n \"Accept-Encoding\": \"gzip, deflate, compress\",\n \"Accept\": \"*/*\",\n \"Host\": \"httpbin.org\",\n \"User-Agent\": \"python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29\",\n \"Connection\": \"close\"\n },\n \"origin\": \"72.160.214.132\",\n \"args\": {},\n \"url\": \"http://httpbin.org/get\"\n}", "encoding": null }, "url": "http://httpbin.org/get" }, "request": { "method": "GET", "headers": { "Accept-Encoding": "gzip, deflate, compress", "Accept": "*/*", "User-Agent": "python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29" }, "uri": "http://httpbin.org/get", "body": "" } }, { "recorded_at": "2013-12-22T16:30:30", "response": { "headers": { "server": "gunicorn/0.17.4", "access-control-allow-origin": "*", "date": "Sun, 22 Dec 2013 16:31:13 GMT", "connection": "keep-alive", "content-type": "application/json", "content-length": "295" }, "status_code": 200, "body": { "string": "{\n \"headers\": {\n \"Accept-Encoding\": \"gzip, deflate, compress\",\n \"Accept\": \"*/*\",\n \"Host\": \"httpbin.org\",\n \"User-Agent\": \"python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29\",\n \"Connection\": \"close\"\n },\n \"origin\": \"72.160.214.133\",\n \"args\": {},\n \"url\": \"http://httpbin.org/get\"\n}", "encoding": null }, "url": "http://httpbin.org/get" }, "request": { "method": "GET", "headers": { "Accept-Encoding": "gzip, deflate, compress", "Accept": "*/*", "User-Agent": "python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29" }, "uri": "http://httpbin.org/get", "body": "" } } ], "recorded_with": "betamax" } betamax-0.8.1/tests/cassettes/once_record_mode.json0000664000175000017500000000170313040145704024063 0ustar icordascicordasc00000000000000{"http_interactions": [{"recorded_at": "2013-12-22T16:30:30", "response": {"headers": {"server": "gunicorn/0.17.4", "access-control-allow-origin": "*", "date": "Sun, 22 Dec 2013 16:31:13 GMT", "connection": "keep-alive", "content-type": "application/json", "content-length": "295"}, "status_code": 200, "body": {"string": "{\n \"headers\": {\n \"Accept-Encoding\": \"gzip, deflate, compress\",\n \"Accept\": \"*/*\",\n \"Host\": \"httpbin.org\",\n \"User-Agent\": \"python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29\",\n \"Connection\": \"close\"\n },\n \"origin\": \"72.160.214.132\",\n \"args\": {},\n \"url\": \"http://httpbin.org/get\"\n}", "encoding": null}, "url": "http://httpbin.org/get"}, "request": {"method": "GET", "headers": {"Accept-Encoding": "gzip, deflate, compress", "Accept": "*/*", "User-Agent": "python-requests/2.0.0 CPython/3.3.2 Linux/3.2.29"}, "uri": "http://httpbin.org/get", "body": ""}}], "recorded_with": "betamax"}betamax-0.8.1/tests/cassettes/replay_interactions.json0000664000175000017500000000175713040145704024664 0ustar icordascicordasc00000000000000{"http_interactions": [{"response": {"url": "http://httpbin.org/get", "status": {"message": "OK", "code": 200}, "body": {"string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"python-requests/2.9.1\"\n }, \n \"origin\": \"96.37.91.79\", \n \"url\": \"http://httpbin.org/get\"\n}\n", "encoding": null}, "headers": {"Access-Control-Allow-Origin": ["*"], "Access-Control-Allow-Credentials": ["true"], "Connection": ["keep-alive"], "Content-Type": ["application/json"], "Date": ["Thu, 07 Apr 2016 13:22:08 GMT"], "Server": ["nginx"], "Content-Length": ["235"]}}, "recorded_at": "2016-04-07T13:22:13", "request": {"uri": "http://httpbin.org/get", "method": "GET", "body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Connection": ["keep-alive"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"]}}}], "recorded_with": "betamax/0.5.1"}betamax-0.8.1/tests/cassettes/test-multiple-cookies-regression.json0000664000175000017500000000427013040145704027217 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "method": "GET", "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.6.0 CPython/3.4.2 Darwin/14.1.0"], "Accept": ["*/*"]}, "uri": "https://httpbin.org/cookies/set?cookie2=value2&cookie1=value1&cookie3=value3&cookie0=value0"}, "recorded_at": "2015-04-18T16:05:26", "response": {"body": {"encoding": "utf-8", "string": "\nRedirecting...\n

Redirecting...

\n

You should be redirected automatically to target URL: /cookies. If not click the link."}, "url": "https://httpbin.org/cookies/set?cookie2=value2&cookie1=value1&cookie3=value3&cookie0=value0", "headers": {"date": ["Sat, 18 Apr 2015 16:05:26 GMT"], "content-length": ["223"], "set-cookie": ["cookie1=value1; Path=/", "cookie0=value0; Path=/", "cookie3=value3; Path=/", "cookie2=value2; Path=/"], "location": ["/cookies"], "connection": ["keep-alive"], "access-control-allow-origin": ["*"], "content-type": ["text/html; charset=utf-8"], "access-control-allow-credentials": ["true"], "server": ["nginx"]}, "status": {"message": "FOUND", "code": 302}}}, {"request": {"body": {"encoding": "utf-8", "string": ""}, "method": "GET", "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.6.0 CPython/3.4.2 Darwin/14.1.0"], "Accept": ["*/*"], "Cookie": ["cookie2=value2; cookie1=value1; cookie3=value3; cookie0=value0"]}, "uri": "https://httpbin.org/cookies"}, "recorded_at": "2015-04-18T16:05:26", "response": {"body": {"encoding": null, "string": "{\n \"cookies\": {\n \"cookie0\": \"value0\", \n \"cookie1\": \"value1\", \n \"cookie2\": \"value2\", \n \"cookie3\": \"value3\"\n }\n}\n"}, "url": "https://httpbin.org/cookies", "headers": {"date": ["Sat, 18 Apr 2015 16:05:26 GMT"], "content-length": ["125"], "access-control-allow-credentials": ["true"], "connection": ["keep-alive"], "access-control-allow-origin": ["*"], "content-type": ["application/json"], "server": ["nginx"]}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.4.1"}betamax-0.8.1/tests/cassettes/GitHub_emojis.json0000664000175000017500000003125113040145704023326 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": "", "headers": {"Accept-Charset": "utf-8", "Content-Type": "application/json", "Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "github3.py/0.8.0"}, "method": "GET", "uri": "https://api.github.com/emojis"}, "response": {"body": {"base64_string": "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", "encoding": "utf-8"}, "headers": {"status": "200 OK", "x-ratelimit-remaining": "56", "x-github-media-type": "github.v3; param=full; format=json", "x-content-type-options": "nosniff", "access-control-expose-headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", "transfer-encoding": "chunked", "x-github-request-id": "48A0C840:6705:C48B6D:52C21872", "content-encoding": "gzip", "vary": "Accept, Accept-Encoding", "server": "GitHub.com", "cache-control": "public, max-age=60, s-maxage=60", "x-ratelimit-limit": "60", "etag": "\"afa229eed862c51b678841bc6936cbd4\"", "access-control-allow-credentials": "true", "date": "Tue, 31 Dec 2013 01:05:54 GMT", "access-control-allow-origin": "*", "content-type": "application/json; charset=utf-8", "x-ratelimit-reset": "1388454935"}, "url": "https://api.github.com/emojis", "status_code": 200}, "recorded_at": "2013-12-31T01:04:59"}], "recorded_with": "betamax"}././@LongLink0000000000000000000000000000020600000000000011213 Lustar 00000000000000betamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[ccc-ddd].jsonbetamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesyste0000664000175000017500000000007313252043570036335 0ustar icordascicordasc00000000000000{"http_interactions": [], "recorded_with": "betamax/0.8.0"}././@LongLink0000000000000000000000000000021700000000000011215 Lustar 00000000000000betamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.TestPyTestParametrizedFixtures.test_pytest_fixture[https---httpbin.org-get].jsonbetamax-0.8.1/tests/cassettes/tests.integration.test_fixtures.TestPyTestParametrizedFixtures.test_py0000664000175000017500000000176513252043570036112 0ustar icordascicordasc00000000000000{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.13.0"]}, "method": "GET", "uri": "https://httpbin.org/get"}, "response": {"body": {"string": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"python-requests/2.13.0\"\n }, \n \"origin\": \"216.98.56.20\", \n \"url\": \"https://httpbin.org/get\"\n}\n", "encoding": null}, "headers": {"Content-Length": ["238"], "Server": ["nginx"], "Connection": ["keep-alive"], "Access-Control-Allow-Credentials": ["true"], "Date": ["Fri, 10 Mar 2017 16:58:21 GMT"], "Access-Control-Allow-Origin": ["*"], "Content-Type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get"}, "recorded_at": "2017-03-10T16:58:21"}], "recorded_with": "betamax/0.8.0"}betamax-0.8.1/tests/unit/0000775000175000017500000000000013252047671016673 5ustar icordascicordasc00000000000000betamax-0.8.1/tests/unit/test_fixtures.py0000664000175000017500000000533413040145704022151 0ustar icordascicordasc00000000000000try: import unittest.mock as mock except ImportError: import mock import pytest import unittest import requests import betamax from betamax.fixtures import pytest as pytest_fixture from betamax.fixtures import unittest as unittest_fixture class TestPyTestFixture(unittest.TestCase): def setUp(self): self.mocked_betamax = mock.MagicMock() self.patched_betamax = mock.patch.object( betamax.recorder, 'Betamax', return_value=self.mocked_betamax) self.patched_betamax.start() def tearDown(self): self.patched_betamax.stop() def test_adds_stop_as_a_finalizer(self): # Mock a pytest request object request = mock.MagicMock() request.cls = request.module = None request.function.__name__ = 'test' pytest_fixture.betamax_recorder(request) assert request.addfinalizer.called is True request.addfinalizer.assert_called_once_with(self.mocked_betamax.stop) def test_auto_starts_the_recorder(self): # Mock a pytest request object request = mock.MagicMock() request.cls = request.module = None request.function.__name__ = 'test' pytest_fixture.betamax_recorder(request) self.mocked_betamax.start.assert_called_once_with() class FakeBetamaxTestCase(unittest_fixture.BetamaxTestCase): def test_fake(self): pass class TestUnittestFixture(unittest.TestCase): def setUp(self): self.mocked_betamax = mock.MagicMock() self.patched_betamax = mock.patch.object( betamax.recorder, 'Betamax', return_value=self.mocked_betamax) self.betamax = self.patched_betamax.start() self.fixture = FakeBetamaxTestCase(methodName='test_fake') def tearDown(self): self.patched_betamax.stop() def test_setUp(self): self.fixture.setUp() self.mocked_betamax.use_cassette.assert_called_once_with( 'FakeBetamaxTestCase.test_fake' ) self.mocked_betamax.start.assert_called_once_with() def test_setUp_rejects_arbitrary_session_classes(self): self.fixture.SESSION_CLASS = object with pytest.raises(AssertionError): self.fixture.setUp() def test_setUp_accepts_session_subclasses(self): class TestSession(requests.Session): pass self.fixture.SESSION_CLASS = TestSession self.fixture.setUp() assert self.betamax.called is True call_kwargs = self.betamax.call_args[-1] assert isinstance(call_kwargs['session'], TestSession) def test_tearDown_calls_stop(self): recorder = mock.Mock() self.fixture.recorder = recorder self.fixture.tearDown() recorder.stop.assert_called_once_with() betamax-0.8.1/tests/unit/test_options.py0000664000175000017500000000530313040145704021767 0ustar icordascicordasc00000000000000import unittest from itertools import permutations import pytest from betamax import exceptions from betamax.options import Options, validate_record, validate_matchers class TestValidators(unittest.TestCase): def test_validate_record(self): for mode in ['once', 'none', 'all', 'new_episodes']: assert validate_record(mode) is True def test_validate_matchers(self): matchers = ['method', 'uri', 'query', 'host', 'body'] for i in range(1, len(matchers)): for l in permutations(matchers, i): assert validate_matchers(l) is True matchers.append('foobar') assert validate_matchers(matchers) is False class TestOptions(unittest.TestCase): def setUp(self): self.data = { 're_record_interval': 10000, 'match_requests_on': ['method'], 'serialize': 'json' } self.options = Options(self.data) def test_data_is_valid(self): for key in self.data: assert key in self.options def test_raise_on_unknown_option(self): data = self.data.copy() data['fake'] = 'value' with pytest.raises(exceptions.InvalidOption): Options(data) def test_raise_on_invalid_body_bytes(self): data = self.data.copy() data['preserve_exact_body_bytes'] = None with pytest.raises(exceptions.BodyBytesValidationError): Options(data) def test_raise_on_invalid_matchers(self): data = self.data.copy() data['match_requests_on'] = ['foo', 'bar', 'bogus'] with pytest.raises(exceptions.MatchersValidationError): Options(data) def test_raise_on_invalid_placeholders(self): data = self.data.copy() data['placeholders'] = None with pytest.raises(exceptions.PlaceholdersValidationError): Options(data) def test_raise_on_invalid_playback_repeats(self): data = self.data.copy() data['allow_playback_repeats'] = None with pytest.raises(exceptions.PlaybackRepeatsValidationError): Options(data) def test_raise_on_invalid_record(self): data = self.data.copy() data['record'] = None with pytest.raises(exceptions.RecordValidationError): Options(data) def test_raise_on_invalid_record_interval(self): data = self.data.copy() data['re_record_interval'] = -1 with pytest.raises(exceptions.RecordIntervalValidationError): Options(data) def test_raise_on_invalid_serializer(self): data = self.data.copy() data['serialize_with'] = None with pytest.raises(exceptions.SerializerValidationError): Options(data) betamax-0.8.1/tests/unit/test_adapter.py0000664000175000017500000000214013040145704021710 0ustar icordascicordasc00000000000000import unittest try: from unittest import mock except ImportError: import mock from betamax.adapter import BetamaxAdapter from requests.adapters import HTTPAdapter class TestBetamaxAdapter(unittest.TestCase): def setUp(self): http_adapter = mock.Mock() self.adapters_dict = {'http://': http_adapter} self.adapter = BetamaxAdapter(old_adapters=self.adapters_dict) def tearDown(self): self.adapter.eject_cassette() def test_has_http_adatper(self): assert self.adapter.http_adapter is not None assert isinstance(self.adapter.http_adapter, HTTPAdapter) def test_empty_initial_state(self): assert self.adapter.cassette is None assert self.adapter.cassette_name is None assert self.adapter.serialize is None def test_load_cassette(self): filename = 'test' self.adapter.load_cassette(filename, 'json', { 'record': 'none', 'cassette_library_dir': 'tests/cassettes/' }) assert self.adapter.cassette is not None assert self.adapter.cassette_name == filename betamax-0.8.1/tests/unit/test_exceptions.py0000664000175000017500000000323413252046551022463 0ustar icordascicordasc00000000000000import unittest import inspect from betamax import exceptions def exception_classes(): for _, module_object in inspect.getmembers(exceptions): if inspect.isclass(module_object): yield module_object class TestExceptions(unittest.TestCase): def test_all_exceptions_are_betamax_errors(self): for exception_class in exception_classes(): assert isinstance(exception_class('msg'), exceptions.BetamaxError) def test_all_validation_errors_are_in_validation_error_map(self): validation_error_map_values = exceptions.validation_error_map.values() for exception_class in exception_classes(): if exception_class.__name__ == 'ValidationError' or \ not exception_class.__name__.endswith('ValidationError'): continue assert exception_class in validation_error_map_values def test_all_validation_errors_are_validation_errors(self): for exception_class in exception_classes(): if not exception_class.__name__.endswith('ValidationError'): continue assert isinstance(exception_class('msg'), exceptions.ValidationError) def test_invalid_option_is_validation_error(self): assert isinstance(exceptions.InvalidOption('msg'), exceptions.ValidationError) def test_betamaxerror_repr(self): """Ensure errors don't raise exceptions in their __repr__. This should protect against regression. If this test starts failing, heavily modify it to not be flakey. """ assert "BetamaxError" in repr(exceptions.BetamaxError('test')) betamax-0.8.1/tests/unit/test_configure.py0000664000175000017500000000433313040145704022257 0ustar icordascicordasc00000000000000import collections import copy import unittest from betamax.configure import Configuration from betamax.cassette import Cassette class TestConfiguration(unittest.TestCase): def setUp(self): self.cassette_options = copy.deepcopy( Cassette.default_cassette_options ) self.cassette_dir = Configuration.CASSETTE_LIBRARY_DIR def tearDown(self): Cassette.default_cassette_options = self.cassette_options Cassette.hooks = collections.defaultdict(list) Configuration.CASSETTE_LIBRARY_DIR = self.cassette_dir def test_acts_as_pass_through(self): c = Configuration() c.default_cassette_options['foo'] = 'bar' assert 'foo' in Cassette.default_cassette_options assert Cassette.default_cassette_options.get('foo') == 'bar' def test_sets_cassette_library(self): c = Configuration() c.cassette_library_dir = 'foo' assert Configuration.CASSETTE_LIBRARY_DIR == 'foo' def test_is_a_context_manager(self): with Configuration() as c: assert isinstance(c, Configuration) def test_allows_registration_of_placeholders(self): opts = copy.deepcopy(Cassette.default_cassette_options) c = Configuration() c.define_cassette_placeholder('', 'test') assert opts != Cassette.default_cassette_options placeholders = Cassette.default_cassette_options['placeholders'] assert placeholders[0]['placeholder'] == '' assert placeholders[0]['replace'] == 'test' def test_registers_pre_record_hooks(self): c = Configuration() assert Cassette.hooks['before_record'] == [] c.before_record(callback=lambda: None) assert Cassette.hooks['before_record'] != [] assert len(Cassette.hooks['before_record']) == 1 assert callable(Cassette.hooks['before_record'][0]) def test_registers_pre_playback_hooks(self): c = Configuration() assert Cassette.hooks['before_playback'] == [] c.before_playback(callback=lambda: None) assert Cassette.hooks['before_playback'] != [] assert len(Cassette.hooks['before_playback']) == 1 assert callable(Cassette.hooks['before_playback'][0]) betamax-0.8.1/tests/unit/test_replays.py0000664000175000017500000000135313040145704021754 0ustar icordascicordasc00000000000000from betamax import Betamax, BetamaxError from requests import Session import unittest class TestReplays(unittest.TestCase): def setUp(self): self.session = Session() def test_replays_response_on_right_order(self): s = self.session opts = {'record': 'none'} with Betamax(s).use_cassette('test_replays_response_on_right_order', **opts) as betamax: self.cassette_path = betamax.current_cassette.cassette_path r0 = s.get('http://httpbin.org/get') r1 = s.get('http://httpbin.org/get') r0_found = (b'72.160.214.132' in r0.content) assert r0_found == True r1_found = (b'72.160.214.133' in r1.content) assert r1_found == True betamax-0.8.1/tests/unit/test_serializers.py0000664000175000017500000000252413040145704022632 0ustar icordascicordasc00000000000000import os import pytest import unittest from betamax.serializers import BaseSerializer, JSONSerializer class TestJSONSerializer(unittest.TestCase): def setUp(self): self.cassette_dir = 'fake_dir' self.cassette_name = 'cassette_name' def test_generate_cassette_name(self): assert (os.path.join('fake_dir', 'cassette_name.json') == JSONSerializer.generate_cassette_name(self.cassette_dir, self.cassette_name)) def test_generate_cassette_name_with_instance(self): serializer = JSONSerializer() assert (os.path.join('fake_dir', 'cassette_name.json') == serializer.generate_cassette_name(self.cassette_dir, self.cassette_name)) class Serializer(BaseSerializer): name = 'test' class TestBaseSerializer(unittest.TestCase): def test_serialize_is_an_interface(self): serializer = Serializer() with pytest.raises(NotImplementedError): serializer.serialize({}) def test_deserialize_is_an_interface(self): serializer = Serializer() with pytest.raises(NotImplementedError): serializer.deserialize('path') def test_requires_a_name(self): with pytest.raises(ValueError): BaseSerializer() betamax-0.8.1/tests/unit/test_decorator.py0000664000175000017500000000176713040145704022270 0ustar icordascicordasc00000000000000try: from unittest import mock except ImportError: import mock import betamax from betamax.decorator import use_cassette @mock.patch('betamax.recorder.Betamax', autospec=True) def test_wraps_session(Betamax): # This needs to be a magic mock so it will mock __exit__ recorder = mock.MagicMock(spec=betamax.Betamax) recorder.use_cassette.return_value = recorder Betamax.return_value = recorder @use_cassette('foo', cassette_library_dir='fizbarbogus') def _test(session): pass _test() Betamax.assert_called_once_with( session=mock.ANY, cassette_library_dir='fizbarbogus', default_cassette_options={} ) recorder.use_cassette.assert_called_once_with('foo') @mock.patch('betamax.recorder.Betamax', autospec=True) @mock.patch('requests.Session') def test_creates_a_new_session(Session, Betamax): @use_cassette('foo', cassette_library_dir='dir') def _test(session): pass _test() assert Session.call_count == 1 betamax-0.8.1/tests/unit/test_betamax.py0000664000175000017500000000361113040145704021715 0ustar icordascicordasc00000000000000import unittest from betamax import Betamax, matchers from betamax.adapter import BetamaxAdapter from betamax.cassette import Cassette from requests import Session from requests.adapters import HTTPAdapter class TestBetamax(unittest.TestCase): def setUp(self): self.session = Session() self.vcr = Betamax(self.session) def test_initialization_does_alter_the_session(self): for v in self.session.adapters.values(): assert not isinstance(v, BetamaxAdapter) assert isinstance(v, HTTPAdapter) def test_entering_context_alters_adapters(self): with self.vcr: for v in self.session.adapters.values(): assert isinstance(v, BetamaxAdapter) def test_exiting_resets_the_adapters(self): with self.vcr: pass for v in self.session.adapters.values(): assert not isinstance(v, BetamaxAdapter) def test_current_cassette(self): assert self.vcr.current_cassette is None self.vcr.use_cassette('test') assert isinstance(self.vcr.current_cassette, Cassette) def test_use_cassette_returns_cassette_object(self): assert self.vcr.use_cassette('test') is self.vcr def test_register_request_matcher(self): class FakeMatcher(object): name = 'fake' Betamax.register_request_matcher(FakeMatcher) assert 'fake' in matchers.matcher_registry assert isinstance(matchers.matcher_registry['fake'], FakeMatcher) def test_stores_the_session_instance(self): assert self.session is self.vcr.session def test_replaces_all_adapters(self): mount_point = 'fake_protocol://' s = Session() s.mount(mount_point, HTTPAdapter()) with Betamax(s): adapter = s.adapters.get(mount_point) assert adapter is not None assert isinstance(adapter, BetamaxAdapter) betamax-0.8.1/tests/unit/test_recorder.py0000664000175000017500000000551613040145704022107 0ustar icordascicordasc00000000000000import unittest from betamax import matchers, serializers from betamax.adapter import BetamaxAdapter from betamax.cassette import cassette from betamax.recorder import Betamax from requests import Session from requests.adapters import HTTPAdapter class TestBetamax(unittest.TestCase): def setUp(self): self.session = Session() self.vcr = Betamax(self.session) def test_initialization_does_not_alter_the_session(self): for v in self.session.adapters.values(): assert not isinstance(v, BetamaxAdapter) assert isinstance(v, HTTPAdapter) def test_initialization_converts_placeholders(self): placeholders = [{'placeholder': '', 'replace': 'replace-with'}] default_cassette_options = {'placeholders': placeholders} self.vcr = Betamax(self.session, default_cassette_options=default_cassette_options) assert self.vcr.config.default_cassette_options['placeholders'] == [{ 'placeholder': '', 'replace': 'replace-with', }] def test_entering_context_alters_adapters(self): with self.vcr: for v in self.session.adapters.values(): assert isinstance(v, BetamaxAdapter) def test_exiting_resets_the_adapters(self): with self.vcr: pass for v in self.session.adapters.values(): assert not isinstance(v, BetamaxAdapter) def test_current_cassette(self): assert self.vcr.current_cassette is None self.vcr.use_cassette('test') assert isinstance(self.vcr.current_cassette, cassette.Cassette) def test_use_cassette_returns_cassette_object(self): assert self.vcr.use_cassette('test') is self.vcr def test_register_request_matcher(self): class FakeMatcher(object): name = 'fake_matcher' Betamax.register_request_matcher(FakeMatcher) assert 'fake_matcher' in matchers.matcher_registry assert isinstance(matchers.matcher_registry['fake_matcher'], FakeMatcher) def test_register_serializer(self): class FakeSerializer(object): name = 'fake_serializer' Betamax.register_serializer(FakeSerializer) assert 'fake_serializer' in serializers.serializer_registry assert isinstance(serializers.serializer_registry['fake_serializer'], FakeSerializer) def test_stores_the_session_instance(self): assert self.session is self.vcr.session def test_use_cassette_passes_along_placeholders(self): placeholders = [{'placeholder': '', 'replace': 'replace-with'}] self.vcr.use_cassette('test', placeholders=placeholders) assert self.vcr.current_cassette.placeholders == [ cassette.Placeholder.from_dict(p) for p in placeholders ] betamax-0.8.1/tests/unit/test_matchers.py0000664000175000017500000001510613040145704022104 0ustar icordascicordasc00000000000000import unittest from requests import PreparedRequest from requests.cookies import RequestsCookieJar from betamax import matchers class TestMatchers(unittest.TestCase): def setUp(self): self.alt_url = ('http://example.com/path/to/end/point?query=string' '&foo=bar') self.p = PreparedRequest() self.p.body = 'Foo bar' self.p.headers = {'User-Agent': 'betamax/test'} self.p.url = 'http://example.com/path/to/end/point?query=string' self.p.method = 'GET' self.p._cookies = RequestsCookieJar() def test_matcher_registry_has_body_matcher(self): assert 'body' in matchers.matcher_registry def test_matcher_registry_has_digest_auth_matcher(self): assert 'digest-auth' in matchers.matcher_registry def test_matcher_registry_has_headers_matcher(self): assert 'headers' in matchers.matcher_registry def test_matcher_registry_has_host_matcher(self): assert 'host' in matchers.matcher_registry def test_matcher_registry_has_method_matcher(self): assert 'method' in matchers.matcher_registry def test_matcher_registry_has_path_matcher(self): assert 'path' in matchers.matcher_registry def test_matcher_registry_has_query_matcher(self): assert 'query' in matchers.matcher_registry def test_matcher_registry_has_uri_matcher(self): assert 'uri' in matchers.matcher_registry def test_body_matcher(self): match = matchers.matcher_registry['body'].match assert match(self.p, { 'body': 'Foo bar', 'headers': {'User-Agent': 'betamax/test'}, 'uri': 'http://example.com/path/to/end/point?query=string', 'method': 'GET', }) assert match(self.p, { 'body': b'', 'headers': {'User-Agent': 'betamax/test'}, 'uri': 'http://example.com/path/to/end/point?query=string', 'method': 'GET', }) is False def test_body_matcher_without_body(self): p = self.p.copy() p.body = None match = matchers.matcher_registry['body'].match assert match(p, { 'body': 'Foo bar', 'headers': {'User-Agent': 'betamax/test'}, 'uri': 'http://example.com/path/to/end/point?query=string', 'method': 'GET', }) is False assert match(p, { 'body': b'', 'headers': {'User-Agent': 'betamax/test'}, 'uri': 'http://example.com/path/to/end/point?query=string', 'method': 'GET', }) def test_digest_matcher(self): match = matchers.matcher_registry['digest-auth'].match assert match(self.p, {'headers': {}}) saved_auth = ( 'Digest username="user", realm="realm", nonce="nonce", uri="/", ' 'response="r", opaque="o", qop="auth", nc=00000001, cnonce="c"' ) self.p.headers['Authorization'] = saved_auth assert match(self.p, {'headers': {}}) is False assert match(self.p, {'headers': {'Authorization': saved_auth}}) new_auth = ( 'Digest username="user", realm="realm", nonce="nonce", uri="/", ' 'response="e", opaque="o", qop="auth", nc=00000001, cnonce="n"' ) assert match(self.p, {'headers': {'Authorization': new_auth}}) new_auth = ( 'Digest username="u", realm="realm", nonce="nonce", uri="/", ' 'response="e", opaque="o", qop="auth", nc=00000001, cnonce="n"' ) assert match(self.p, {'headers': {'Authorization': new_auth}}) is False def test_headers_matcher(self): match = matchers.matcher_registry['headers'].match assert match(self.p, {'headers': {'User-Agent': 'betamax/test'}}) assert match(self.p, {'headers': {'X-Sha': '6bbde0af'}}) is False def test_host_matcher(self): match = matchers.matcher_registry['host'].match assert match(self.p, {'uri': 'http://example.com'}) assert match(self.p, {'uri': 'https://example.com'}) assert match(self.p, {'uri': 'https://example.com/path'}) assert match(self.p, {'uri': 'https://example2.com'}) is False def test_method_matcher(self): match = matchers.matcher_registry['method'].match assert match(self.p, {'method': 'GET'}) assert match(self.p, {'method': 'POST'}) is False def test_path_matcher(self): match = matchers.matcher_registry['path'].match assert match(self.p, {'uri': 'http://example.com/path/to/end/point'}) assert match(self.p, {'uri': 'http://example.com:8000/path/to/end/point'}) assert match(self.p, {'uri': 'http://example.com:8000/path/to/end/'}) is False def test_query_matcher(self): match = matchers.matcher_registry['query'].match assert match( self.p, {'uri': 'http://example.com/path/to/end/point?query=string'} ) assert match( self.p, {'uri': 'http://example.com/?query=string'} ) self.p.url = self.alt_url assert match( self.p, {'uri': self.alt_url} ) # Regression test (order independence) assert match( self.p, {'uri': 'http://example.com/?foo=bar&query=string'} ) # Regression test (no query issue) assert match(self.p, {'uri': 'http://example.com'}) is False # Regression test (query with no value) self.p.url = 'https://example.com/?foo' assert match(self.p, {'uri': 'https://httpbin.org/?foo'}) is True def test_uri_matcher(self): match = matchers.matcher_registry['uri'].match assert match( self.p, {'uri': 'http://example.com/path/to/end/point?query=string'} ) assert match(self.p, {'uri': 'http://example.com'}) is False def test_uri_matcher_handles_query_strings(self): match = matchers.matcher_registry['uri'].match self.p.url = 'http://example.com/path/to?query=string&form=value' other_uri = 'http://example.com/path/to?form=value&query=string' assert match(self.p, {'uri': other_uri}) is True class TestBaseMatcher(unittest.TestCase): def setUp(self): class Matcher(matchers.BaseMatcher): pass self.Matcher = Matcher def test_requires_name(self): self.assertRaises(ValueError, self.Matcher) def test_requires_you_overload_match(self): self.Matcher.name = 'test' m = self.Matcher() self.assertRaises(NotImplementedError, m.match, None, None) betamax-0.8.1/tests/unit/test_cassette.py0000664000175000017500000004030513252043570022113 0ustar icordascicordasc00000000000000import email import os import unittest from datetime import datetime import pytest from betamax import __version__ from betamax.cassette import cassette from betamax import mock_response from betamax import recorder from betamax import serializers from betamax import util from requests.models import Response, Request from requests.packages import urllib3 from requests.structures import CaseInsensitiveDict try: from requests.packages.urllib3._collections import HTTPHeaderDict except ImportError: from betamax.headers import HTTPHeaderDict def decode(s): if hasattr(s, 'decode'): return s.decode() return s class Serializer(serializers.BaseSerializer): name = 'test' @staticmethod def generate_cassette_name(cassette_library_dir, cassette_name): return 'test_cassette.test' def on_init(self): self.serialize_calls = [] self.deserialize_calls = [] def serialize(self, data): self.serialize_calls.append(data) return '' def deserialize(self, data): self.deserialize_calls.append(data) return {} class TestSerialization(unittest.TestCase): """Unittests for the serialization and deserialization functions. This tests: - deserialize_prepared_request - deserialize_response - serialize_prepared_request - serialize_response """ def test_serialize_response(self): r = Response() r.status_code = 200 r.reason = 'OK' r.encoding = 'utf-8' r.headers = CaseInsensitiveDict() r.url = 'http://example.com' util.add_urllib3_response({ 'body': { 'string': decode('foo'), 'encoding': 'utf-8' } }, r, HTTPHeaderDict()) serialized = util.serialize_response(r, False) assert serialized is not None assert serialized != {} assert serialized['body']['encoding'] == 'utf-8' assert serialized['body']['string'] == 'foo' assert serialized['headers'] == {} assert serialized['url'] == 'http://example.com' assert serialized['status'] == {'code': 200, 'message': 'OK'} def test_deserialize_response_old(self): """For the previous version of Betamax and backwards compatibility.""" s = { 'body': { 'string': decode('foo'), 'encoding': 'utf-8' }, 'headers': { 'Content-Type': decode('application/json') }, 'url': 'http://example.com/', 'status_code': 200, 'recorded_at': '2013-08-31T00:00:01' } r = util.deserialize_response(s) assert r.content == b'foo' assert r.encoding == 'utf-8' assert r.headers == {'Content-Type': 'application/json'} assert r.url == 'http://example.com/' assert r.status_code == 200 def test_deserialize_response_new(self): """This adheres to the correct cassette specification.""" s = { 'body': { 'string': decode('foo'), 'encoding': 'utf-8' }, 'headers': { 'Content-Type': [decode('application/json')] }, 'url': 'http://example.com/', 'status': {'code': 200, 'message': 'OK'}, 'recorded_at': '2013-08-31T00:00:01' } r = util.deserialize_response(s) assert r.content == b'foo' assert r.encoding == 'utf-8' assert r.headers == {'Content-Type': 'application/json'} assert r.url == 'http://example.com/' assert r.status_code == 200 assert r.reason == 'OK' def test_serialize_prepared_request(self): r = Request() r.method = 'GET' r.url = 'http://example.com' r.headers = {'User-Agent': 'betamax/test header'} r.data = {'key': 'value'} p = r.prepare() serialized = util.serialize_prepared_request(p, False) assert serialized is not None assert serialized != {} assert serialized['method'] == 'GET' assert serialized['uri'] == 'http://example.com/' assert serialized['headers'] == { 'Content-Length': ['9'], 'Content-Type': ['application/x-www-form-urlencoded'], 'User-Agent': ['betamax/test header'], } assert serialized['body']['string'] == 'key=value' def test_deserialize_prepared_request(self): s = { 'body': 'key=value', 'headers': { 'User-Agent': 'betamax/test header', }, 'method': 'GET', 'uri': 'http://example.com/', } p = util.deserialize_prepared_request(s) assert p.body == 'key=value' assert p.headers == CaseInsensitiveDict( {'User-Agent': 'betamax/test header'} ) assert p.method == 'GET' assert p.url == 'http://example.com/' def test_from_list_returns_an_element(self): a = ['value'] assert util.from_list(a) == 'value' def test_from_list_handles_non_lists(self): a = 'value' assert util.from_list(a) == 'value' def test_add_urllib3_response(self): r = Response() r.status_code = 200 r.headers = {} util.add_urllib3_response({ 'body': { 'string': decode('foo'), 'encoding': 'utf-8' } }, r, HTTPHeaderDict()) assert isinstance(r.raw, urllib3.response.HTTPResponse) assert r.content == b'foo' assert isinstance(r.raw._original_response, mock_response.MockHTTPResponse) def test_cassette_initialization(): serializers.serializer_registry['test'] = Serializer() cassette.Cassette.default_cassette_options['placeholders'] = [] with recorder.Betamax.configure() as config: config.define_cassette_placeholder('', 'default') config.define_cassette_placeholder('', 'config') placeholders = [{ 'placeholder': '', 'replace': 'override', }, { 'placeholder': '', 'replace': 'cassette', }] instance = cassette.Cassette( 'test_cassette', 'test', placeholders=placeholders ) expected = [ cassette.Placeholder('', 'override'), cassette.Placeholder('', 'config'), cassette.Placeholder('', 'cassette'), ] assert instance.placeholders == expected cassette.Cassette.default_cassette_options['placeholders'] = [] class TestCassette(unittest.TestCase): cassette_name = 'test_cassette' def setUp(self): # Make a new serializer to test with self.test_serializer = Serializer() serializers.serializer_registry['test'] = self.test_serializer # Instantiate the cassette to test with self.cassette = cassette.Cassette( TestCassette.cassette_name, 'test', record_mode='once' ) # Create a new object to serialize r = Response() r.status_code = 200 r.reason = 'OK' r.encoding = 'utf-8' r.headers = CaseInsensitiveDict({'Content-Type': decode('foo')}) r.url = 'http://example.com' util.add_urllib3_response({ 'body': { 'string': decode('foo'), 'encoding': 'utf-8' } }, r, HTTPHeaderDict({'Content-Type': decode('foo')})) self.response = r # Create an associated request r = Request() r.method = 'GET' r.url = 'http://example.com' r.headers = {} r.data = {'key': 'value'} self.response.request = r.prepare() self.response.request.headers.update( {'User-Agent': 'betamax/test header'} ) # Expected serialized cassette data. self.json = { 'request': { 'body': { 'encoding': 'utf-8', 'string': 'key=value', }, 'headers': { 'User-Agent': ['betamax/test header'], 'Content-Length': ['9'], 'Content-Type': ['application/x-www-form-urlencoded'], }, 'method': 'GET', 'uri': 'http://example.com/', }, 'response': { 'body': { 'string': decode('foo'), 'encoding': 'utf-8', }, 'headers': {'Content-Type': [decode('foo')]}, 'status': {'code': 200, 'message': 'OK'}, 'url': 'http://example.com', }, 'recorded_at': '2013-08-31T00:00:00', } self.date = datetime(2013, 8, 31) self.cassette.save_interaction(self.response, self.response.request) self.interaction = self.cassette.interactions[0] def tearDown(self): try: self.cassette.eject() except: pass if os.path.exists(TestCassette.cassette_name): os.unlink(TestCassette.cassette_name) def test_serialize_interaction(self): serialized = self.interaction.data assert serialized['request'] == self.json['request'] assert serialized['response'] == self.json['response'] assert serialized.get('recorded_at') is not None def test_holds_interactions(self): assert isinstance(self.cassette.interactions, list) assert self.cassette.interactions != [] assert self.interaction in self.cassette.interactions def test_find_match(self): self.cassette.match_options = set(['uri', 'method']) self.cassette.record_mode = 'none' i = self.cassette.find_match(self.response.request) assert i is not None assert self.interaction is i def test_find_match__missing_matcher(self): self.cassette.match_options = set(['uri', 'method', 'invalid']) self.cassette.record_mode = 'none' with pytest.raises(KeyError): self.cassette.find_match(self.response.request) def test_eject(self): serializer = self.test_serializer self.cassette.eject() assert serializer.serialize_calls == [ {'http_interactions': [self.cassette.interactions[0].data], 'recorded_with': 'betamax/{0}'.format(__version__)} ] def test_earliest_recorded_date(self): assert self.interaction.recorded_at is not None assert self.cassette.earliest_recorded_date is not None class TestInteraction(unittest.TestCase): def setUp(self): self.request = { 'body': { 'string': 'key=value&key2=secret_value', 'encoding': 'utf-8' }, 'headers': { 'User-Agent': ['betamax/test header'], 'Content-Length': ['9'], 'Content-Type': ['application/x-www-form-urlencoded'], 'Authorization': ['123456789abcdef'], }, 'method': 'GET', 'uri': 'http://example.com/', } self.response = { 'body': { 'string': decode('foo'), 'encoding': 'utf-8' }, 'headers': { 'Content-Type': [decode('foo')], 'Set-Cookie': ['cookie_name=cookie_value', 'sessionid=deadbeef'] }, 'status_code': 200, 'url': 'http://example.com', } self.json = { 'request': self.request, 'response': self.response, 'recorded_at': '2013-08-31T00:00:00', } self.interaction = cassette.Interaction(self.json) self.date = datetime(2013, 8, 31) def test_as_response(self): r = self.interaction.as_response() assert isinstance(r, Response) def test_as_response_returns_new_instance(self): r1 = self.interaction.as_response() r2 = self.interaction.as_response() assert r1 is not r2 def test_deserialized_response(self): def check_uri(attr): # Necessary since PreparedRequests do not have a uri attr if attr == 'uri': return 'url' return attr r = self.interaction.as_response() for attr in ['status_code', 'url']: assert self.response[attr] == decode(getattr(r, attr)) headers = dict((k, ', '.join(v)) for k, v in self.response['headers'].items()) assert headers == r.headers tested_cookie = False for cookie in r.cookies: cookie_str = "{0}={1}".format(cookie.name, cookie.value) assert cookie_str in r.headers['Set-Cookie'] tested_cookie = True assert tested_cookie assert self.response['body']['string'] == decode(r.content) actual_req = r.request expected_req = self.request for attr in ['method', 'uri']: assert expected_req[attr] == getattr(actual_req, check_uri(attr)) assert self.request['body']['string'] == decode(actual_req.body) headers = dict((k, v[0]) for k, v in expected_req['headers'].items()) assert headers == actual_req.headers assert self.date == self.interaction.recorded_at def test_match(self): matchers = [lambda x: True, lambda x: False, lambda x: True] assert self.interaction.match(matchers) is False matchers[1] = lambda x: True assert self.interaction.match(matchers) is True def test_replace(self): self.interaction.replace('123456789abcdef', '') self.interaction.replace('cookie_value', '') self.interaction.replace('secret_value', '') self.interaction.replace('foo', '') self.interaction.replace('http://example.com', '') header = ( self.interaction.data['request']['headers']['Authorization'][0]) assert header == '' header = self.interaction.data['response']['headers']['Set-Cookie'] assert header[0] == 'cookie_name=' assert header[1] == 'sessionid=deadbeef' body = self.interaction.data['request']['body']['string'] assert body == 'key=value&key2=' body = self.interaction.data['response']['body'] assert body == {'encoding': 'utf-8', 'string': ''} uri = self.interaction.data['request']['uri'] assert uri == '/' uri = self.interaction.data['response']['url'] assert uri == '' def test_replace_in_headers(self): self.interaction.replace_in_headers('123456789abcdef', '') self.interaction.replace_in_headers('cookie_value', '') header = ( self.interaction.data['request']['headers']['Authorization'][0]) assert header == '' header = self.interaction.data['response']['headers']['Set-Cookie'][0] assert header == 'cookie_name=' def test_replace_in_body(self): self.interaction.replace_in_body('secret_value', '') self.interaction.replace_in_body('foo', '') body = self.interaction.data['request']['body']['string'] assert body == 'key=value&key2=' body = self.interaction.data['response']['body'] assert body == {'encoding': 'utf-8', 'string': ''} def test_replace_in_uri(self): self.interaction.replace_in_uri('http://example.com', '') uri = self.interaction.data['request']['uri'] assert uri == '/' uri = self.interaction.data['response']['url'] assert uri == '' class TestMockHTTPResponse(unittest.TestCase): def setUp(self): self.resp = mock_response.MockHTTPResponse(HTTPHeaderDict({ decode('Header'): decode('value') })) def test_isclosed(self): assert self.resp.isclosed() is False def test_is_Message(self): assert isinstance(self.resp.msg, email.message.Message) betamax-0.8.1/tests/regression/0000775000175000017500000000000013252047671020074 5ustar icordascicordasc00000000000000betamax-0.8.1/tests/regression/test_cassettes_retain_global_configuration.py0000664000175000017500000000151313040145704031303 0ustar icordascicordasc00000000000000import pytest import unittest from betamax import Betamax, cassette from requests import Session class TestCassetteRecordMode(unittest.TestCase): def setUp(self): with Betamax.configure() as config: config.default_cassette_options['record_mode'] = 'none' def tearDown(self): with Betamax.configure() as config: config.default_cassette_options['record_mode'] = 'once' def test_record_mode_is_none(self): s = Session() with pytest.raises(ValueError): with Betamax(s) as recorder: recorder.use_cassette('regression_record_mode') assert recorder.current_cassette is None def test_class_variables_retain_their_value(self): opts = cassette.Cassette.default_cassette_options assert opts['record_mode'] == 'none' betamax-0.8.1/tests/regression/test_requests_2_11_body_matcher.py0000664000175000017500000000163613040145704026617 0ustar icordascicordasc00000000000000import os import unittest import pytest import requests from betamax import Betamax class TestRequests211BodyMatcher(unittest.TestCase): def tearDown(self): os.unlink('tests/cassettes/requests_2_11_body_matcher.json') @pytest.mark.skipif(requests.__build__ < 0x020401, reason="No json keyword.") def test_requests_with_json_body(self): s = requests.Session() with Betamax(s).use_cassette('requests_2_11_body_matcher', match_requests_on=['body']): r = s.post('https://httpbin.org/post', json={'a': 2}) assert r.json() is not None s = requests.Session() with Betamax(s).use_cassette('requests_2_11_body_matcher', match_requests_on=['body']): r = s.post('https://httpbin.org/post', json={'a': 2}) assert r.json() is not None betamax-0.8.1/tests/regression/test_gzip_compression.py0000664000175000017500000000226113040145704025067 0ustar icordascicordasc00000000000000import os import unittest from betamax import Betamax from requests import Session class TestGZIPRegression(unittest.TestCase): def tearDown(self): os.unlink('tests/cassettes/gzip_regression.json') def test_saves_content_as_gzip(self): s = Session() with Betamax(s).use_cassette('gzip_regression'): r = s.get( 'https://api.github.com/repos/github3py/fork_this/issues/1', headers={'Accept-Encoding': 'gzip, deflate, compress'} ) assert r.headers.get('Content-Encoding') == 'gzip' assert r.json() is not None r2 = s.get( 'https://api.github.com/repos/github3py/fork_this/issues/1', headers={'Accept-Encoding': 'gzip, deflate, compress'} ) assert r2.headers.get('Content-Encoding') == 'gzip' assert r2.json() is not None assert r2.json() == r.json() s = Session() with Betamax(s).use_cassette('gzip_regression'): r = s.get( 'https://api.github.com/repos/github3py/fork_this/issues/1' ) assert r.json() is not None betamax-0.8.1/tests/regression/test_once_prevents_new_interactions.py0000664000175000017500000000072513040145704030005 0ustar icordascicordasc00000000000000import pytest import unittest from betamax import Betamax, BetamaxError from requests import Session class TestOncePreventsNewInteractions(unittest.TestCase): """Test that using a cassette with once record mode prevents new requests. """ def test_once_prevents_new_requests(self): s = Session() with Betamax(s).use_cassette('once_record_mode'): with pytest.raises(BetamaxError): s.get('http://example.com') betamax-0.8.1/tests/regression/test_can_replay_interactions_multiple_times.py0000664000175000017500000000121713040145704031510 0ustar icordascicordasc00000000000000import unittest from betamax import Betamax from requests import Session class TestReplayInteractionMultipleTimes(unittest.TestCase): """ Test that an Interaction can be replayed multiple times within the same betamax session. """ def test_replay_interaction_more_than_once(self): s = Session() with Betamax(s).use_cassette('replay_multiple_times', record='once', allow_playback_repeats=True): for k in range(1, 5): r = s.get('http://httpbin.org/stream/3', stream=True) assert r.raw.read(1028), "Stream already consumed. Try: %d" % k betamax-0.8.1/tests/regression/test_works_with_digest_auth.py0000664000175000017500000000156113040145704026257 0ustar icordascicordasc00000000000000import unittest from betamax import Betamax from requests import Session from requests.auth import HTTPDigestAuth class TestDigestAuth(unittest.TestCase): def test_saves_content_as_gzip(self): s = Session() cassette_name = 'handles_digest_auth' match = ['method', 'uri', 'digest-auth'] with Betamax(s).use_cassette(cassette_name, match_requests_on=match): r = s.get('https://httpbin.org/digest-auth/auth/user/passwd', auth=HTTPDigestAuth('user', 'passwd')) assert r.ok assert r.history[0].status_code == 401 s = Session() with Betamax(s).use_cassette(cassette_name, match_requests_on=match): r = s.get('https://httpbin.org/digest-auth/auth/user/passwd', auth=HTTPDigestAuth('user', 'passwd')) assert r.json() is not None betamax-0.8.1/tests/__init__.py0000664000175000017500000000000013040145704020002 0ustar icordascicordasc00000000000000betamax-0.8.1/tests/integration/0000775000175000017500000000000013252047671020237 5ustar icordascicordasc00000000000000betamax-0.8.1/tests/integration/test_fixtures.py0000664000175000017500000000451613252043570023521 0ustar icordascicordasc00000000000000import 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 @pytest.mark.usefixtures('betamax_parametrized_session') class TestPyTestParametrizedFixtures: @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 'TestPyTestParametrizedFixtures.' # Class name 'test_pytest_fixture' # Test function name '[https---httpbin.org-get]' # Parameter '.json') file_name = os.path.join(test_dir, 'tests', 'cassettes', cassette_name) assert os.path.exists(file_name) is True request.addfinalizer(finalizer) @pytest.mark.parametrize('url', ('https://httpbin.org/get',)) def test_pytest_fixture(self, betamax_parametrized_session, url): """Exercise the fixture itself.""" resp = betamax_parametrized_session.get(url) assert resp.ok @pytest.mark.parametrize('problematic_arg', [r'aaa\bbb', 'ccc:ddd', 'eee*fff']) def test_pytest_parametrize_with_filesystem_problematic_chars( betamax_parametrized_session, problematic_arg): """ Exercice parametrized args containing characters which might cause problems when getting translated into file names. """ assert True betamax-0.8.1/tests/integration/test_allow_playback_repeats.py0000664000175000017500000000210313040145704026342 0ustar icordascicordasc00000000000000import betamax from tests.integration import helper class TestPlaybackRepeatInteractions(helper.IntegrationHelper): def test_will_replay_the_same_interaction(self): self.cassette_created = False s = self.session recorder = betamax.Betamax(s) # NOTE(sigmavirus24): Ensure the cassette is recorded with recorder.use_cassette('replay_interactions'): cassette = recorder.current_cassette r = s.get('http://httpbin.org/get') assert r.status_code == 200 assert len(cassette.interactions) == 1 with recorder.use_cassette('replay_interactions', allow_playback_repeats=True): cassette = recorder.current_cassette r = s.get('http://httpbin.org/get') assert r.status_code == 200 assert len(cassette.interactions) == 1 r = s.get('http://httpbin.org/get') assert r.status_code == 200 assert len(cassette.interactions) == 1 assert cassette.interactions[0].used is False betamax-0.8.1/tests/integration/test_backwards_compat.py0000664000175000017500000000350313040145704025144 0ustar icordascicordasc00000000000000import 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].data header = interaction['request']['headers']['Accept'] assert not isinstance(header, list) betamax-0.8.1/tests/integration/test_preserve_exact_body_bytes.py0000664000175000017500000000344713040145704027111 0ustar icordascicordasc00000000000000from .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].data 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].data assert 'base64_string' in interaction['response']['body'] betamax-0.8.1/tests/integration/test_hooks.py0000664000175000017500000000456613040145704022775 0ustar icordascicordasc00000000000000import betamax from . import helper def prerecord_hook(interaction, cassette): assert cassette.interactions == [] interaction.data['response']['headers']['Betamax-Fake-Header'] = 'success' def ignoring_hook(interaction, cassette): interaction.ignore() def preplayback_hook(interaction, cassette): assert cassette.interactions != [] interaction.data['response']['headers']['Betamax-Fake-Header'] = 'temp' class TestHooks(helper.IntegrationHelper): def tearDown(self): super(TestHooks, self).tearDown() # Clear out the hooks betamax.cassette.Cassette.hooks.pop('before_record', None) betamax.cassette.Cassette.hooks.pop('before_playback', None) def test_prerecord_hook(self): with betamax.Betamax.configure() as config: config.before_record(callback=prerecord_hook) recorder = betamax.Betamax(self.session) with recorder.use_cassette('prerecord_hook'): self.cassette_path = recorder.current_cassette.cassette_path response = self.session.get('https://httpbin.org/get') assert response.headers['Betamax-Fake-Header'] == 'success' with recorder.use_cassette('prerecord_hook', record='none'): response = self.session.get('https://httpbin.org/get') assert response.headers['Betamax-Fake-Header'] == 'success' def test_preplayback_hook(self): with betamax.Betamax.configure() as config: config.before_playback(callback=preplayback_hook) recorder = betamax.Betamax(self.session) with recorder.use_cassette('preplayback_hook'): self.cassette_path = recorder.current_cassette.cassette_path self.session.get('https://httpbin.org/get') with recorder.use_cassette('preplayback_hook', record='none'): response = self.session.get('https://httpbin.org/get') assert response.headers['Betamax-Fake-Header'] == 'temp' def test_prerecord_ignoring_hook(self): with betamax.Betamax.configure() as config: config.before_record(callback=ignoring_hook) recorder = betamax.Betamax(self.session) with recorder.use_cassette('ignore_hook'): self.cassette_path = recorder.current_cassette.cassette_path self.session.get('https://httpbin.org/get') assert recorder.current_cassette.interactions == [] betamax-0.8.1/tests/integration/test_record_modes.py0000664000175000017500000001332013252043570024306 0ustar icordascicordasc00000000000000from 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) == 2 assert r1.status_code == 200 r0_headers = r0.headers.copy() r0_headers.pop('Date') r0_headers.pop('Age', None) r0_headers.pop('X-Processed-Time', None) r1_headers = r1.headers.copy() r1_headers.pop('Date') r1_headers.pop('Age', None) r1_headers.pop('X-Processed-Time', None) # NOTE(sigmavirus24): This fails if the second request is # technically a second later. Ignoring the Date headers allows # this test to succeed. # NOTE(hroncok): httpbin.org added X-Processed-Time header that # can possibly differ (and often does) 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) == 4 assert cassette.is_empty() is False s.get('https://httpbin.org/get') assert len(cassette.interactions) == 5 with Betamax(s).use_cassette('test_record_new') as betamax: cassette = betamax.current_cassette assert len(cassette.interactions) == 5 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') self.session.get('http://httpbin.org/get') 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) == 5 assert cassette.is_empty() is False s.post('http://httpbin.org/post', data={'foo': 'bar'}) assert len(cassette.interactions) == 6 with Betamax(s).use_cassette('test_record_all') as betamax: assert len(betamax.current_cassette.interactions) == 6 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) == 5 assert cassette.is_empty() is False s.get('http://httpbin.org/get') assert len(cassette.interactions) == 5 betamax-0.8.1/tests/integration/helper.py0000664000175000017500000000056213040145704022062 0ustar icordascicordasc00000000000000import 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.8.1/tests/integration/test_multiple_cookies.py0000664000175000017500000000265513040145704025216 0ustar icordascicordasc00000000000000import 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.8.1/tests/integration/__init__.py0000664000175000017500000000000013040145704022325 0ustar icordascicordasc00000000000000betamax-0.8.1/tests/integration/test_placeholders.py0000664000175000017500000000274513252043570024317 0ustar icordascicordasc00000000000000from 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'] assert placeholders == [{ 'placeholder': '', 'replace': b64_foobar, }] 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 self.cassette_path = cassette.cassette_path assert r.status_code == 200 auth = r.json()['headers']['Authorization'] assert b64_foobar in auth self.cassette_path = cassette.cassette_path i = cassette.interactions[0] auth = i.data['request']['headers']['Authorization'] assert '' in auth[0] betamax-0.8.1/tests/integration/test_unicode.py0000664000175000017500000000073113040145704023266 0ustar icordascicordasc00000000000000from 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) betamax-0.8.1/tests/conftest.py0000664000175000017500000000026013040145704020100 0ustar icordascicordasc00000000000000import os import sys import betamax sys.path.insert(0, os.path.abspath('.')) with betamax.Betamax.configure() as config: config.cassette_library_dir = 'tests/cassettes/' betamax-0.8.1/setup.py0000664000175000017500000000352213252047377016271 0ustar icordascicordasc00000000000000import os import re import sys from setuptools import setup, find_packages packages = find_packages(exclude=['tests', 'tests.integration']) 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="Apache 2.0", author="Ian Stapleton 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.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', ] ) betamax-0.8.1/betamax.egg-info/0000775000175000017500000000000013252047671017665 5ustar icordascicordasc00000000000000betamax-0.8.1/betamax.egg-info/entry_points.txt0000664000175000017500000000006513252047671023164 0ustar icordascicordasc00000000000000[pytest11] pytest-betamax = betamax.fixtures.pytest betamax-0.8.1/betamax.egg-info/dependency_links.txt0000664000175000017500000000000113252047671023733 0ustar icordascicordasc00000000000000 betamax-0.8.1/betamax.egg-info/SOURCES.txt0000664000175000017500000000670113252047671021555 0ustar icordascicordasc00000000000000AUTHORS.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/headers.py betamax/mock_response.py betamax/options.py betamax/recorder.py betamax/util.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/interaction.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 docs/Makefile docs/api.rst docs/cassettes.rst docs/conf.py docs/configuring.rst docs/implementation_details.rst docs/index.rst docs/integrations.rst docs/introduction.rst docs/long_term_usage.rst docs/matchers.rst docs/record_modes.rst docs/serializers.rst docs/third_party_packages.rst docs/usage_patterns.rst tests/__init__.py tests/conftest.py tests/cassettes/GitHub_create_issue.json tests/cassettes/GitHub_emojis.json tests/cassettes/global_preserve_exact_body_bytes.json tests/cassettes/handles_digest_auth.json tests/cassettes/once_record_mode.json tests/cassettes/preserve_exact_bytes.json tests/cassettes/replay_interactions.json tests/cassettes/replay_multiple_times.json tests/cassettes/test-multiple-cookies-regression.json tests/cassettes/test.json tests/cassettes/test_replays_response_on_right_order.json tests/cassettes/tests.integration.test_fixtures.TestPyTestFixtures.test_pytest_fixture.json tests/cassettes/tests.integration.test_fixtures.TestPyTestParametrizedFixtures.test_pytest_fixture[https---httpbin.org-get].json tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[aaa-bbb].json tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[ccc-ddd].json tests/cassettes/tests.integration.test_fixtures.test_pytest_parametrize_with_filesystem_problematic_chars[eee-fff].json tests/integration/__init__.py tests/integration/helper.py tests/integration/test_allow_playback_repeats.py tests/integration/test_backwards_compat.py tests/integration/test_fixtures.py tests/integration/test_hooks.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.py tests/regression/test_can_replay_interactions_multiple_times.py tests/regression/test_cassettes_retain_global_configuration.py tests/regression/test_gzip_compression.py tests/regression/test_once_prevents_new_interactions.py tests/regression/test_requests_2_11_body_matcher.py tests/regression/test_works_with_digest_auth.py tests/unit/test_adapter.py tests/unit/test_betamax.py tests/unit/test_cassette.py tests/unit/test_configure.py tests/unit/test_decorator.py tests/unit/test_exceptions.py tests/unit/test_fixtures.py tests/unit/test_matchers.py tests/unit/test_options.py tests/unit/test_recorder.py tests/unit/test_replays.py tests/unit/test_serializers.pybetamax-0.8.1/betamax.egg-info/requires.txt0000664000175000017500000000001613252047671022262 0ustar icordascicordasc00000000000000requests>=2.0 betamax-0.8.1/betamax.egg-info/PKG-INFO0000664000175000017500000003100613252047671020762 0ustar icordascicordasc00000000000000Metadata-Version: 1.1 Name: betamax Version: 0.8.1 Summary: A VCR imitation for python-requests Home-page: https://github.com/sigmavirus24/betamax Author: Ian Stapleton Cordasco Author-email: graffatcolmingov@gmail.com License: Apache 2.0 Description-Content-Type: UNKNOWN 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.8.1 - 2018-03-13 ------------------ - Previous attempts to sanitize cassette names were incomplete. Sanitization has become more thorough which could have some affects on existing cassette files. **This may cause new cassettes to be generated.** - Fix bug where there may be an exception raised in a ``betamax.exceptions.BetamaxError`` repr. 0.8.0 - 2016-08-16 ------------------ - Add ``betamax_parametrized_recorder`` and ``betamax_parametrized_session`` to our list of pytest fixtures so that users will have parametrized cassette names when writing parametrized tests with our fixtures. (I wonder if I can mention parametrization a bunch more times so I can say parametrize a lot in this bullet note.) - Add ``ValidationError`` and a set of subclasses for each possible validation error. - Raise ``InvalidOption`` on unknown cassette options rather than silently ignoring extra options. - Raise a subclass of ``ValidationError`` when a particular cassette option is invalid, rather than silently ignoring the validation failure. 0.7.2 - 2016-08-04 ------------------ - Fix bug with query string matcher where query-strings without values (e.g., ``?foo&bar`` as opposed to ``?foo=1&bar=2``) were treated as if there were no query string. 0.7.1 - 2016-06-14 ------------------ - Fix issue #108 by effectively copying the items in the match_requests_on list into the match_options set on a Cassette instance 0.7.0 - 2016-04-29 ------------------ - Add ``before_record`` and ``before_playback`` hooks - Allow per-cassette placeholders to be merged and override global placeholders - Fix bug where the ``QueryMatcher`` failed matching on high Unicode points 0.6.0 - 2016-04-12 ------------------ - Add ``betamax_recorder`` pytest fixture - Change default behaviour to allow duplicate interactions to be recorded in single cassette - Add ``allow_playback_repeats`` to allow an interaction to be used more than once from a single cassette - Always return a new ``Response`` object from an Interaction to allow for a streaming response to be usable multiple times - Remove CI support for Pythons 2.6 and 3.2 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.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython betamax-0.8.1/betamax.egg-info/top_level.txt0000664000175000017500000000001013252047671022406 0ustar icordascicordasc00000000000000betamax betamax-0.8.1/PKG-INFO0000664000175000017500000003100613252047671015647 0ustar icordascicordasc00000000000000Metadata-Version: 1.1 Name: betamax Version: 0.8.1 Summary: A VCR imitation for python-requests Home-page: https://github.com/sigmavirus24/betamax Author: Ian Stapleton Cordasco Author-email: graffatcolmingov@gmail.com License: Apache 2.0 Description-Content-Type: UNKNOWN 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.8.1 - 2018-03-13 ------------------ - Previous attempts to sanitize cassette names were incomplete. Sanitization has become more thorough which could have some affects on existing cassette files. **This may cause new cassettes to be generated.** - Fix bug where there may be an exception raised in a ``betamax.exceptions.BetamaxError`` repr. 0.8.0 - 2016-08-16 ------------------ - Add ``betamax_parametrized_recorder`` and ``betamax_parametrized_session`` to our list of pytest fixtures so that users will have parametrized cassette names when writing parametrized tests with our fixtures. (I wonder if I can mention parametrization a bunch more times so I can say parametrize a lot in this bullet note.) - Add ``ValidationError`` and a set of subclasses for each possible validation error. - Raise ``InvalidOption`` on unknown cassette options rather than silently ignoring extra options. - Raise a subclass of ``ValidationError`` when a particular cassette option is invalid, rather than silently ignoring the validation failure. 0.7.2 - 2016-08-04 ------------------ - Fix bug with query string matcher where query-strings without values (e.g., ``?foo&bar`` as opposed to ``?foo=1&bar=2``) were treated as if there were no query string. 0.7.1 - 2016-06-14 ------------------ - Fix issue #108 by effectively copying the items in the match_requests_on list into the match_options set on a Cassette instance 0.7.0 - 2016-04-29 ------------------ - Add ``before_record`` and ``before_playback`` hooks - Allow per-cassette placeholders to be merged and override global placeholders - Fix bug where the ``QueryMatcher`` failed matching on high Unicode points 0.6.0 - 2016-04-12 ------------------ - Add ``betamax_recorder`` pytest fixture - Change default behaviour to allow duplicate interactions to be recorded in single cassette - Add ``allow_playback_repeats`` to allow an interaction to be used more than once from a single cassette - Always return a new ``Response`` object from an Interaction to allow for a streaming response to be usable multiple times - Remove CI support for Pythons 2.6 and 3.2 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.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython betamax-0.8.1/AUTHORS.rst0000664000175000017500000000047613040145704016427 0ustar icordascicordasc00000000000000Development Lead ---------------- - Ian Cordasco Requests ```````` - Kenneth Reitz Design Advice ------------- - Cory Benfield Contributors ------------ - Marc Abramowitz (@msabramo) - Bryce Boe (@bboe) - Alex Richard-Hoyling <@arhoyling) betamax-0.8.1/docs/0000775000175000017500000000000013252047671015502 5ustar icordascicordasc00000000000000betamax-0.8.1/docs/serializers.rst0000664000175000017500000000356313040145704020566 0ustar icordascicordasc00000000000000Serializers =========== You can tell Betamax how you would like it to serialize the cassettes when saving them to a file. By default Betamax will serialize your cassettes to JSON. The only default serializer is the JSON serializer, but writing your own is very easy. Creating Your Own Serializer ---------------------------- Betamax handles the structuring of the cassette and writing to a file, your serializer simply takes a `dictionary `_ and returns a string. Every Serializer has to inherit from :class:`betamax.BaseSerializer` and implement three methods: - ``betamax.BaseSerializer.generate_cassette_name`` which is a static method. This will take the directory the user (you) wants to store the cassettes in and the name of the cassette and generate the file name. - :py:meth:`betamax.BaseSerializer.seralize` is a method that takes the dictionary and returns the dictionary serialized as a string - :py:meth:`betamax.BaseSerializer.deserialize` is a method that takes a string and returns the data serialized in it as a dictionary. .. autoclass:: betamax.BaseSerializer :members: Here's the default (JSON) serializer as an example: .. literalinclude:: ../betamax/serializers/json_serializer.py :language: python This is incredibly simple. We take advantage of the :mod:`os.path` to properly join the directory name and the file name. Betamax uses this method to find an existing cassette or create a new one. Next we have the :py:meth:`betamax.serializers.JSONSerializer.serialize` which takes the cassette dictionary and turns it into a string for us. Here we are just leveraging the :mod:`json` module and its ability to dump any valid dictionary to a string. Finally, there is the :py:meth:`betamax.serializers.JSONSerializer.deserialize` method which takes a string and turns it into the dictionary that betamax needs to function. betamax-0.8.1/docs/conf.py0000664000175000017500000001772313040145704017002 0ustar icordascicordasc00000000000000# -*- coding: utf-8 -*- # # Requests documentation build configuration file, created by # sphinx-quickstart on Sun Feb 13 23:54:25 2011. # # This file is execfile()d with the current directory set to its containing # dir # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # This environment variable makes decorators not decorate functions, so their # signatures in the generated documentation are still correct os.environ['GENERATING_DOCUMENTATION'] = "betamax" # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) import betamax # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Betamax' copyright = u'2013-2015 - Ian Cordasco' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = betamax.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = 'flask_theme_support.FlaskyStyle' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output ----------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'betamax.doc' # -- Options for LaTeX output ---------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'betamax.tex', u'Betamax Documentation', u'Ian Cordasco', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ---------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'betamax', u'Betamax Documentation', [u'Ian Cordasco'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output -------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'betamax', u'Betamax Documentation', u'Ian Cordasco', 'Betamax', "Python imitation of Ruby's VCR", 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. texinfo_appendices = [] # Intersphinx configuration intersphinx_mapping = { 'python': ('https://docs.python.org/3.4', (None, 'python-inv.txt')), 'requests': ('http://docs.python-requests.org/en/latest', (None, 'requests-inv.txt')), } betamax-0.8.1/docs/implementation_details.rst0000664000175000017500000000306513040145704022761 0ustar icordascicordasc00000000000000Implementation Details ====================== Everything here is an implementation detail and subject to volatile change. I would not rely on anything here for any mission critical code. Gzip Content-Encoding --------------------- By default, requests sets an ``Accept-Encoding`` header value that includes ``gzip`` (specifically, unless overridden, requests always sends ``Accept-Encoding: gzip, deflate, compress``). When a server supports this and responds with a response that has the ``Content-Encoding`` header set to ``gzip``, ``urllib3`` automatically decompresses the body for requests. This can only be prevented in the case where the ``stream`` parameter is set to ``True``. Since Betamax refuses to alter the headers on the response object in any way, we force ``stream`` to be ``True`` so we can capture the compressed data before it is decompressed. We then properly repopulate the response object so you perceive no difference in the interaction. To preserve the response exactly as is, we then must ``base64`` encode the body of the response before saving it to the file object. In other words, whenever a server responds with a compressed body, you will not have a human readable response body. There is, at the present moment, no way to configure this so that this does not happen and because of the way that Betamax works, you can not remove the ``Content-Encoding`` header to prevent this from happening. Class Details ------------- .. autoclass:: betamax.cassette.Cassette :members: .. autoclass:: betamax.cassette.Interaction :members: betamax-0.8.1/docs/third_party_packages.rst0000664000175000017500000001213113040145704022410 0ustar icordascicordasc00000000000000Third-Party Packages ==================== Betamax was created to be a very close imitation of `VCR`_. As such, it has the default set of request matchers and a subset of the supported cassette serializers for VCR. As part of my own usage of Betamax, and supporting other people's usage of Betamax, I've created (and maintain) two third party packages that provide extra request matchers and cassette serializers. - `betamax-matchers`_ - `betamax-serializers`_ For simplicity, those modules will be documented here instead of on their own documentation sites. Request Matchers ---------------- There are three third-party request matchers provided by the `betamax-matchers`_ package: - :class:`~betamax_matchers.form_urlencoded.URLEncodedBodyMatcher`, ``'form-urlencoded-body'`` - :class:`~betamax_matchers.json_body.JSONBodyMatcher`, ``'json-body'`` - :class:`~betamax_matchers.multipart.MultipartFormDataBodyMatcher`, ``'multipart-form-data-body'`` In order to use any of these we have to register them with Betamax. Below we will register all three but you do not need to do that if you only need to use one: .. code-block:: python import betamax from betamax_matchers import form_urlencoded from betamax_matchers import json_body from betamax_matchers import multipart betamax.Betamax.register_request_matcher( form_urlencoded.URLEncodedBodyMatcher ) betamax.Betamax.register_request_matcher( json_body.JSONBodyMatcher ) betamax.Betamax.register_request_matcher( multipart.MultipartFormDataBodyMatcher ) All of these classes inherit from :class:`betamax.BaseMatcher` which means that each needs a name that will be used when specifying what matchers to use with Betamax. I have noted those next to the class name for each matcher above. Let's use the JSON body matcher in an example though: .. code-block:: python import betamax from betamax_matchers import json_body # This example requires at least requests 2.5.0 import requests betamax.Betamax.register_request_matcher( json_body.JSONBodyMatcher ) def main(): session = requests.Session() recorder = betamax.Betamax(session, cassette_library_dir='.') url = 'https://httpbin.org/post' json_data = {'key': 'value', 'other-key': 'other-value', 'yet-another-key': 'yet-another-value'} matchers = ['method', 'uri', 'json-body'] with recorder.use_cassette('json-body-example', match_requests_on=matchers): r = session.post(url, json=json_data) if __name__ == '__main__': main() If we ran that request without those matcher with hash seed randomization, then we would occasionally receive exceptions that a request could not be matched. That is because dictionaries are not inherently ordered so the body string of the request can change and be any of the following: .. code-block:: js {"key": "value", "other-key": "other-value", "yet-another-key": "yet-another-value"} .. code-block:: js {"key": "value", "yet-another-key": "yet-another-value", "other-key": "other-value"} .. code-block:: js {"other-key": "other-value", "yet-another-key": "yet-another-value", "key": "value"} .. code-block:: js {"yet-another-key": "yet-another-value", "key": "value", "other-key": "other-value"} .. code-block:: js {"yet-another-key": "yet-another-value", "other-key": "other-value", "key": "value"} .. code-block:: js {"other-key": "other-value", "key": "value", "yet-another-key": "yet-another-value"} But using the ``'json-body'`` matcher, the matcher will parse the request and compare python dictionaries instead of python strings. That will completely bypass the issues introduced by hash randomization. I use this matcher extensively in `github3.py`_\ 's tests. Cassette Serializers -------------------- By default, Betamax only comes with the JSON serializer. `betamax-serializers`_ provides extra serializer classes that users have contributed. For example, as we've seen elsewhere in our documentation, the default JSON serializer does not create beautiful or easy to read cassettes. As a substitute for that, we have the :class:`~betamax_serializers.pretty_json.PrettyJSONSerializer` that does that for you. .. code-block:: python from betamax import Betamax from betamax_serializers import pretty_json import requests Betamax.register_serializer(pretty_json.PrettyJSONSerializer) session = requests.Session() recorder = Betamax(session) with recorder.use_cassette('testpretty', serialize_with='prettyjson'): session.request(method=method, url=url, ...) This will give us a pretty-printed cassette like: .. literalinclude:: ../examples/cassettes/more-complicated-cassettes.json :language: js .. links .. _VCR: https://relishapp.com/vcr/vcr .. _betamax-matchers: https://pypi.python.org/pypi/betamax-matchers .. _betamax-serializers: https://pypi.python.org/pypi/betamax-serializers .. _github3.py: https://github.com/sigmavirus24/github3.py betamax-0.8.1/docs/configuring.rst0000664000175000017500000002665413040145704020552 0ustar icordascicordasc00000000000000Configuring Betamax =================== By now you've seen examples where we pass a great deal of keyword arguments to :meth:`~betamax.Betamax.use_cassette`. You have also seen that we used :meth:`betamax.Betamax.configure`. In this section, we'll go into a deep description of the different approaches and why you might pick one over the other. Global Configuration -------------------- Admittedly, I am not too proud of my decision to borrow this design from `VCR`_, but I did and I use it and it isn't entirely terrible. (Note: I do hope to come up with an elegant way to redesign it for v1.0.0 but that's a long way off.) The best way to configure Betamax globally is by using :meth:`betamax.Betamax.configure`. This returns a :class:`betamax.configure.Configuration` instance. This instance can be used as a context manager in order to make the usage look more like `VCR`_'s way of configuring the library. For example, in `VCR`_, you might do .. code-block:: ruby VCR.configure do |config| config.cassette_library_dir = 'examples/cassettes' config.default_cassette_options[:record] = :none # ... end Where as with Betamax you might do .. code-block:: python from betamax import Betamax with Betamax.configure() as config: config.cassette_library_dir = 'examples/cassettes' config.default_cassette_options['record_mode'] = 'none' Alternatively, since the object returned is really just an object and does not do anything special as a context manager, you could just as easily do .. code-block:: python from betamax import Betamax config = Betamax.configure() config.cassette_library_dir = 'examples/cassettes' config.default_cassette_options['record_mode'] = 'none' We'll now move on to specific use-cases when configuring Betamax. We'll exclude the portion of each example where we create a :class:`~betamax.configure.Configuration` instance. Setting the Directory in which Betamax Should Store Cassette Files `````````````````````````````````````````````````````````````````` Each and every time we use Betamax we need to tell it where to store (and discover) cassette files. By default we do this by setting the ``cassette_library_dir`` attribute on our ``config`` object, e.g., .. code-block:: python config.cassette_library_dir = 'tests/integration/cassettes' Note that these paths are relative to what Python thinks is the current working directory. Wherever you run your tests from, write the path to be relative to that directory. Setting Default Cassette Options ```````````````````````````````` Cassettes have default options used by Betamax if none are set. For example, - The default record mode is ``once``. - The default matchers used are ``method`` and ``uri``. - Cassettes do **not** preserve the exact body bytes by default. These can all be configured as you please. For example, if you want to change the default matchers and preserve exact body bytes, you would do .. code-block:: python config.default_cassette_options['match_requests_on'] = [ 'method', 'uri', 'headers', ] config.preserve_exact_body_bytes = True Filtering Sensitive Data ```````````````````````` It's unlikely that you'll want to record an interaction that will not require authentication. For this we can define placeholders in our cassettes. Let's use a very real example. Let's say that you want to get your user data from GitHub using Requests. You might have code that looks like this: .. code-block:: python def me(username, password, session): r = session.get('https://api.github.com/user', auth=(username, password)) r.raise_for_status() return r.json() You would test this something like: .. code-block:: python import os import betamax import requests from my_module import me session = requests.Session() recorder = betamax.Betamax(session) username = os.environ.get('USERNAME', 'testuser') password = os.environ.get('PASSWORD', 'testpassword') with recorder.use_cassette('test-me'): json = me(username, password, session) # assertions about the JSON returned The problem is that now your username and password will be recorded in the cassette which you don't then want to push to your version control. How can we prevent that from happening? .. code-block:: python import base64 username = os.environ.get('USERNAME', 'testuser') password = os.environ.get('PASSWORD', 'testpassword') config.define_cassette_placeholder( '', base64.b64encode( '{0}:{1}'.format(username, password).encode('utf-8') ) ) .. note:: Obviously you can refactor this a bit so you can pull those environment variables out in only one place, but I'd rather be clear than not here. The first time you run the test script you would invoke your tests like so: .. code-block:: sh $ USERNAME='my-real-username' PASSWORD='supersecretep@55w0rd' \ python test_script.py Future runs of the script could simply be run without those environment variables, e.g., .. code-block:: sh $ python test_script.py This means that you can run these tests on a service like Travis-CI without providing credentials. In the event that you can not anticipate what you will need to filter out, version 0.7.0 of Betamax adds ``before_record`` and ``before_playback`` hooks. These two hooks both will pass the :class:`~betamax.cassette.interaction.Interaction` and :class:`~betamax.cassette.cassette.Cassette` to the function provided. An example callback would look like: .. code-block:: python def hook(interaction, cassette): pass You would then register this callback: .. code-block:: python # Either config.before_record(callback=hook) # Or config.before_playback(callback=hook) You can register callables for both hooks. If you wish to ignore an interaction and prevent it from being recorded or replayed, you can call the :meth:`~betamax.cassette.interaction.Interaction.ignore`. You also have full access to all of the methods and attributes on an instance of an Interaction. This will allow you to inspect the response produced by the interaction and then modify it. Let's say, for example, that you are talking to an API that grants authorization tokens on a specific request. In this example, you might authenticate initially using a username and password and then use a token after authenticating. You want, however, for the token to be kept secret. In that case you might configure Betamax to replace the username and password, e.g., .. code-block:: python config.define_cassette_placeholder('', username) config.define_cassette_placeholder('', password) And you would also write a function that, prior to recording, finds the token, saves it, and obscures it from the recorded version of the cassette: .. code-block:: python from betamax.cassette import cassette def sanitize_token(interaction, current_cassette): # Exit early if the request did not return 200 OK because that's the # only time we want to look for Authorization-Token headers if interaction.data['response']['status']['code'] != 200: return headers = interaction.data['response']['headers'] token = headers.get('Authorization-Token') # If there was no token header in the response, exit if token is None: return # Otherwise, create a new placeholder so that when cassette is saved, # Betamax will replace the token with our placeholder. current_cassette.placeholders.append( cassette.Placeholder(placeholder='', replace=token) ) This will dynamically create a placeholder for that cassette only. Once we have our hook, we need merely register it like so: .. code-block:: python config.before_record(callback=sanitize_token) And we no longer need to worry about leaking sensitive data. Setting default serializer `````````````````````````` If you want to use a specific serializer for every cassette, you can set ``serialize_with`` as a default cassette option. For example, if you wanted to use the ``prettyjson`` serializer for every cassette you would do: .. code-block:: python config.default_cassette_options['serialize_with'] = 'prettyjson' Per-Use Configuration --------------------- Each time you create a :class:`~betamax.Betamax` instance or use :meth:`~betamax.Betamax.use_cassette`, you can pass some of the options from above. Setting the Directory in which Betamax Should Store Cassette Files `````````````````````````````````````````````````````````````````` When using per-use configuration of Betamax, you can specify the cassette directory when you instantiate a :class:`~betamax.Betamax` object: .. code-block:: python session = requests.Session() recorder = betamax.Betamax(session, cassette_library_dir='tests/cassettes/') Setting Default Cassette Options ```````````````````````````````` You can also set default cassette options when instantiating a :class:`~betamax.Betamax` object: .. code-block:: python session = requests.Session() recorder = betamax.Betamax(session, default_cassette_options={ 'record_mode': 'once', 'match_requests_on': ['method', 'uri', 'headers'], 'preserve_exact_body_bytes': True }) You can also set the above when calling :meth:`~betamax.Betamax.use_cassette`: .. code-block:: python session = requests.Session() recorder = betamax.Betamax(session) with recorder.use_cassette('cassette-name', preserve_exact_body_bytes=True, match_requests_on=['method', 'uri', 'headers'], record='once'): session.get('https://httpbin.org/get') Filtering Sensitive Data ```````````````````````` Filtering sensitive data on a per-usage basis is the only difficult (or perhaps, less convenient) case. Cassette placeholders are part of the default cassette options, so we'll set this value similarly to how we set the other default cassette options, the catch is that placeholders have a specific structure. Placeholders are stored as a list of dictionaries. Let's use our example above and convert it. .. code-block:: python import base64 username = os.environ.get('USERNAME', 'testuser') password = os.environ.get('PASSWORD', 'testpassword') session = requests.Session() recorder = betamax.Betamax(session, default_cassette_options={ 'placeholders': [{ 'placeholder': '', 'replace': base64.b64encode( '{0}:{1}'.format(username, password).encode('utf-8') ), }] }) Note that what we passed as our first argument is assigned to the ``'placeholder'`` key while the value we're replacing is assigned to the ``'replace'`` key. This isn't the typical way that people filter sensitive data because they tend to want to do it globally. Mixing and Matching ------------------- It's not uncommon to mix and match configuration methodologies. I do this in `github3.py`_. I use global configuration to filter sensitive data and set defaults based on the environment the tests are running in. On Travis-CI, the record mode is set to ``'none'``. I also set how we match requests and when we preserve exact body bytes on a per-use basis. .. links .. _VCR: https://relishapp.com/vcr/vcr .. _github3.py: https://github.com/sigmavirus24/github3.py betamax-0.8.1/docs/long_term_usage.rst0000664000175000017500000001047713040145704021406 0ustar icordascicordasc00000000000000Long Term Usage Patterns ======================== Now that we've covered the basics in :ref:`getting_started`, let's look at some patterns and problems we might encounter when using Betamax over a period of months instead of minutes. Adding New Requests to a Cassette --------------------------------- Let's reuse an example. Specifically let's reuse our :file:`examples/more_complicated_cassettes.py` example. .. literalinclude:: ../examples/more_complicated_cassettes.py :language: python Let's add a new ``POST`` request in there: .. code-block:: python session.post('https://httpbin.org/post', params={'id': '20'}, json={'some-other-attribute': 'some-other-value'}) If we run this cassette now, we should expect to see that there was an exception because Betamax couldn't find a matching request for it. We expect this because the post requests have two completely different bodies, right? Right. The problem you'll find is that by default Betamax **only** matches on the URI and the Method. So Betamax will find a matching request/response pair for ``("POST", "https://httpbin.org/post?id=20")`` and reuse it. So now we need to update how we use Betamax so it will match using the ``body`` as well: .. literalinclude:: ../examples/more_complicated_cassettes_2.py :language: python Now when we run that we should see something like this: .. literalinclude:: ../examples/more_complicated_cassettes_2.traceback :language: pytb This is what we do expect to see. So, how do we fix it? We have a few options to fix it. Option 1: Re-recording the Cassette ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ One of the easiest ways to fix this situation is to simply remove the cassette that was recorded and run the script again. This will recreate the cassette and subsequent runs will work just fine. To be clear, we're advocating for this option that the user do: .. code:: $ rm examples/cassettes/{{ cassette-name }} This is the favorable option if you don't foresee yourself needing to add new interactions often. Option 2: Changing the Record Mode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A different way would be to update the recording mode used by Betamax. We would update the line in our file that currently reads: .. code-block:: python with recorder.use_cassette('more-complicated-cassettes', serialize_with='prettyjson', match_requests_on=matchers): to add one more parameter to the call to :meth:`~betamax.Betamax.use_cassette`. We want to use the ``record`` parameter to tell Betamax to use either the ``new_episodes`` or ``all`` modes. Which you choose depends on your use case. ``new_episodes`` will only record new request/response interactions that Betamax sees. ``all`` will just re-record every interaction every time. In our example, we'll use ``new_episodes`` so our code now looks like: .. code-block:: python with recorder.use_cassette('more-complicated-cassettes', serialize_with='prettyjson', match_requests_on=matchers, record='new_episodes'): Known Issues ------------ Tests Periodically Slow Down ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Description:** Requests checks if it should use or bypass proxies using the standard library function ``proxy_bypass``. This has been known to cause slow downs when using Requests and can cause your recorded requests to slow down as well. Betamax presently has no way to prevent this from being called as it operates at a lower level in Requests than is necessary. **Workarounds:** - Mock gethostbyname method from socket library, to force a localhost setting, e.g., .. code-block:: python import socket socket.gethostbyname = lambda x: '127.0.0.1' - Set ``trust_env`` to ``False`` on the session used with Betamax. This will prevent Requests from checking for proxies and whether it needs bypass them. **Related bugs:** - https://github.com/sigmavirus24/betamax/issues/96 - https://github.com/kennethreitz/requests/issues/2988 .. Template for known issues Descriptive Title ~~~~~~~~~~~~~~~~~ **Description:** **Workaround(s):** - List - of - workarounds **Related bug(s):** - List - of - bug - links betamax-0.8.1/docs/usage_patterns.rst0000664000175000017500000000773613040145704021264 0ustar icordascicordasc00000000000000Usage Patterns ============== Below are suggested patterns for using Betamax efficiently. Configuring Betamax in py.test's conftest.py -------------------------------------------- Betamax and github3.py (the project which instigated the creation of Betamax) both utilize py.test_ and its feature of configuring how the tests run with ``conftest.py`` [#]_. One pattern that I have found useful is to include this in your ``conftest.py`` file: .. code-block:: python import betamax with betamax.Betamax.configure() as config: config.cassette_library_dir = 'tests/cassettes/' This configures your cassette directory for all of your tests. If you do not check your cassettes into your version control system, then you can also add: .. code-block:: python import os if not os.path.exists('tests/cassettes'): os.makedirs('tests/cassettes') An Example from github3.py ^^^^^^^^^^^^^^^^^^^^^^^^^^ You can configure other aspects of Betamax via the ``conftest.py`` file. For example, in github3.py, I do the following: .. code-block:: python import os record_mode = 'none' if os.environ.get('TRAVIS_GH3') else 'once' with betamax.Betamax.configure() as config: config.cassette_library_dir = 'tests/cassettes/' config.default_cassette_options['record_mode'] = record_mode config.define_cassette_placeholder( '', os.environ.get('GH_AUTH', 'x' * 20) ) In essence, if the tests are being run on `Travis CI`_, then we want to make sure to not try to record new cassettes or interactions. We also, want to ensure we're authenticated when possible but that we do not leave our placeholder in the cassettes when they're replayed. Using Human Readable JSON Cassettes ----------------------------------- Using the ``PrettyJSONSerializer`` provided by the ``betamax_serializers`` package provides human readable JSON cassettes. Cassettes output in this way make it easy to compare modifications to cassettes to ensure only expected changes are introduced. While you can use the ``serialize_with`` option when creating each individual cassette, it is simpler to provide this setting globally. The following example demonstrates how to configure Betamax to use the ``PrettyJSONSerializer`` for all newly created cassettes: .. code-block:: python from betamax_serializers import pretty_json betamax.Betamax.register_serializer(pretty_json.PrettyJSONSerializer) # ... config.default_cassette_options['serialize_with'] = 'prettyjson' Updating Existing Betamax Cassettes to be Human Readable ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you already have a library of cassettes when applying the previous configuration update, then you will probably want to also update all your existing cassettes into the new human readable format. The following script will help you transform your existing cassettes: .. code-block:: python import os import glob import json import sys try: cassette_dir = sys.argv[1] cassettes = glob.glob(os.path.join(cassette_dir, '*.json')) except: print('Usage: {0} CASSETTE_DIRECTORY'.format(sys.argv[0])) sys.exit(1) for cassette_path in cassettes: with open(cassette_path, 'r') as fp: data = json.load(fp) with open(cassette_path, 'w') as fp: json.dump(data, fp, sort_keys=True, indent=2, separators=(',', ': ')) print('Updated {0} cassette{1}.'.format( len(cassettes), '' if len(cassettes) == 1 else 's')) Copy and save the above script as ``fix_cassettes.py`` and then run it like: .. code-block:: bash python fix_cassettes.py PATH_TO_CASSETTE_DIRECTORY If you're not already using a version control system (e.g., git, svn) then it is recommended you make a backup of your cassettes first in the event something goes wrong. .. [#] http://pytest.org/latest/plugins.html .. _py.test: http://pytest.org/latest/ .. _Travis CI: https://travis-ci.org/ betamax-0.8.1/docs/record_modes.rst0000664000175000017500000000755213040145704020701 0ustar icordascicordasc00000000000000Record Modes ============ Betamax, like `VCR`_, has four modes that it can use to record cassettes: - ``'all'`` - ``'new_episodes'`` - ``'none'`` - ``'once'`` You can only ever use one record mode. Below are explanations and examples of each record mode. The explanations are blatantly taken from VCR's own `Record Modes documentation`_. All --- The ``'all'`` record mode will: - Record new interactions. - Never replay previously recorded interactions. This can be temporarily used to force VCR to re-record a cassette (i.e., to ensure the responses are not out of date) or can be used when you simply want to log all HTTP requests. Given our file, ``examples/record_modes/all/example.py``, .. literalinclude:: ../examples/record_modes/all/example.py :language: python Every time we run it, our cassette (``examples/record_modes/all/all-example.json``) will be updated with new values. New Episodes ------------ The ``'new_episodes'`` record mode will: - Record new interactions. - Replay previously recorded interactions. It is similar to the ``'once'`` record mode, but will always record new interactions, even if you have an existing recorded one that is similar (but not identical, based on the :match_request_on option). Given our file, ``examples/record_modes/new_episodes/example_original.py``, with which we have already recorded ``examples/record_modes/new_episodes/new-episodes-example.json`` .. literalinclude:: ../examples/record_modes/new_episodes/example_original.py :language: python If we then run ``examples/record_modes/new_episodes/example_updated.py`` .. literalinclude:: ../examples/record_modes/new_episodes/example_updated.py :language: python The new request at the end of the file will be added to the cassette without updating the other interactions that were already recorded. None ---- The ``'none'`` record mode will: - Replay previously recorded interactions. - Cause an error to be raised for any new requests. This is useful when your code makes potentially dangerous HTTP requests. The ``'none'`` record mode guarantees that no new HTTP requests will be made. Given our file, ``examples/record_modes/none/example_original.py``, with a cassette that already has interactions recorded in ``examples/record_modes/none/none-example.json`` .. literalinclude:: ../examples/record_modes/none/example_original.py :language: python If we then run ``examples/record_modes/none/example_updated.py`` .. literalinclude:: ../examples/record_modes/none/example_updated.py :language: python We'll see an exception indicating that new interactions were prevented: .. literalinclude:: ../examples/record_modes/none/example_updated.traceback :language: pytb Once ---- The ``'once'`` record mode will: - Replay previously recorded interactions. - Record new interactions if there is no cassette file. - Cause an error to be raised for new requests if there is a cassette file. It is similar to the ``'new_episodes'`` record mode, but will prevent new, unexpected requests from being made (i.e. because the request URI changed or whatever). ``'once'`` is the default record mode, used when you do not set one. If we have a file, ``examples/record_modes/once/example_original.py``, .. literalinclude:: ../examples/record_modes/once/example_original.py :language: python And we run it, we'll see a cassette named ``examples/record_modes/once/once-example.json`` has been created. If we then run ``examples/record_modes/once/example_updated.py``, .. literalinclude:: ../examples/record_modes/once/example_updated.py :language: python We'll see an exception similar to the one we see when using the ``'none'`` record mode. .. literalinclude:: ../examples/record_modes/once/example_updated.traceback :language: pytb .. _VCR: https://relishapp.com/vcr/vcr .. _Record Modes documentation: https://relishapp.com/vcr/vcr/v/2-9-3/docs/record-modes/ betamax-0.8.1/docs/integrations.rst0000664000175000017500000001062513040145704020735 0ustar icordascicordasc00000000000000Integrating Betamax with Test Frameworks ======================================== It's nice to have a way to integrate libraries you use for testing into your testing frameworks. Having considered this, the authors of and contributors to Betamax have included integrations in the package. Betamax comes with integrations for py.test and unittest. (If you need an integration for another framework, please suggest it and send a patch!) PyTest Integration ------------------ .. versionadded:: 0.5.0 .. versionchanged:: 0.6.0 When you install Betamax, it now installs two `py.test`_ fixtures by default. To use it in your tests you need only follow the `instructions`_ on pytest's documentation. To use the ``betamax_session`` fixture for an entire class of tests you would do: .. code-block:: python # tests/test_http_integration.py import pytest @pytest.mark.usefixtures('betamax_session') class TestMyHttpClient: def test_get(self, betamax_session): betamax_session.get('https://httpbin.org/get') This will generate a cassette name for you, e.g., ``tests.test_http_integration.TestMyHttpClient.test_get``. After running this test you would have a cassette file stored in your cassette library directory named ``tests.test_http_integration.TestMyHttpClient.test_get.json``. To use this fixture at the module level, you need only do .. code-block:: python # tests/test_http_integration.py import pytest pytest.mark.usefixtures('betamax_session') class TestMyHttpClient: def test_get(self, betamax_session): betamax_session.get('https://httpbin.org/get') class TestMyOtherHttpClient: def test_post(self, betamax_session): betamax_session.post('https://httpbin.org/post') If you need to customize the recorder object, however, you can instead use the ``betamax_recorder`` fixture: .. code-block:: python # tests/test_http_integration.py import pytest pytest.mark.usefixtures('betamax_recorder') class TestMyHttpClient: def test_post(self, betamax_recorder): betamax_recorder.current_cassette.match_options.add('json-body') session = betamax_recorder.session session.post('https://httpbin.org/post', json={'foo': 'bar'}) Unittest Integration -------------------- .. versionadded:: 0.5.0 When writing tests with unittest, a common pattern is to either import :class:`unittest.TestCase` or subclass that and use that subclass in your tests. When integrating Betamax with your unittest testsuite, you should do the following: .. code-block:: python from betamax.fixtures import unittest class IntegrationTestCase(unittest.BetamaxTestCase): # Add the rest of the helper methods you want for your # integration tests class SpecificTestCase(IntegrationTestCase): def test_something(self): # Test something The unittest integration provides the following attributes on the test case instance: - ``session`` the instance of ``BetamaxTestCase.SESSION_CLASS`` created for that test. - ``recorder`` the instance of :class:`betamax.Betamax` created. The integration also generates a cassette name from the test case class name and test method. So the cassette generated for the above example would be named ``SpecificTestCase.test_something``. To override that behaviour, you need to override the :meth:`~betamax.fixtures.BetamaxTestCase.generate_cassette_name` method in your subclass. If you are subclassing :class:`requests.Session` in your application, then it follows that you will want to use that in your tests. To facilitate this, you can set the ``SESSION_CLASS`` attribute. To give a fuller example, let's say you're changing the default cassette name and you're providing your own session class, your code might look like: .. code-block:: python from betamax.fixtures import unittest from myapi import session class IntegrationTestCase(unittest.BetamaxTestCase): # Add the rest of the helper methods you want for your # integration tests SESSION_CLASS = session.MyApiSession def generate_cassette_name(self): classname = self.__class__.__name__ method = self._testMethodName return 'integration_{0}_{1}'.format(classname, method) .. _py.test: http://pytest.org/latest/ .. _instructions: http://pytest.org/latest/fixture.html#using-fixtures-from-classes-modules-or-projects betamax-0.8.1/docs/matchers.rst0000664000175000017500000000613513040145704020036 0ustar icordascicordasc00000000000000Matchers ======== You can specify how you would like Betamax to match requests you are making with the recorded requests. You have the following options for default (built-in) matchers: ======= ========= Matcher Behaviour ======= ========= body This matches by checking the equality of the request bodies. headers This matches by checking the equality of all of the request headers host This matches based on the host of the URI method This matches based on the method, e.g., ``GET``, ``POST``, etc. path This matches on the path of the URI query This matches on the query part of the URI uri This matches on the entirety of the URI ======= ========= Default Matchers ---------------- By default, Betamax matches on ``uri`` and ``method``. Specifying Matchers ------------------- You can specify the matchers to be used in the entire library by configuring Betamax like so: .. code-block:: python import betamax with betamax.Betamax.configure() as config: config.default_cassette_options['match_requests_on'].extend([ 'headers', 'body' ]) Instead of configuring global state, though, you can set it per cassette. For example: .. code-block:: python import betamax import requests session = requests.Session() recorder = betamax.Betamax(session) match_on = ['uri', 'method', 'headers', 'body'] with recorder.use_cassette('example', match_requests_on=match_on): # ... Making Your Own Matcher ----------------------- So long as you are matching requests, you can define your own way of matching. Each request matcher has to inherit from ``betamax.BaseMatcher`` and implement ``match``. .. autoclass:: betamax.BaseMatcher :members: Some examples of matchers are in the source reproduced here: .. literalinclude:: ../betamax/matchers/headers.py :language: python .. literalinclude:: ../betamax/matchers/host.py :language: python .. literalinclude:: ../betamax/matchers/method.py :language: python .. literalinclude:: ../betamax/matchers/path.py :language: python .. literalinclude:: ../betamax/matchers/path.py :language: python .. literalinclude:: ../betamax/matchers/uri.py :language: python When you have finished writing your own matcher, you can instruct betamax to use it like so: .. code-block:: python import betamax class MyMatcher(betamax.BaseMatcher): name = 'my' def match(self, request, recorded_request): return True betamax.Betamax.register_request_matcher(MyMatcher) To use it, you simply use the name you set like you use the name of the default matchers, e.g.: .. code-block:: python with Betamax(s).use_cassette('example', match_requests_on=['uri', 'my']): # ... ``on_init`` ~~~~~~~~~~~ As you can see in the code for ``URIMatcher``, we use ``on_init`` to initialize an attribute on the ``URIMatcher`` instance. This method serves to provide the matcher author with a different way of initializing the object outside of the ``match`` method. This also means that the author does not have to override the base class' ``__init__`` method. betamax-0.8.1/docs/Makefile0000664000175000017500000001076613040145704017143 0ustar icordascicordasc00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Raclette.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Raclette.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Raclette" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Raclette" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." betamax-0.8.1/docs/api.rst0000664000175000017500000001111013040145704016766 0ustar icordascicordasc00000000000000API === .. module:: betamax .. autoclass:: Betamax :members: .. autofunction:: betamax.decorator.use_cassette .. autoclass:: betamax.configure.Configuration :members: .. automodule:: betamax.fixtures.pytest .. automodule:: betamax.fixtures.unittest Examples -------- Basic Usage ^^^^^^^^^^^ Let `example.json` be a file in a directory called `cassettes` with the content: .. code-block:: javascript { "http_interactions": [ { "request": { "body": { "string": "", "encoding": "utf-8" }, "headers": { "User-Agent": ["python-requests/v1.2.3"] }, "method": "GET", "uri": "https://httpbin.org/get" }, "response": { "body": { "string": "example body", "encoding": "utf-8" }, "headers": {}, "status": { "code": 200, "message": "OK" }, "url": "https://httpbin.org/get" } } ], "recorded_with": "betamax" } The following snippet will not raise any exceptions .. code-block:: python from betamax import Betamax from requests import Session s = Session() with Betamax(s, cassette_library_dir='cassettes') as betamax: betamax.use_cassette('example', record='none') r = s.get("https://httpbin.org/get") On the other hand, this will raise an exception: .. code-block:: python from betamax import Betamax from requests import Session s = Session() with Betamax(s, cassette_library_dir='cassettes') as betamax: betamax.use_cassette('example', record='none') r = s.post("https://httpbin.org/post", data={"key": "value"}) Finally, we can also use a decorator in order to simplify things: .. code-block:: python import unittest from betamax.decorator import use_cassette class TestExample(unittest.TestCase): @use_cassette('example', cassette_library_dir='cassettes') def test_example(self, session): session.get('https://httpbin.org/get') # Or if you're using something like py.test @use_cassette('example', cassette_library_dir='cassettes') def test_example_pytest(session): session.get('https://httpbin.org/get') .. _opinions: Opinions at Work ---------------- If you use ``requests``'s default ``Accept-Encoding`` header, servers that support gzip content encoding will return responses that Betamax cannot serialize in a human-readable format. In this event, the cassette will look like this: .. code-block:: javascript :emphasize-lines: 17 { "http_interactions": [ { "request": { "body": { "base64_string": "", "encoding": "utf-8" }, "headers": { "User-Agent": ["python-requests/v1.2.3"] }, "method": "GET", "uri": "https://httpbin.org/get" }, "response": { "body": { "base64_string": "Zm9vIGJhcgo=", "encoding": "utf-8" }, "headers": { "Content-Encoding": ["gzip"] }, "status": { "code": 200, "message": "OK" }, "url": "https://httpbin.org/get" } } ], "recorded_with": "betamax" } Forcing bytes to be preserved ----------------------------- You may want to force betamax to preserve the exact bytes in the body of a response (or request) instead of relying on the :ref:`opinions held by the library `. In this case you have two ways of telling betamax to do this. The first, is on a per-cassette basis, like so: .. code-block:: python from betamax import Betamax import requests session = Session() with Betamax.configure() as config: c.cassette_library_dir = '.' with Betamax(session).use_cassette('some_cassette', preserve_exact_body_bytes=True): r = session.get('http://example.com') On the other hand, you may want to the preserve exact body bytes for all cassettes. In this case, you can do: .. code-block:: python from betamax import Betamax import requests session = Session() with Betamax.configure() as config: c.cassette_library_dir = '.' c.preserve_exact_body_bytes = True with Betamax(session).use_cassette('some_cassette'): r = session.get('http://example.com') betamax-0.8.1/docs/index.rst0000664000175000017500000000100713040145704017330 0ustar icordascicordasc00000000000000.. include:: ../README.rst Contents of Betamax's Documentation =================================== .. toctree:: :caption: Narrative Documentation :maxdepth: 3 introduction long_term_usage configuring record_modes third_party_packages usage_patterns integrations .. toctree:: :caption: API Documentation :maxdepth: 2 api cassettes implementation_details matchers serializers Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` betamax-0.8.1/docs/introduction.rst0000664000175000017500000001005213040145704020742 0ustar icordascicordasc00000000000000.. _getting_started: Getting Started =============== The first step is to make sure Betamax is right for you. Let's start by answering the following questions - Are you using `Requests`_? If you're not using Requests, Betamax is not for you. You should checkout `VCRpy`_. - Are you using Sessions or are you using the functional API (e.g., ``requests.get``)? If you're using the functional API, and aren't willing to use Sessions, Betamax is not *yet* for you. So if you're using Requests and you're using Sessions, you're in the right place. Betamax officially supports `py.test`_ and `unittest`_ but it should integrate well with nose as well. Installation ------------ .. code-block:: bash $ pip install betamax Configuration ------------- When starting with Betamax, you need to tell it where to store the cassettes that it creates. There's two ways to do this: 1. If you're using :class:`~betamax.recorder.Betamax` or :class:`~betamax.decorator.use_cassette` you can pass the ``cassette_library_dir`` option. For example, .. code-block:: python import betamax import requests session = requests.Session() recorder = betamax.Betamax(session, cassette_library_dir='cassettes') with recorder.use_cassette('introduction'): # ... 2. You can do it once, globally, for your test suite. .. code-block:: python import betamax with betamax.Betamax.configure() as config: config.cassette_library_dir = 'cassettes' .. note:: If you don't set a cassette directory, Betamax won't save cassettes to disk There are other configuration options that *can* be provided, but this is the only one that is *required*. Recording Your First Cassette ----------------------------- Let's make a file named ``our_first_recorded_session.py``. Let's add the following to our file: .. literalinclude:: ../examples/our_first_recorded_session.py :language: python If we then run our script, we'll see that a new file is created in our specified cassette directory. It should look something like: .. literalinclude:: ../examples/cassettes/our-first-recorded-session.json :language: javascript Now, each subsequent time that we run that script, we will use the recorded interaction instead of talking to the internet over and over again. Recording More Complex Cassettes -------------------------------- Most times we cannot isolate our tests to a single request at a time, so we'll have cassettes that make multiple requests. Betamax can handle these with ease, let's take a look at an example. .. literalinclude:: ../examples/more_complicated_cassettes.py :language: python Before we run this example, we have to install a new package: ``betamax-serializers``, e.g., ``pip install betamax-serializers``. If we now run our new example, we'll see a new file appear in our :file:`examples/cassettes/` directory named :file:`more-complicated-cassettes.json`. This cassette will be much larger as a result of making 3 requests and receiving 3 responses. You'll also notice that we imported :mod:`betamax_serializers.pretty_json` and called :meth:`~betamax.Betamax.register_serializer` with :class:`~betamax_serializers.pretty_json.PrettyJSONSerializer`. Then we added a keyword argument to our invocation of :meth:`~betamax.Betamax.use_cassette`, ``serialize_with='prettyjson'``. :class:`~betamax_serializers.pretty_json.PrettyJSONSerializer` is a class provided by the ``betamax-serializers`` package on PyPI that can serialize and deserialize cassette data into JSON while allowing it to be easily human readable and pretty. Let's see the results: .. literalinclude:: ../examples/cassettes/more-complicated-cassettes.json :language: javascript This makes the cassette easy to read and helps us recognize that requests and responses are paired together. We'll explore cassettes more a bit later. .. links .. _Requests: http://docs.python-requests.org/ .. _VCRpy: https://github.com/kevin1024/vcrpy .. _py.test: http://pytest.org/ .. _unittest: https://docs.python.org/3/library/unittest.html betamax-0.8.1/docs/cassettes.rst0000664000175000017500000000675713040145704020240 0ustar icordascicordasc00000000000000What is a cassette? =================== A cassette is a set of recorded interactions serialized to a specific format. Currently the only supported format is JSON_. A cassette has a list (or array) of interactions and information about the library that recorded it. This means that the cassette's structure (using JSON) is .. code:: javascript { "http_interactions": [ // ... ], "recorded_with": "betamax" } Each interaction is the object representing the request and response as well as the date it was recorded. The structure of an interaction is .. code:: javascript { "request": { // ... }, "response": { // ... }, "recorded_at": "2013-09-28T01:25:38" } Each request has the body, method, uri, and an object representing the headers. A serialized request looks like: .. code:: javascript { "body": { "string": "...", "encoding": "utf-8" }, "method": "GET", "uri": "http://example.com", "headers": { // ... } } A serialized response has the status_code, url, and objects representing the headers and the body. A serialized response looks like: .. code:: javascript { "body": { "encoding": "utf-8", "string": "..." }, "url": "http://example.com", "status": { "code": 200, "message": "OK" }, "headers": { // ... } } If you put everything together, you get: .. _cassette-dict: .. code:: javascript { "http_interactions": [ { "request": { { "body": { "string": "...", "encoding": "utf-8" }, "method": "GET", "uri": "http://example.com", "headers": { // ... } } }, "response": { { "body": { "encoding": "utf-8", "string": "..." }, "url": "http://example.com", "status": { "code": 200, "message": "OK" }, "headers": { // ... } } }, "recorded_at": "2013-09-28T01:25:38" } ], "recorded_with": "betamax" } If you were to pretty-print a cassette, this is vaguely what you would see. Keep in mind that since Python does not keep dictionaries ordered, the items may not be in the same order as this example. .. note:: **Pro-tip** You can pretty print a cassette like so: ``python -m json.tool cassette.json``. What is a cassette library? =========================== When configuring Betamax, you can choose your own cassette library directory. This is the directory available from the current directory in which you want to store your cassettes. For example, let's say that you set your cassette library to be ``tests/cassettes/``. In that case, when you record a cassette, it will be saved there. To continue the example, let's say you use the following code: .. code:: python from requests import Session from betamax import Betamax s = Session() with Betamax(s, cassette_library_dir='tests/cassettes').use_cassette('example'): r = s.get('https://httpbin.org/get') You would then have the following directory structure:: . `-- tests `-- cassettes `-- example.json .. _JSON: http://json.org betamax-0.8.1/MANIFEST.in0000664000175000017500000000027013040145704016276 0ustar icordascicordasc00000000000000include README.rst include LICENSE include HISTORY.rst include AUTHORS.rst recursive-include docs Makefile *.py *.rst recursive-include tests *.json *.py prune *.pyc prune docs/_build betamax-0.8.1/setup.cfg0000664000175000017500000000007513252047671016375 0ustar icordascicordasc00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0