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"
}