docs: vcr only set up for markdown (#3339)

- PR branch for testing w/ typescript (@benjamincburns )
- implementation needs better cache invalidation for the cassettes
(@eyurtsev)

---------

Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
This commit is contained in:
Eugene Yurtsev
2025-02-11 15:38:17 -05:00
committed by GitHub
co-authored by Ben Burns
parent 29c317887d
commit bce4545021
16 changed files with 3830 additions and 2669 deletions
@@ -0,0 +1,75 @@
import nock, { Definition } from "nock";
import msgpack from "msgpack-lite";
import zlib from "node:zlib";
import fs from "node:fs/promises";
import { Buffer } from "node:buffer";
// deno style imports here because we're running this in the deno jupyter kernel
interface NockCassetteData {
hash: string;
entries: Definition[];
}
// Utility functions for compression & serialization
function compressData(data: NockCassetteData, compressionLevel = 9): string {
const packed = msgpack.encode(data);
const compressed = zlib.deflateSync(packed, { level: compressionLevel });
return compressed.toString("base64");
}
function decompressData(compressedString: string): NockCassetteData {
const decoded = Buffer.from(compressedString, "base64");
const decompressed = zlib.inflateSync(decoded);
return msgpack.decode(decompressed) as NockCassetteData;
}
// deno-lint-ignore no-unused-vars
class HashedCassette {
private recording = true;
constructor(
private readonly cassettePath: string,
private readonly hash: string
) {}
async enter() {
try {
const rawCassette = await fs.readFile(this.cassettePath, "utf-8");
const data = decompressData(rawCassette);
if (data.hash === this.hash) {
this.recording = false;
nock.disableNetConnect();
nock.define(data.entries);
return;
}
} catch (error) {
if (error instanceof Error && error.message.includes("ENOENT")) {
this.recording = true;
} else {
throw error;
}
}
nock.recorder.rec({
dont_print: true,
output_objects: true,
});
}
async exit() {
if (this.recording) {
const entries = nock.recorder.play() as Definition[];
const data = {
hash: this.hash,
entries,
};
const compressed = compressData(data);
await fs.writeFile(this.cassettePath, compressed);
} else {
nock.enableNetConnect();
nock.restore();
nock.cleanAll();
}
}
}
+108
View File
@@ -0,0 +1,108 @@
import base64
import os
import zlib
from logging import getLogger
from types import TracebackType
from typing import Optional, Any, Type
import msgpack
import vcr
logger = getLogger(__name__)
os.environ.pop("LANGCHAIN_TRACING_V2", None)
custom_vcr = vcr.VCR()
def compress_data(data: Any, compression_level: int = 9) -> str:
packed = msgpack.packb(data, use_bin_type=True)
compressed = zlib.compress(packed, level=compression_level)
return base64.b64encode(compressed).decode("utf-8")
def decompress_data(compressed_string: str) -> Any:
decoded = base64.b64decode(compressed_string)
decompressed = zlib.decompress(decoded)
return msgpack.unpackb(decompressed, raw=False)
class AdvancedCompressedSerializer:
def serialize(self, cassette_dict: Any) -> str:
return compress_data(cassette_dict)
def deserialize(self, cassette_string: str) -> Any:
return decompress_data(cassette_string)
custom_vcr.register_serializer("advanced_compressed", AdvancedCompressedSerializer())
custom_vcr.serializer = "advanced_compressed"
class HashedCassette:
def __init__(self, cassette_path: str, hash_value: str) -> None:
"""A context manager for using VCR cassettes with an embedded hash value.
Args:
cassette_path (str): The file path of the cassette (independent of hash).
hash_value (str): The expected hash value (e.g. a uuid string).
This class provides a context manager for using VCR cassettes with an embedded hash value.
The hash value is used to ensure that the cassette matches the expected state, and if not,
the cassette is removed or updated with the new hash value.
"""
self.cassette_path: str = cassette_path
self.hash_value: str = hash_value
self.vcr: vcr.VCR = custom_vcr
self.cassette_context: Optional[Any] = None
def __enter__(self) -> Any:
# Get the serializer instance from the VCR instance.
serializer = self.vcr.serializers[self.vcr.serializer]
# If the cassette file exists, check its embedded hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
print(f"Error deserializing cassette, removing file: {e}")
os.remove(self.cassette_path)
else:
existing_hash = cassette_data.get("cassette_hash")
if existing_hash != self.hash_value:
print("Hash mismatch. Removing outdated cassette.")
os.remove(self.cassette_path)
# Now enter the VCR cassette context.
self.cassette_context = custom_vcr.use_cassette(
self.cassette_path,
filter_headers=["x-api-key", "authorization"],
record_mode="once",
serializer="advanced_compressed",
)
return self.cassette_context.__enter__()
def __exit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> Optional[bool]:
# Exit the VCR cassette context.
result = self.cassette_context.__exit__(exc_type, exc_val, exc_tb)
serializer = self.vcr.serializers[self.vcr.serializer]
# If a cassette was recorded (or updated), open and update its hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
logger.error(f"Error deserializing cassette during exit: {e}")
return result
# Update the cassette data with the expected hash.
if cassette_data.get("cassette_hash") != self.hash_value:
cassette_data["cassette_hash"] = self.hash_value
serialized_data = serializer.serialize(cassette_data)
with open(self.cassette_path, "w") as f:
f.write(serialized_data)
return result