""" Migration script for old 'yaml' and 'json' cassettes .. warning:: Backup your cassettes files before migration. It merges and deletes the request obsolete keys (protocol, host, port, path) into new 'uri' key. Usage:: python3 -m vcr.migration PATH The PATH can be path to the directory with cassettes or cassette itself """ import json import os import shutil import sys import tempfile import yaml from . import request from .serialize import serialize from .serializers import jsonserializer, yamlserializer from .stubs.compat import get_httpmessage # Use the libYAML versions if possible try: from yaml import CLoader as Loader except ImportError: from yaml import Loader def preprocess_yaml(cassette): # this is the hack that makes the whole thing work. The old version used # to deserialize to Request objects automatically using pyYaml's !!python # tag system. This made it difficult to deserialize old cassettes on new # versions. So this just strips the tags before deserializing. STRINGS_TO_NUKE = [ "!!python/object:vcr.request.Request", "!!python/object/apply:__builtin__.frozenset", "!!python/object/apply:builtins.frozenset", ] for s in STRINGS_TO_NUKE: cassette = cassette.replace(s, "") return cassette PARTS = ["protocol", "host", "port", "path"] def build_uri(**parts): port = parts["port"] scheme = parts["protocol"] default_port = {"https": 443, "http": 80}[scheme] parts["port"] = f":{port}" if port != default_port else "" return "{protocol}://{host}{port}{path}".format(**parts) def _migrate(data): interactions = [] for item in data: req = item["request"] res = item["response"] uri = {k: req.pop(k) for k in PARTS} req["uri"] = build_uri(**uri) # convert headers to dict of lists headers = req["headers"] for k in headers: headers[k] = [headers[k]] response_headers = {} for k, v in get_httpmessage(b"".join(h.encode("utf-8") for h in res["headers"])).items(): response_headers.setdefault(k, []) response_headers[k].append(v) res["headers"] = response_headers interactions.append({"request": req, "response": res}) return { "requests": [request.Request._from_dict(i["request"]) for i in interactions], "responses": [i["response"] for i in interactions], } def migrate_json(in_fp, out_fp): data = json.load(in_fp) if _already_migrated(data): return False interactions = _migrate(data) out_fp.write(serialize(interactions, jsonserializer)) return True def _list_of_tuples_to_dict(fs): return dict(fs[0]) def _already_migrated(data): try: if data.get("version") == 1: return True except AttributeError: return False def migrate_yml(in_fp, out_fp): data = yaml.load(preprocess_yaml(in_fp.read()), Loader=Loader) if _already_migrated(data): return False for i in range(len(data)): data[i]["request"]["headers"] = _list_of_tuples_to_dict(data[i]["request"]["headers"]) interactions = _migrate(data) out_fp.write(serialize(interactions, yamlserializer)) return True def migrate(file_path, migration_fn): # because we assume that original files can be reverted # we will try to copy the content. (os.rename not needed) with tempfile.TemporaryFile(mode="w+") as out_fp: with open(file_path) as in_fp: if not migration_fn(in_fp, out_fp): return False with open(file_path, "w") as in_fp: out_fp.seek(0) shutil.copyfileobj(out_fp, in_fp) return True def try_migrate(path): if path.endswith(".json"): return migrate(path, migrate_json) elif path.endswith((".yaml", ".yml")): return migrate(path, migrate_yml) return False def main(): if len(sys.argv) != 2: raise SystemExit( "Please provide path to cassettes directory or file. Usage: python3 -m vcr.migration PATH", ) path = sys.argv[1] if not os.path.isabs(path): path = os.path.abspath(path) files = [path] if os.path.isdir(path): files = (os.path.join(root, name) for (root, dirs, files) in os.walk(path) for name in files) for file_path in files: migrated = try_migrate(file_path) status = "OK" if migrated else "FAIL" sys.stderr.write(f"[{status}] {file_path}\n") sys.stderr.write("Done.\n") if __name__ == "__main__": main()