Third-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.

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:

  • URLEncodedBodyMatcher, 'form-urlencoded-body'
  • JSONBodyMatcher, 'json-body'
  • 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:

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 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:

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:

{"key": "value", "other-key": "other-value", "yet-another-key":
"yet-another-value"}
{"key": "value", "yet-another-key": "yet-another-value", "other-key":
"other-value"}
{"other-key": "other-value", "yet-another-key": "yet-another-value",
"key": "value"}
{"yet-another-key": "yet-another-value", "key": "value", "other-key":
"other-value"}
{"yet-another-key": "yet-another-value", "other-key": "other-value",
"key": "value"}
{"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 PrettyJSONSerializer that does that for you.

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:

{
  "http_interactions": [
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": ""
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "GET",
        "uri": "https://httpbin.org/get"
      },
      "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.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/get\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "265"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/get"
      }
    },
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": "{\"some-attribute\": \"some-value\"}"
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "Content-Length": [
            "32"
          ],
          "Content-Type": [
            "application/json"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "POST",
        "uri": "https://httpbin.org/post?id=20"
      },
      "response": {
        "body": {
          "encoding": null,
          "string": "{\n  \"args\": {\n    \"id\": \"20\"\n  }, \n  \"data\": \"{\\\"some-attribute\\\": \\\"some-value\\\"}\", \n  \"files\": {}, \n  \"form\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Content-Length\": \"32\", \n    \"Content-Type\": \"application/json\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"json\": {\n    \"some-attribute\": \"some-value\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/post?id=20\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "495"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/post?id=20"
      }
    },
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": ""
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "GET",
        "uri": "https://httpbin.org/get?id=20"
      },
      "response": {
        "body": {
          "encoding": null,
          "string": "{\n  \"args\": {\n    \"id\": \"20\"\n  }, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/get?id=20\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "289"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/get?id=20"
      }
    }
  ],
  "recorded_with": "betamax/0.4.2"
}