Skip to content

Commit 17cab49

Browse files
authored
fix: normalize HTML responses for deterministic Django replay (#58)
1 parent 3a7f613 commit 17cab49

File tree

8 files changed

+847
-147
lines changed

8 files changed

+847
-147
lines changed

drift/core/communication/communicator.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -612,31 +612,7 @@ def _extract_response_data(self, struct: Any) -> dict[str, Any]:
612612
return {}
613613

614614
try:
615-
616-
def value_to_python(value):
617-
"""Convert protobuf Value to Python type."""
618-
if hasattr(value, "null_value"):
619-
return None
620-
elif hasattr(value, "number_value"):
621-
return value.number_value
622-
elif hasattr(value, "string_value"):
623-
return value.string_value
624-
elif hasattr(value, "bool_value"):
625-
return value.bool_value
626-
elif hasattr(value, "struct_value") and value.struct_value:
627-
return struct_to_dict(value.struct_value)
628-
elif hasattr(value, "list_value") and value.list_value:
629-
return [value_to_python(v) for v in value.list_value.values]
630-
return None
631-
632-
def struct_to_dict(s):
633-
"""Convert protobuf Struct to Python dict."""
634-
if not hasattr(s, "fields"):
635-
return {}
636-
result = {}
637-
for key, value in s.fields.items():
638-
result[key] = value_to_python(value)
639-
return result
615+
from ..protobuf_utils import struct_to_dict
640616

641617
data = struct_to_dict(struct)
642618

drift/core/communication/types.py

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ def extract_response_data(struct: Any) -> dict[str, Any]:
379379
380380
The CLI returns response data wrapped in a Struct with a "response" field.
381381
"""
382+
from ..protobuf_utils import value_to_python
383+
382384
try:
383385
# Handle betterproto dict-like struct
384386
if hasattr(struct, "items"):
@@ -391,8 +393,8 @@ def extract_response_data(struct: Any) -> dict[str, Any]:
391393
if hasattr(struct, "fields"):
392394
fields = struct.fields
393395
if "response" in fields:
394-
return _value_to_python(fields["response"])
395-
return {k: _value_to_python(v) for k, v in fields.items()}
396+
return value_to_python(fields["response"])
397+
return {k: value_to_python(v) for k, v in fields.items()}
396398

397399
# Direct dict access
398400
if isinstance(struct, dict):
@@ -403,20 +405,3 @@ def extract_response_data(struct: Any) -> dict[str, Any]:
403405
return {}
404406
except Exception:
405407
return {}
406-
407-
408-
def _value_to_python(value: Any) -> Any:
409-
"""Convert a protobuf Value to Python native type."""
410-
if hasattr(value, "null_value"):
411-
return None
412-
if hasattr(value, "number_value"):
413-
return value.number_value
414-
if hasattr(value, "string_value"):
415-
return value.string_value
416-
if hasattr(value, "bool_value"):
417-
return value.bool_value
418-
if hasattr(value, "struct_value"):
419-
return {k: _value_to_python(v) for k, v in value.struct_value.fields.items()}
420-
if hasattr(value, "list_value"):
421-
return [_value_to_python(v) for v in value.list_value.values]
422-
return value

drift/core/protobuf_utils.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Protobuf conversion utilities.
2+
3+
This module provides utilities for converting protobuf Value and Struct objects
4+
to Python native types. Handles both Google protobuf and betterproto variants.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Any
10+
11+
12+
def value_to_python(value: Any) -> Any:
13+
"""Convert protobuf Value or Python native type to Python native type.
14+
15+
Handles:
16+
1. Python native types (from betterproto's Struct which stores values directly)
17+
2. Google protobuf Value objects (from google.protobuf.struct_pb2)
18+
3. betterproto Value objects (from betterproto.lib.google.protobuf)
19+
20+
Args:
21+
value: A protobuf Value, native Python type, or nested structure
22+
23+
Returns:
24+
Python native type (None, bool, int, float, str, list, or dict)
25+
"""
26+
# Handle Python native types (from betterproto or already converted)
27+
if value is None:
28+
return None
29+
if isinstance(value, (bool, int, float, str)):
30+
# Note: bool must be checked before int since bool is a subclass of int
31+
return value
32+
if isinstance(value, list):
33+
return [value_to_python(v) for v in value]
34+
if isinstance(value, dict):
35+
return {k: value_to_python(v) for k, v in value.items()}
36+
37+
# Handle Google protobuf Value objects using WhichOneof
38+
if hasattr(value, "WhichOneof"):
39+
kind = value.WhichOneof("kind")
40+
if kind == "null_value":
41+
return None
42+
elif kind == "number_value":
43+
return value.number_value
44+
elif kind == "string_value":
45+
return value.string_value
46+
elif kind == "bool_value":
47+
return value.bool_value
48+
elif kind == "struct_value":
49+
return struct_to_dict(value.struct_value)
50+
elif kind == "list_value":
51+
return [value_to_python(v) for v in value.list_value.values]
52+
53+
# Handle betterproto Value objects using is_set method
54+
if hasattr(value, "is_set"):
55+
try:
56+
if value.is_set("null_value"):
57+
return None
58+
elif value.is_set("number_value"):
59+
return value.number_value
60+
elif value.is_set("string_value"):
61+
return value.string_value
62+
elif value.is_set("bool_value"):
63+
return value.bool_value
64+
elif value.is_set("struct_value"):
65+
sv = value.struct_value
66+
if isinstance(sv, dict):
67+
return {k: value_to_python(v) for k, v in sv.get("fields", sv).items()}
68+
return struct_to_dict(sv)
69+
elif value.is_set("list_value"):
70+
lv = value.list_value
71+
# Handle dict-style list_value (betterproto can store dicts)
72+
if isinstance(lv, dict):
73+
return [value_to_python(v) for v in lv.get("values", [])]
74+
return [value_to_python(v) for v in lv.values]
75+
except (AttributeError, TypeError):
76+
pass # Not a betterproto Value, fall through
77+
78+
# Fallback: return the value as-is
79+
return value
80+
81+
82+
def struct_to_dict(struct: Any) -> dict[str, Any]:
83+
"""Convert protobuf Struct to Python dict.
84+
85+
Args:
86+
struct: A protobuf Struct object (Google or betterproto)
87+
88+
Returns:
89+
Python dict with converted values
90+
"""
91+
if not struct:
92+
return {}
93+
94+
# Handle dict-like struct (betterproto sometimes returns dicts directly)
95+
if isinstance(struct, dict):
96+
return {k: value_to_python(v) for k, v in struct.items()}
97+
98+
# Handle struct with fields attribute
99+
if hasattr(struct, "fields"):
100+
return {k: value_to_python(v) for k, v in struct.fields.items()}
101+
102+
return {}

drift/instrumentation/django/csrf_utils.py

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)