parse_cassette.py
parse_cassette.pyBrowse 2 files
854 tokens
3,554 bytes
Token encoding: o200k_base
Snapshot 9e9fdc4
← Back to SKILL.md
1#!/usr/bin/env python32"""Parse VCR cassette files and pretty-print request/response bodies."""3 4import argparse5import json6import re7import sys8from pathlib import Path9 10import yaml11 12 13def truncate_base64(obj: object, max_len: int = 100) -> object:14 """Recursively truncate base64-like strings in nested structures."""15 if isinstance(obj, str):16 if len(obj) > max_len and re.match(r'^[A-Za-z0-9+/=]+$', obj[:100]):17 return f'{obj[:50]}...[truncated {len(obj)} chars]...{obj[-20:]}'18 if obj.startswith('data:') and len(obj) > max_len:19 return f'{obj[:80]}...[truncated {len(obj)} chars]'20 return obj21 elif isinstance(obj, dict):22 return {k: truncate_base64(v, max_len) for k, v in obj.items()}23 elif isinstance(obj, list):24 return [truncate_base64(item, max_len) for item in obj]25 return obj26 27 28def _extract_body(part: dict[str, object]) -> object | None:29 """Extract body from a request/response, trying parsed_body first, then standard VCR body.string."""30 if 'parsed_body' in part:31 return part['parsed_body']32 body = part.get('body')33 if isinstance(body, dict):34 body_str = body.get('string')35 if isinstance(body_str, str) and body_str:36 try:37 return json.loads(body_str)38 except json.JSONDecodeError:39 return body_str40 elif isinstance(body, str) and body:41 try:42 return json.loads(body)43 except json.JSONDecodeError:44 return body45 return None46 47 48def parse_cassette(path: Path, interaction_idx: int | None = None) -> None:49 """Parse and print cassette contents."""50 with open(path) as f:51 data = yaml.safe_load(f)52 53 interactions = data.get('interactions', [])54 if not interactions:55 print('No interactions found in cassette')56 return57 58 indices = [interaction_idx] if interaction_idx is not None else range(len(interactions))59 60 for i in indices:61 if i < 0 or i >= len(interactions):62 print(f'Interaction {i} not found (only {len(interactions)} interactions)')63 continue64 65 interaction = interactions[i]66 req = interaction.get('request', {})67 resp = interaction.get('response', {})68 69 print(f'\n{"="*60}')70 print(f'INTERACTION {i}')71 print('='*60)72 73 print(f'\n--- REQUEST ---')74 print(f'Method: {req.get("method", "N/A")}')75 print(f'URI: {req.get("uri", "N/A")}')76 req_body = _extract_body(req)77 if req_body is not None:78 truncated = truncate_base64(req_body)79 print(f'Body:\n{json.dumps(truncated, indent=2)}')80 81 print(f'\n--- RESPONSE ---')82 status = resp.get('status', {})83 print(f'Status: {status.get("code", "N/A")} {status.get("message", "")}')84 resp_body = _extract_body(resp)85 if resp_body is not None:86 truncated = truncate_base64(resp_body)87 print(f'Body:\n{json.dumps(truncated, indent=2)}')88 89 90def main() -> None:91 parser = argparse.ArgumentParser(description='Parse VCR cassette files')92 parser.add_argument('cassette', type=Path, help='Path to cassette YAML file')93 parser.add_argument('--interaction', '-i', type=int, help='Specific interaction index (0-based)')94 args = parser.parse_args()95 96 if not args.cassette.exists():97 print(f'File not found: {args.cassette}', file=sys.stderr)98 sys.exit(1)99 100 parse_cassette(args.cassette, args.interaction)101 102 103if __name__ == '__main__':104 main()105