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
« 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
6import yaml
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
16FILTERED_HEADER_PREFIXES = ['anthropic-', 'cf-', 'x-']
17FILTERED_HEADERS = {'authorization', 'date', 'request-id', 'server', 'user-agent', 'via', 'set-cookie', 'api-key'}
20class LiteralDumper(Dumper):
21 """
22 A custom dumper that will represent multi-line strings using literal style.
23 """
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)
33# Register the custom presenter on our dumper
34LiteralDumper.add_representer(str, str_presenter)
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
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
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)
82 # Use our custom dumper
83 return yaml.dump(cassette_dict, Dumper=LiteralDumper, allow_unicode=True, width=120)