Coverage for tests/json_body_serializer.py: 82.43%

50 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-28 17:27 +0000

1# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false 

2import json 

3import urllib.parse 

4from typing import TYPE_CHECKING, Any 

5 

6import yaml 

7 

8if TYPE_CHECKING: 

9 from yaml import Dumper, Loader 

10else: 

11 try: 

12 from yaml import CDumper as Dumper, CLoader as Loader 

13 except ImportError: 

14 from yaml import Dumper, Loader 

15 

16FILTERED_HEADER_PREFIXES = ['anthropic-', 'cf-', 'x-'] 

17FILTERED_HEADERS = {'authorization', 'date', 'request-id', 'server', 'user-agent', 'via', 'set-cookie', 'api-key'} 

18 

19 

20class LiteralDumper(Dumper): 

21 """ 

22 A custom dumper that will represent multi-line strings using literal style. 

23 """ 

24 

25 

26def str_presenter(dumper: Dumper, data: str): 

27 """If the string contains newlines, represent it as a literal block.""" 

28 if '\n' in data: 

29 return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') 

30 return dumper.represent_scalar('tag:yaml.org,2002:str', data) 

31 

32 

33# Register the custom presenter on our dumper 

34LiteralDumper.add_representer(str, str_presenter) 

35 

36 

37def deserialize(cassette_string: str): 

38 cassette_dict = yaml.load(cassette_string, Loader=Loader) 

39 for interaction in cassette_dict['interactions']: 

40 for kind, data in interaction.items(): 

41 parsed_body = data.pop('parsed_body', None) 

42 if parsed_body is not None: 

43 dumped_body = json.dumps(parsed_body) 

44 data['body'] = {'string': dumped_body} if kind == 'response' else dumped_body 

45 return cassette_dict 

46 

47 

48def serialize(cassette_dict: Any): 

49 for interaction in cassette_dict['interactions']: 

50 for _kind, data in interaction.items(): 

51 headers: dict[str, list[str]] = data.get('headers', {}) 

52 # make headers lowercase 

53 headers = {k.lower(): v for k, v in headers.items()} 

54 # filter headers by name 

55 headers = {k: v for k, v in headers.items() if k not in FILTERED_HEADERS} 

56 # filter headers by prefix 

57 headers = { 

58 k: v for k, v in headers.items() if not any(k.startswith(prefix) for prefix in FILTERED_HEADER_PREFIXES) 

59 } 

60 # update headers on source object 

61 data['headers'] = headers 

62 

63 content_type = headers.get('content-type', []) 

64 if any(isinstance(header, str) and header.startswith('application/json') for header in content_type): 64 ↛ 76line 64 didn't jump to line 76 because the condition on line 64 was always true

65 # Parse the body as JSON 

66 body: Any = data.get('body', None) 

67 assert body is not None, data 

68 if isinstance(body, dict): 

69 # Responses will have the body under a field called 'string' 

70 body = body.get('string') 

71 if body is not None: 71 ↛ 76line 71 didn't jump to line 76 because the condition on line 71 was always true

72 data['parsed_body'] = json.loads(body) 

73 if 'access_token' in data['parsed_body']: 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true

74 data['parsed_body']['access_token'] = 'scrubbed' 

75 del data['body'] 

76 if content_type == ['application/x-www-form-urlencoded']: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 query_params = urllib.parse.parse_qs(data['body']) 

78 if 'client_secret' in query_params: 

79 query_params['client_secret'] = ['scrubbed'] 

80 data['body'] = urllib.parse.urlencode(query_params) 

81 

82 # Use our custom dumper 

83 return yaml.dump(cassette_dict, Dumper=LiteralDumper, allow_unicode=True, width=120)